diff --git a/app/client/hooks/auth.tsx b/app/client/hooks/auth.tsx new file mode 100644 index 0000000..0e83fa6 --- /dev/null +++ b/app/client/hooks/auth.tsx @@ -0,0 +1,83 @@ +import { gql } from '@apollo/client'; +import { Session } from '@generated/types'; +import { createContext, useContext } from 'react'; +import { sessionFragment } from '../fragments'; +import { useToastMutation } from '../hooks/useToastMutation'; +import { url } from '../main'; + +interface AuthContextProps { + headers: Session | null; + setHeaders: (session: Session) => void; +} + +export const AuthContext = createContext(null); +export const AuthProvider = AuthContext.Provider; + +interface UseAuthOutput extends AuthContextProps { + isAuthenticated: boolean; + login: (value: { email: string; password: string }) => Promise; +} + +export function useAuth(): UseAuthOutput { + const authContext = useContext(AuthContext); + const { update: login } = useToastMutation<{ login: Session }>({ + mutation: loginMutation, + successMsg: 'Logged in successfully.' + }); + + if (!authContext) throw new Error('useAuth must be used within an AuthProvider'); + + return { + ...authContext, + isAuthenticated: authContext.headers != null, + login: async ({ email, password }: { email: string; password: string }) => { + const { data } = await login({ + variables: { + input: { + email, + password + } + } + }); + + if (data != null) { + authContext.setHeaders(data.login); + return data.login; + } + return data; + } + }; +} + +interface UseRestAuthOutput { + refreshToken: () => Promise; +} + +export function useRestAuth(): UseRestAuthOutput { + return { + refreshToken: async () => { + try { + const response = await fetch(`${url}/api/refreshAccessToken`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error(`[error]: ${response.status}`); + } + const session = await response.json(); + return session; + } catch (error) { + console.error(error); + } + } + }; +} + +const loginMutation = gql` + mutation AuthLogin($input: LoginInput!) { + login(input: $input) { + ...Session + } + } + + ${sessionFragment} +`; diff --git a/app/client/hooks/useToastMutation.tsx b/app/client/hooks/useToastMutation.tsx index dacbfd9..d1c0ea7 100644 --- a/app/client/hooks/useToastMutation.tsx +++ b/app/client/hooks/useToastMutation.tsx @@ -32,7 +32,5 @@ export function useToastMutation(props: UseToastMutationProps): UseToastMutat } }); - return { - update - }; + return { update }; } diff --git a/app/client/main.tsx b/app/client/main.tsx index 895b468..d8919d0 100644 --- a/app/client/main.tsx +++ b/app/client/main.tsx @@ -1,19 +1,24 @@ import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, from } from '@apollo/client'; +import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev'; import { onError } from '@apollo/client/link/error'; +import { Skeleton } from '@chakra-ui/react'; import '@client/assets/styles/index.css'; import '@client/assets/styles/nerd-fonts-icons.css'; -import { Provider } from '@client/components/ui/provider'; +import { Provider as ChakraProvider } from '@client/components/ui/provider'; import { Toaster } from '@client/components/ui/toaster'; import { RouterProvider, createRouter } from '@tanstack/react-router'; -import { StrictMode, createContext, useState } from 'react'; +import { StrictMode, useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; +import { AuthProvider, useRestAuth } from './hooks/auth'; // TanStackRouter - Import the generated route tree -import { Session } from 'inspector'; +import { Session } from '@generated/types'; import { routeTree } from './routeTree.gen'; // Create a new router instance -const router = createRouter({ routeTree }); +const router = createRouter({ + routeTree +}); // Register the router instance for type safety declare module '@tanstack/react-router' { @@ -28,35 +33,63 @@ const isProd = MODE === 'prod'; const HOST = import.meta.env.VITE_HOST || 'localhost'; const PORT = import.meta.env.VITE_PORT || '3000'; -const uri = `https://${HOST}:${PORT}/graphql`; - -// Auth context -export const AuthContext = createContext(null); +export const url = `https://${HOST}:${PORT}`; +const graphql_uri = `${url}/graphql`; function App() { const [authHeaders, setAuthHeaders] = useState(null); - const AuthProvider = AuthContext.Provider; + const [initialLoading, setInitialLoading] = useState(true); + const { refreshToken } = useRestAuth(); - // if (authHeaders == null) refreshToken(); + useEffect(() => { + const getPreviousSession = async () => { + const session = await refreshToken(); + if (session !== undefined) setAuthHeaders(session); + setInitialLoading(false); + }; + if (initialLoading) getPreviousSession(); + }, []); + + if (initialLoading) + return ( + + + + + + ); const httpLink = new HttpLink({ - uri, + uri: graphql_uri, credentials: 'include', headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined }); - const errorLink = onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) - graphQLErrors.forEach(({ message, locations, path }) => - console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) - ); + const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => { + if (graphQLErrors != null && graphQLErrors.length > 0) + graphQLErrors.map(async error => { + if (error.message.includes('jwt expired')) { + const session = await refreshToken(); + if (session != undefined) setAuthHeaders(session); + return forward(operation); + } + if (!isProd) + console.error( + `[GraphQlError]: Message: ${error.message}, Location: ${JSON.stringify(error.locations)}, Path: ${error.path}` + ); + }); - if (networkError) console.error(`[Network error]: ${networkError}`); + if (networkError != null && graphQLErrors == null) console.error(networkError); }); + if (!isProd) { + loadDevMessages(); + loadErrorMessages(); + } + const client = new ApolloClient({ - uri, - link: isProd ? undefined : from([errorLink, httpLink]), + uri: graphql_uri, + link: from([errorLink, httpLink]), cache: new InMemoryCache(), connectToDevTools: !isProd, credentials: 'include' @@ -66,17 +99,17 @@ function App() { { setAuthHeaders(session); } }} > - + - + diff --git a/app/client/pages/index.tsx b/app/client/pages/index.tsx index 6ba1e0e..30e9780 100644 --- a/app/client/pages/index.tsx +++ b/app/client/pages/index.tsx @@ -1,4 +1,4 @@ -import { Stack, Text } from '@chakra-ui/react'; +import { Box, Button, Stack, Text } from '@chakra-ui/react'; import { createFileRoute } from '@tanstack/react-router'; import { gql, useQuery } from '@apollo/client'; @@ -10,7 +10,7 @@ export const Route = createFileRoute('/')({ }); function Index() { - const { data } = useQuery(getBooksQuery); + const { data, refetch } = useQuery(getBooksQuery); return ( @@ -27,6 +27,9 @@ function Index() { ))} )} + + + ); diff --git a/app/client/pages/login.tsx b/app/client/pages/login.tsx index f7f5bb9..c523ac9 100644 --- a/app/client/pages/login.tsx +++ b/app/client/pages/login.tsx @@ -1,17 +1,12 @@ -import { gql } from '@apollo/client'; import { Heading, Input, Stack } from '@chakra-ui/react'; import { Button } from '@client/components/ui/button'; import { Field } from '@client/components/ui/field'; import { valibotResolver } from '@hookform/resolvers/valibot'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { Session } from 'inspector'; -import { useContext } from 'react'; import { useForm } from 'react-hook-form'; import * as v from 'valibot'; import { PasswordInput } from '../components/ui/password-input'; -import { sessionFragment } from '../fragments'; -import { useToastMutation } from '../hooks/useToastMutation'; -import { AuthContext } from '../main'; +import { useAuth } from '../hooks/auth'; export const Route = createFileRoute('/login')({ component: LoginComponent @@ -33,10 +28,6 @@ interface FormValues { function LoginForm() { const navigate = useNavigate(); - const { update: login } = useToastMutation<{ login: Session }>({ - mutation, - successMsg: 'Logged in successfully.' - }); const schema = v.pipe( v.object({ @@ -51,24 +42,16 @@ function LoginForm() { formState: { errors } } = useForm({ resolver: valibotResolver(schema) }); - const { header, setHeaders } = useContext(AuthContext); - if (header != null) navigate({ to: '/' }); + const { login } = useAuth(); return (
{ - const { data } = await login({ - variables: { - input: { - email: values.email, - password: values.password - } - } + const session = await login({ + email: values.email, + password: values.password }); - if (data != null) { - setHeaders(data.login); - navigate({ to: '/' }); - } + if (session != null) navigate({ to: '/' }); })} > @@ -87,13 +70,3 @@ function LoginForm() { ); } - -const mutation = gql` - mutation AuthLogin($input: LoginInput!) { - login(input: $input) { - ...Session - } - } - - ${sessionFragment} -`; diff --git a/app/server/config.ts b/app/server/config.ts new file mode 100644 index 0000000..d69d21c --- /dev/null +++ b/app/server/config.ts @@ -0,0 +1,74 @@ +import cors from 'cors'; +import 'dotenv/config'; + +interface AppConfig { + readonly isProd: boolean; + readonly host: string; + readonly port: number; + readonly limit: { readonly limit: string }; + readonly origin: string[]; + readonly corsOptions: cors.CorsOptions; + readonly greeting: string; + readonly accessToken: TokenConfig; + readonly refreshToken: RefreshTokenConfig; +} + +interface TokenConfig { + readonly secret: string; + readonly expiresIn: number; +} + +interface RefreshTokenConfig extends TokenConfig { + readonly maxAge: number; +} + +export function toAppConfig(): AppConfig { + const MODE = process.env.VITE_MODE || 'prod'; + const isProd = MODE === 'prod'; + + const host = process.env.VITE_HOST || 'localhost'; + const port = parseInt(process.env.VITE_PORT || '3000'); + + const urlRemote = `https://${host}:${port}`; + const urlLocal = host !== 'localhost' ? `https://localhost:${port}` : undefined; + + const limit = { limit: '50mb' }; + + const origin = urlLocal != null ? [urlLocal, urlRemote] : [urlRemote]; + const corsOptions: cors.CorsOptions = { + origin, + credentials: true + }; + + const accessTokenExpiresIn = 24 * 60 * 60; + const refreshTokenExpiresIn = 15 * 24 * 60 * 60; + + const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; + const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET; + if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found'); + + const accessToken: TokenConfig = { + secret: accessTokenSecret, + expiresIn: accessTokenExpiresIn + }; + + const refreshToken: RefreshTokenConfig = { + secret: refreshTokenSecret, + expiresIn: refreshTokenExpiresIn, + maxAge: refreshTokenExpiresIn * 1000 + }; + + const greeting = `\n\n\n\x1b[35m-server is listening at...-\x1b[0m\n\n${urlRemote}\n${urlLocal}\n\n\n`; + + return { + isProd, + host, + port, + limit, + origin, + corsOptions, + accessToken, + refreshToken, + greeting + }; +} diff --git a/app/server/express/auth/refreshToken.ts b/app/server/express/auth/refreshToken.ts new file mode 100644 index 0000000..b790dc8 --- /dev/null +++ b/app/server/express/auth/refreshToken.ts @@ -0,0 +1,51 @@ +import { config } from '@app/server/main'; +import { prisma } from '@app/server/prisma/client'; +import { Session } from '@generated/types'; +import 'dotenv/config'; +import { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; + +export async function refreshTokenController(req: Request, res: Response) { + try { + const cookie: { jwt?: string | null } = req.cookies; + + if (cookie == null || cookie.jwt == null) { + res.sendStatus(401); + return; + } + + const refreshToken = cookie.jwt; + + const user = await prisma.user.findUnique({ + where: { refreshToken }, + select: { id: true } + }); + + if (user == null) { + res.statusMessage = 'User not found.'; + res.status(401).end(); + return; + } + + const decoded = jwt.verify(refreshToken, config.refreshToken.secret) as { + userId: string; + iat: number; + exp: number; + }; + + if (user.id !== decoded.userId) { + res.statusMessage = 'Invalid token.'; + res.status(401).end(); + return; + } + + const accessToken = jwt.sign({ userId: user.id }, config.accessToken.secret, { + expiresIn: config.accessToken.expiresIn + }); + + const session: Session = { accessToken }; + res.json(session); + } catch (error) { + console.error(error); + } +} diff --git a/app/server/graphql/graphql.ts b/app/server/graphql/graphql.ts index c62d6c4..55eee78 100644 --- a/app/server/graphql/graphql.ts +++ b/app/server/graphql/graphql.ts @@ -1,22 +1,24 @@ /* gql imports */ -import { QueryResolvers } from '@generated/types'; +import { MutationResolvers, QueryResolvers } from '@generated/types'; /* Query */ import { BooksManyResolver } from './resolvers/booksManyResolver'; import { BookOneResolver } from './resolvers/bookOneResolver'; /* Mutation */ -import { LoginResolver } from './resolvers/auth/login'; import { SignUpResolver } from './resolvers/auth/signUp'; +import { LoginResolver } from './resolvers/auth/login'; +import { LogoutResolver } from './resolvers/auth/logout'; export const Query: QueryResolvers = { booksMany: BooksManyResolver, bookOne: BookOneResolver }; -export const Mutation = { +export const Mutation: MutationResolvers = { + signUp: SignUpResolver, login: LoginResolver, - signUp: SignUpResolver + logout: LogoutResolver }; export const Subscription = {}; diff --git a/app/server/graphql/resolvers/auth/login.ts b/app/server/graphql/resolvers/auth/login.ts index c43c8ba..a0e11a7 100644 --- a/app/server/graphql/resolvers/auth/login.ts +++ b/app/server/graphql/resolvers/auth/login.ts @@ -3,6 +3,7 @@ import { prisma } from '@app/server/prisma/client'; import { MutationLoginArgs, Session } from '@generated/types'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; +import { config } from '@app/server/main'; export async function LoginResolver( _: any, @@ -19,16 +20,11 @@ export async function LoginResolver( const match = await bcrypt.compare(input.password, user.password); if (!match) throw new Error(error); - const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; - const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET; - - if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found'); - - const accessToken = jwt.sign({ userId: user.id }, accessTokenSecret, { - expiresIn: '30s' + const accessToken = jwt.sign({ userId: user.id }, config.accessToken.secret, { + expiresIn: config.accessToken.expiresIn }); - const refreshToken = jwt.sign({ userId: user.id }, refreshTokenSecret, { - expiresIn: '1d' + const refreshToken = jwt.sign({ userId: user.id }, config.refreshToken.secret, { + expiresIn: config.refreshToken.expiresIn }); await prisma.user.update({ @@ -40,17 +36,10 @@ export async function LoginResolver( httpOnly: true, sameSite: 'none', secure: true, - maxAge: 24 * 60 * 60 * 1000 + maxAge: config.refreshToken.maxAge }); return { accessToken - // refreshToken: { - // token: refreshToken, - // httpOnly: true, - // secure: true, - // maxAge: 24 * 60 * 60 * 1000, - // sameSite: 'None' - // } }; } diff --git a/app/server/graphql/resolvers/auth/logout.ts b/app/server/graphql/resolvers/auth/logout.ts index e69de29..5de1fbd 100644 --- a/app/server/graphql/resolvers/auth/logout.ts +++ b/app/server/graphql/resolvers/auth/logout.ts @@ -0,0 +1,32 @@ +import { IdentityContext } from '@app/server/identity'; +import { prisma } from '@app/server/prisma/client'; + +export async function LogoutResolver( + _: any, + __: any, + { req, res, identity }: IdentityContext +): Promise { + const cookie: { jwt?: string | null } = req.cookies; + if (cookie == null || cookie.jwt == null) return; + + const refreshToken = cookie.jwt; + + const user = await prisma.user.findUnique({ + where: { refreshToken }, + select: { id: true } + }); + + if (user == null) { + res.clearCookie('jwt', { httpOnly: true }); + return; + } + + await prisma.user.update({ + where: { + id: user.id + }, + data: { refreshToken: null } + }); + + res.clearCookie('jwt', { httpOnly: true }); +} diff --git a/app/server/graphql/resolvers/auth/refreshToken.ts b/app/server/graphql/resolvers/auth/refreshToken.ts deleted file mode 100644 index a52c6fa..0000000 --- a/app/server/graphql/resolvers/auth/refreshToken.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IdentityContext } from '@app/server/identity'; -import { prisma } from '@app/server/prisma/client'; -import { Session } from '@generated/types'; -import jwt from 'jsonwebtoken'; - -export async function RefreshTokenResolver( - _: any, - { req, res, identity }: IdentityContext -): Promise { - const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; - const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET; - if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found.'); - - const cookie: { jwt?: string | null } = req.cookies; - if (cookie == null || cookie.jwt == null) throw new Error('Unauthorized.'); - - const refreshToken = cookie.jwt; - - const user = await prisma.user.findUniqueOrThrow({ - where: { refreshToken }, - select: { id: true } - }); - - const decoded = jwt.verify(refreshToken, accessTokenSecret) as { - userId: string; - iat: number; - exp: number; - }; - - if (user.id !== decoded.userId) throw new Error('Invalid token.'); - const accessToken = jwt.sign({ userId: user.id }, accessTokenSecret, { - expiresIn: '30s' - }); - - return { accessToken }; -} diff --git a/app/server/graphql/resolvers/auth/signUp.ts b/app/server/graphql/resolvers/auth/signUp.ts index a2ad7b6..a4809da 100644 --- a/app/server/graphql/resolvers/auth/signUp.ts +++ b/app/server/graphql/resolvers/auth/signUp.ts @@ -6,7 +6,7 @@ import bcrypt from 'bcrypt'; export async function SignUpResolver( _: any, { input }: MutationSignUpArgs, - { identity }: IdentityContext + { req, res, identity }: IdentityContext ): Promise { const existingUsername = await prisma.user.findUnique({ where: { username: input.username }, diff --git a/app/server/graphql/resolvers/booksManyResolver.ts b/app/server/graphql/resolvers/booksManyResolver.ts index b4ac9ec..5cf16dc 100644 --- a/app/server/graphql/resolvers/booksManyResolver.ts +++ b/app/server/graphql/resolvers/booksManyResolver.ts @@ -17,8 +17,9 @@ export const books = [ export async function BooksManyResolver( _: any, __: any, - { identity }: IdentityContext + { req, res, identity }: IdentityContext ): Promise { if (identity == null) throw new Error('Unauthorized'); + return books; } diff --git a/app/server/graphql/schema.graphql b/app/server/graphql/schema.graphql index ef31561..f2200c4 100644 --- a/app/server/graphql/schema.graphql +++ b/app/server/graphql/schema.graphql @@ -6,13 +6,9 @@ type Query { } type Mutation { - login(input: LoginInput!): Session! signUp(input: SignUpInput!): Void -} - -input LoginInput { - email: String! - password: String! + login(input: LoginInput!): Session! + logout: Void } input SignUpInput { @@ -21,16 +17,13 @@ input SignUpInput { password: String! } -type Session { - accessToken: String! +input LoginInput { + email: String! + password: String! } -type RefreshToken { - token: String! - maxAge: Int! - secure: Boolean! - httpOnly: Boolean! - sameSite: String! +type Session { + accessToken: String! } type Book { diff --git a/app/server/identity.ts b/app/server/identity.ts index 8472388..14e26bc 100644 --- a/app/server/identity.ts +++ b/app/server/identity.ts @@ -1,8 +1,10 @@ +import { Session } from '@generated/types'; import 'dotenv/config'; import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; +import { config } from './main'; import { prisma } from './prisma/client'; -import { Session } from '@generated/types'; +import { GraphQLError } from 'graphql'; export interface IdentityContext { req: Request; @@ -20,38 +22,42 @@ export async function getIdentityContext({ req: Request; res: Response; }): Promise { - const authHeaders = req.headers.authorization; - if (authHeaders == null) return { req, res }; + try { + const authHeaders = req.headers.authorization; + if (authHeaders == null) return { req, res }; - const session: Session = JSON.parse(authHeaders); - const { accessToken } = session; + const session: Session = JSON.parse(authHeaders); + const { accessToken } = session; - const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; - if (accessTokenSecret == null) throw new Error('No `accessToken`.'); + const decoded = jwt.verify(accessToken, config.accessToken.secret) as { + userId: string; + iat: number; + exp: number; + }; - const decoded = jwt.verify(accessToken, accessTokenSecret) as { - userId: string; - iat: number; - exp: number; - }; - - const identity = await prisma.user.findUnique({ - where: { id: decoded.userId }, - select: { - id: true, - email: true, - username: true - } - }); - if (identity == null) throw new Error("Couldn't find the user."); - return { req, res, identity }; -} - -/* throw new GraphQLError('User is not authenticated', { + const identity = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + email: true, + username: true + } + }); + if (identity == null) throw new Error("Couldn't find the user."); + return { req, res, identity }; + } catch (error) { + if (error instanceof Error) + throw new GraphQLError(error.message, { + extensions: { + code: 'UNAUTHENTICATED', + http: { status: 401 } + } + }); + throw new GraphQLError(typeof error === 'string' ? error : 'Unknown error', { extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } } - }); */ - -// throw new GraphQLError(`${error}`); + }); + } +} diff --git a/app/server/main.ts b/app/server/main.ts index 25a731b..b6f221a 100644 --- a/app/server/main.ts +++ b/app/server/main.ts @@ -1,6 +1,7 @@ import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; +import cookies from 'cookie-parser'; import cors from 'cors'; import 'dotenv/config'; import express from 'express'; @@ -8,17 +9,14 @@ import fs from 'fs'; import https from 'https'; import path from 'path'; import ViteExpress from 'vite-express'; +import { toAppConfig } from './config'; +import { refreshTokenController } from './express/auth/refreshToken'; import { Mutation, Query } from './graphql/graphql'; import { getIdentityContext, IdentityContext } from './identity'; -const MODE = process.env.VITE_MODE || 'prod'; -const isProd = MODE === 'prod'; +export const config = toAppConfig(); const __dirname = import.meta.dirname; -const HOST = process.env.VITE_HOST || 'localhost'; -const PORT = parseInt(process.env.VITE_PORT || '3000'); -const MOTD = `\n\n\n\x1b[35m-server is listening at...-\x1b[0m\n\nhttp://${HOST}:${PORT}\nhttp://localhost:${PORT}\n\n\n`; - const key = fs.readFileSync(path.join(__dirname + '../../../certs/selfsigned.key')); const cert = fs.readFileSync(path.join(__dirname + '../../../certs/selfsigned.crt')); @@ -36,7 +34,7 @@ const resolvers = { const apollo = new ApolloServer({ typeDefs, resolvers, - introspection: !isProd, + introspection: !config.isProd, plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })], formatError: formattedError => { process.stdout.write('\n```\n'); @@ -48,17 +46,21 @@ const apollo = new ApolloServer({ await apollo.start(); +app.use(cors(config.corsOptions)); +app.use(express.json(config.limit)); +app.use(cookies()); +app.get('/api/refreshAccessToken', refreshTokenController); app.use( '/graphql', - cors(), - express.json(), expressMiddleware(apollo, { context: getIdentityContext }) ); -await new Promise(resolve => server.listen({ hostname: HOST, port: PORT }, resolve)); +await new Promise(resolve => + server.listen({ hostname: config.host, port: config.port }, resolve) +); -process.stdout.write(MOTD); +process.stdout.write(config.greeting); ViteExpress.bind(app, server); diff --git a/package-lock.json b/package-lock.json index a55198a..ce2e7df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@tanstack/router-devtools": "^1.85.0", "@tanstack/router-plugin": "^1.84.4", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.9.3", @@ -5770,6 +5771,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", diff --git a/package.json b/package.json index a9f64ec..91281b7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@prisma/client": "^6.4.1", "@tanstack/react-router": "^1.85.0", "@tanstack/router-vite-plugin": "^1.84.4", - "@types/cors": "^2.8.17", "apollo-link-logger": "^2.0.1", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.7", @@ -54,6 +53,8 @@ "@tanstack/router-devtools": "^1.85.0", "@tanstack/router-plugin": "^1.84.4", "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.9.3",