re-implemented JWT auth

This commit is contained in:
lnn0q 2025-04-18 14:47:42 +03:00
parent c190d368de
commit b47b0f3752
18 changed files with 390 additions and 174 deletions

83
app/client/hooks/auth.tsx Normal file
View file

@ -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<AuthContextProps | null>(null);
export const AuthProvider = AuthContext.Provider;
interface UseAuthOutput extends AuthContextProps {
isAuthenticated: boolean;
login: (value: { email: string; password: string }) => Promise<Session | null | undefined>;
}
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<Session | null | undefined>;
}
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}
`;

View file

@ -32,7 +32,5 @@ export function useToastMutation<T>(props: UseToastMutationProps): UseToastMutat
} }
}); });
return { return { update };
update
};
} }

View file

@ -1,19 +1,24 @@
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, from } from '@apollo/client'; import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, from } from '@apollo/client';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { onError } from '@apollo/client/link/error'; import { onError } from '@apollo/client/link/error';
import { Skeleton } from '@chakra-ui/react';
import '@client/assets/styles/index.css'; import '@client/assets/styles/index.css';
import '@client/assets/styles/nerd-fonts-icons.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 { Toaster } from '@client/components/ui/toaster';
import { RouterProvider, createRouter } from '@tanstack/react-router'; 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 ReactDOM from 'react-dom/client';
import { AuthProvider, useRestAuth } from './hooks/auth';
// TanStackRouter - Import the generated route tree // TanStackRouter - Import the generated route tree
import { Session } from 'inspector'; import { Session } from '@generated/types';
import { routeTree } from './routeTree.gen'; import { routeTree } from './routeTree.gen';
// Create a new router instance // Create a new router instance
const router = createRouter({ routeTree }); const router = createRouter({
routeTree
});
// Register the router instance for type safety // Register the router instance for type safety
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@ -28,35 +33,63 @@ const isProd = MODE === 'prod';
const HOST = import.meta.env.VITE_HOST || 'localhost'; const HOST = import.meta.env.VITE_HOST || 'localhost';
const PORT = import.meta.env.VITE_PORT || '3000'; const PORT = import.meta.env.VITE_PORT || '3000';
const uri = `https://${HOST}:${PORT}/graphql`; export const url = `https://${HOST}:${PORT}`;
const graphql_uri = `${url}/graphql`;
// Auth context
export const AuthContext = createContext<any>(null);
function App() { function App() {
const [authHeaders, setAuthHeaders] = useState<Session | null>(null); const [authHeaders, setAuthHeaders] = useState<Session | null>(null);
const AuthProvider = AuthContext.Provider; const [initialLoading, setInitialLoading] = useState<boolean>(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 (
<StrictMode>
<ChakraProvider>
<Skeleton />
</ChakraProvider>
</StrictMode>
);
const httpLink = new HttpLink({ const httpLink = new HttpLink({
uri, uri: graphql_uri,
credentials: 'include', credentials: 'include',
headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined
}); });
const errorLink = onError(({ graphQLErrors, networkError }) => { const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) if (graphQLErrors != null && graphQLErrors.length > 0)
graphQLErrors.forEach(({ message, locations, path }) => graphQLErrors.map(async error => {
console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) 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({ const client = new ApolloClient({
uri, uri: graphql_uri,
link: isProd ? undefined : from([errorLink, httpLink]), link: from([errorLink, httpLink]),
cache: new InMemoryCache(), cache: new InMemoryCache(),
connectToDevTools: !isProd, connectToDevTools: !isProd,
credentials: 'include' credentials: 'include'
@ -66,17 +99,17 @@ function App() {
<StrictMode> <StrictMode>
<AuthProvider <AuthProvider
value={{ value={{
authHeaders, headers: authHeaders,
setHeaders: (session: Session) => { setHeaders: (session: Session) => {
setAuthHeaders(session); setAuthHeaders(session);
} }
}} }}
> >
<ApolloProvider client={client}> <ApolloProvider client={client}>
<Provider> <ChakraProvider>
<Toaster /> <Toaster />
<RouterProvider router={router} /> <RouterProvider router={router} />
</Provider> </ChakraProvider>
</ApolloProvider> </ApolloProvider>
</AuthProvider> </AuthProvider>
</StrictMode> </StrictMode>

View file

@ -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 { createFileRoute } from '@tanstack/react-router';
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
@ -10,7 +10,7 @@ export const Route = createFileRoute('/')({
}); });
function Index() { function Index() {
const { data } = useQuery<BooksManyQuery>(getBooksQuery); const { data, refetch } = useQuery<BooksManyQuery>(getBooksQuery);
return ( return (
<Stack> <Stack>
@ -27,6 +27,9 @@ function Index() {
))} ))}
</> </>
)} )}
<Box>
<Button onClick={async () => refetch()}>Refetch</Button>
</Box>
</Stack> </Stack>
</Stack> </Stack>
); );

View file

@ -1,17 +1,12 @@
import { gql } from '@apollo/client';
import { Heading, Input, Stack } from '@chakra-ui/react'; import { Heading, Input, Stack } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button'; import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field'; import { Field } from '@client/components/ui/field';
import { valibotResolver } from '@hookform/resolvers/valibot'; import { valibotResolver } from '@hookform/resolvers/valibot';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { Session } from 'inspector';
import { useContext } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import * as v from 'valibot'; import * as v from 'valibot';
import { PasswordInput } from '../components/ui/password-input'; import { PasswordInput } from '../components/ui/password-input';
import { sessionFragment } from '../fragments'; import { useAuth } from '../hooks/auth';
import { useToastMutation } from '../hooks/useToastMutation';
import { AuthContext } from '../main';
export const Route = createFileRoute('/login')({ export const Route = createFileRoute('/login')({
component: LoginComponent component: LoginComponent
@ -33,10 +28,6 @@ interface FormValues {
function LoginForm() { function LoginForm() {
const navigate = useNavigate(); const navigate = useNavigate();
const { update: login } = useToastMutation<{ login: Session }>({
mutation,
successMsg: 'Logged in successfully.'
});
const schema = v.pipe( const schema = v.pipe(
v.object({ v.object({
@ -51,24 +42,16 @@ function LoginForm() {
formState: { errors } formState: { errors }
} = useForm<FormValues>({ resolver: valibotResolver(schema) }); } = useForm<FormValues>({ resolver: valibotResolver(schema) });
const { header, setHeaders } = useContext(AuthContext); const { login } = useAuth();
if (header != null) navigate({ to: '/' });
return ( return (
<form <form
onSubmit={handleSubmit(async values => { onSubmit={handleSubmit(async values => {
const { data } = await login({ const session = await login({
variables: { email: values.email,
input: { password: values.password
email: values.email,
password: values.password
}
}
}); });
if (data != null) { if (session != null) navigate({ to: '/' });
setHeaders(data.login);
navigate({ to: '/' });
}
})} })}
> >
<Stack> <Stack>
@ -87,13 +70,3 @@ function LoginForm() {
</form> </form>
); );
} }
const mutation = gql`
mutation AuthLogin($input: LoginInput!) {
login(input: $input) {
...Session
}
}
${sessionFragment}
`;

74
app/server/config.ts Normal file
View file

@ -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
};
}

View file

@ -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);
}
}

View file

@ -1,22 +1,24 @@
/* gql imports */ /* gql imports */
import { QueryResolvers } from '@generated/types'; import { MutationResolvers, QueryResolvers } from '@generated/types';
/* Query */ /* Query */
import { BooksManyResolver } from './resolvers/booksManyResolver'; import { BooksManyResolver } from './resolvers/booksManyResolver';
import { BookOneResolver } from './resolvers/bookOneResolver'; import { BookOneResolver } from './resolvers/bookOneResolver';
/* Mutation */ /* Mutation */
import { LoginResolver } from './resolvers/auth/login';
import { SignUpResolver } from './resolvers/auth/signUp'; import { SignUpResolver } from './resolvers/auth/signUp';
import { LoginResolver } from './resolvers/auth/login';
import { LogoutResolver } from './resolvers/auth/logout';
export const Query: QueryResolvers = { export const Query: QueryResolvers = {
booksMany: BooksManyResolver, booksMany: BooksManyResolver,
bookOne: BookOneResolver bookOne: BookOneResolver
}; };
export const Mutation = { export const Mutation: MutationResolvers = {
signUp: SignUpResolver,
login: LoginResolver, login: LoginResolver,
signUp: SignUpResolver logout: LogoutResolver
}; };
export const Subscription = {}; export const Subscription = {};

View file

@ -3,6 +3,7 @@ import { prisma } from '@app/server/prisma/client';
import { MutationLoginArgs, Session } from '@generated/types'; import { MutationLoginArgs, Session } from '@generated/types';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { config } from '@app/server/main';
export async function LoginResolver( export async function LoginResolver(
_: any, _: any,
@ -19,16 +20,11 @@ export async function LoginResolver(
const match = await bcrypt.compare(input.password, user.password); const match = await bcrypt.compare(input.password, user.password);
if (!match) throw new Error(error); if (!match) throw new Error(error);
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; const accessToken = jwt.sign({ userId: user.id }, config.accessToken.secret, {
const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET; expiresIn: config.accessToken.expiresIn
if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found');
const accessToken = jwt.sign({ userId: user.id }, accessTokenSecret, {
expiresIn: '30s'
}); });
const refreshToken = jwt.sign({ userId: user.id }, refreshTokenSecret, { const refreshToken = jwt.sign({ userId: user.id }, config.refreshToken.secret, {
expiresIn: '1d' expiresIn: config.refreshToken.expiresIn
}); });
await prisma.user.update({ await prisma.user.update({
@ -40,17 +36,10 @@ export async function LoginResolver(
httpOnly: true, httpOnly: true,
sameSite: 'none', sameSite: 'none',
secure: true, secure: true,
maxAge: 24 * 60 * 60 * 1000 maxAge: config.refreshToken.maxAge
}); });
return { return {
accessToken accessToken
// refreshToken: {
// token: refreshToken,
// httpOnly: true,
// secure: true,
// maxAge: 24 * 60 * 60 * 1000,
// sameSite: 'None'
// }
}; };
} }

View file

@ -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<void> {
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 });
}

View file

@ -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<Session> {
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 };
}

View file

@ -6,7 +6,7 @@ import bcrypt from 'bcrypt';
export async function SignUpResolver( export async function SignUpResolver(
_: any, _: any,
{ input }: MutationSignUpArgs, { input }: MutationSignUpArgs,
{ identity }: IdentityContext { req, res, identity }: IdentityContext
): Promise<void> { ): Promise<void> {
const existingUsername = await prisma.user.findUnique({ const existingUsername = await prisma.user.findUnique({
where: { username: input.username }, where: { username: input.username },

View file

@ -17,8 +17,9 @@ export const books = [
export async function BooksManyResolver( export async function BooksManyResolver(
_: any, _: any,
__: any, __: any,
{ identity }: IdentityContext { req, res, identity }: IdentityContext
): Promise<Book[]> { ): Promise<Book[]> {
if (identity == null) throw new Error('Unauthorized'); if (identity == null) throw new Error('Unauthorized');
return books; return books;
} }

View file

@ -6,13 +6,9 @@ type Query {
} }
type Mutation { type Mutation {
login(input: LoginInput!): Session!
signUp(input: SignUpInput!): Void signUp(input: SignUpInput!): Void
} login(input: LoginInput!): Session!
logout: Void
input LoginInput {
email: String!
password: String!
} }
input SignUpInput { input SignUpInput {
@ -21,16 +17,13 @@ input SignUpInput {
password: String! password: String!
} }
type Session { input LoginInput {
accessToken: String! email: String!
password: String!
} }
type RefreshToken { type Session {
token: String! accessToken: String!
maxAge: Int!
secure: Boolean!
httpOnly: Boolean!
sameSite: String!
} }
type Book { type Book {

View file

@ -1,8 +1,10 @@
import { Session } from '@generated/types';
import 'dotenv/config'; import 'dotenv/config';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { config } from './main';
import { prisma } from './prisma/client'; import { prisma } from './prisma/client';
import { Session } from '@generated/types'; import { GraphQLError } from 'graphql';
export interface IdentityContext { export interface IdentityContext {
req: Request; req: Request;
@ -20,38 +22,42 @@ export async function getIdentityContext({
req: Request; req: Request;
res: Response; res: Response;
}): Promise<IdentityContext> { }): Promise<IdentityContext> {
const authHeaders = req.headers.authorization; try {
if (authHeaders == null) return { req, res }; const authHeaders = req.headers.authorization;
if (authHeaders == null) return { req, res };
const session: Session = JSON.parse(authHeaders); const session: Session = JSON.parse(authHeaders);
const { accessToken } = session; const { accessToken } = session;
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; const decoded = jwt.verify(accessToken, config.accessToken.secret) as {
if (accessTokenSecret == null) throw new Error('No `accessToken`.'); userId: string;
iat: number;
exp: number;
};
const decoded = jwt.verify(accessToken, accessTokenSecret) as { const identity = await prisma.user.findUnique({
userId: string; where: { id: decoded.userId },
iat: number; select: {
exp: number; id: true,
}; email: true,
username: true
const identity = await prisma.user.findUnique({ }
where: { id: decoded.userId }, });
select: { if (identity == null) throw new Error("Couldn't find the user.");
id: true, return { req, res, identity };
email: true, } catch (error) {
username: true if (error instanceof Error)
} throw new GraphQLError(error.message, {
}); extensions: {
if (identity == null) throw new Error("Couldn't find the user."); code: 'UNAUTHENTICATED',
return { req, res, identity }; http: { status: 401 }
} }
});
/* throw new GraphQLError('User is not authenticated', { throw new GraphQLError(typeof error === 'string' ? error : 'Unknown error', {
extensions: { extensions: {
code: 'UNAUTHENTICATED', code: 'UNAUTHENTICATED',
http: { status: 401 } http: { status: 401 }
} }
}); */ });
}
// throw new GraphQLError(`${error}`); }

View file

@ -1,6 +1,7 @@
import { ApolloServer } from '@apollo/server'; import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4'; import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import cookies from 'cookie-parser';
import cors from 'cors'; import cors from 'cors';
import 'dotenv/config'; import 'dotenv/config';
import express from 'express'; import express from 'express';
@ -8,17 +9,14 @@ import fs from 'fs';
import https from 'https'; import https from 'https';
import path from 'path'; import path from 'path';
import ViteExpress from 'vite-express'; import ViteExpress from 'vite-express';
import { toAppConfig } from './config';
import { refreshTokenController } from './express/auth/refreshToken';
import { Mutation, Query } from './graphql/graphql'; import { Mutation, Query } from './graphql/graphql';
import { getIdentityContext, IdentityContext } from './identity'; import { getIdentityContext, IdentityContext } from './identity';
const MODE = process.env.VITE_MODE || 'prod'; export const config = toAppConfig();
const isProd = MODE === 'prod';
const __dirname = import.meta.dirname; 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 key = fs.readFileSync(path.join(__dirname + '../../../certs/selfsigned.key'));
const cert = fs.readFileSync(path.join(__dirname + '../../../certs/selfsigned.crt')); const cert = fs.readFileSync(path.join(__dirname + '../../../certs/selfsigned.crt'));
@ -36,7 +34,7 @@ const resolvers = {
const apollo = new ApolloServer<IdentityContext>({ const apollo = new ApolloServer<IdentityContext>({
typeDefs, typeDefs,
resolvers, resolvers,
introspection: !isProd, introspection: !config.isProd,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })], plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })],
formatError: formattedError => { formatError: formattedError => {
process.stdout.write('\n```\n'); process.stdout.write('\n```\n');
@ -48,17 +46,21 @@ const apollo = new ApolloServer<IdentityContext>({
await apollo.start(); await apollo.start();
app.use(cors<cors.CorsRequest>(config.corsOptions));
app.use(express.json(config.limit));
app.use(cookies());
app.get('/api/refreshAccessToken', refreshTokenController);
app.use( app.use(
'/graphql', '/graphql',
cors<cors.CorsRequest>(),
express.json(),
expressMiddleware(apollo, { expressMiddleware(apollo, {
context: getIdentityContext context: getIdentityContext
}) })
); );
await new Promise<void>(resolve => server.listen({ hostname: HOST, port: PORT }, resolve)); await new Promise<void>(resolve =>
server.listen({ hostname: config.host, port: config.port }, resolve)
);
process.stdout.write(MOTD); process.stdout.write(config.greeting);
ViteExpress.bind(app, server); ViteExpress.bind(app, server);

11
package-lock.json generated
View file

@ -48,6 +48,7 @@
"@tanstack/router-devtools": "^1.85.0", "@tanstack/router-devtools": "^1.85.0",
"@tanstack/router-plugin": "^1.84.4", "@tanstack/router-plugin": "^1.84.4",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.9.3", "@types/node": "^20.9.3",
@ -5770,6 +5771,16 @@
"@types/node": "*" "@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": { "node_modules/@types/cors": {
"version": "2.8.17", "version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",

View file

@ -24,7 +24,6 @@
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@tanstack/react-router": "^1.85.0", "@tanstack/react-router": "^1.85.0",
"@tanstack/router-vite-plugin": "^1.84.4", "@tanstack/router-vite-plugin": "^1.84.4",
"@types/cors": "^2.8.17",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
@ -54,6 +53,8 @@
"@tanstack/router-devtools": "^1.85.0", "@tanstack/router-devtools": "^1.85.0",
"@tanstack/router-plugin": "^1.84.4", "@tanstack/router-plugin": "^1.84.4",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.9.3", "@types/node": "^20.9.3",