feat(backend): add fastify api, auth, prisma schema and jobs
This commit is contained in:
51
apps/backend/src/plugins/auth.ts
Normal file
51
apps/backend/src/plugins/auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import fastifyJwt from '@fastify/jwt';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import { env } from '../config/env.js';
|
||||
import { UnauthorizedError } from '../utils/errors.js';
|
||||
|
||||
export const AUTH_COOKIE = 'fw_auth';
|
||||
const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
||||
|
||||
export default fp(async (app) => {
|
||||
await app.register(fastifyCookie, {
|
||||
secret: env.COOKIE_SECRET,
|
||||
parseOptions: {},
|
||||
});
|
||||
|
||||
await app.register(fastifyJwt, {
|
||||
secret: env.JWT_SECRET,
|
||||
cookie: { cookieName: AUTH_COOKIE, signed: false },
|
||||
sign: { expiresIn: `${AUTH_COOKIE_MAX_AGE}s` },
|
||||
});
|
||||
|
||||
app.decorate('authenticate', async (request) => {
|
||||
try {
|
||||
await request.jwtVerify({ onlyCookie: true });
|
||||
} catch {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
});
|
||||
|
||||
// helpers for routes
|
||||
app.decorate('setAuthCookie', ((reply, token) => {
|
||||
reply.setCookie(AUTH_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
secure: env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: AUTH_COOKIE_MAX_AGE,
|
||||
});
|
||||
}) as never);
|
||||
|
||||
app.decorate('clearAuthCookie', ((reply) => {
|
||||
reply.clearCookie(AUTH_COOKIE, { path: '/' });
|
||||
}) as never);
|
||||
});
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
setAuthCookie: (reply: import('fastify').FastifyReply, token: string) => void;
|
||||
clearAuthCookie: (reply: import('fastify').FastifyReply) => void;
|
||||
}
|
||||
}
|
||||
10
apps/backend/src/plugins/cors.ts
Normal file
10
apps/backend/src/plugins/cors.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import fastifyCors from '@fastify/cors';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export default fp(async (app) => {
|
||||
await app.register(fastifyCors, {
|
||||
origin: env.NODE_ENV === 'production' ? env.PUBLIC_APP_URL : true,
|
||||
credentials: true,
|
||||
});
|
||||
});
|
||||
25
apps/backend/src/plugins/guest.ts
Normal file
25
apps/backend/src/plugins/guest.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export const GUEST_COOKIE = 'fw_gid';
|
||||
const GUEST_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2; // 2 years
|
||||
|
||||
export default fp(async (app) => {
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
const existing = request.cookies[GUEST_COOKIE];
|
||||
if (existing && existing.length >= 16 && existing.length <= 64) {
|
||||
request.guestId = existing;
|
||||
return;
|
||||
}
|
||||
const id = nanoid(24);
|
||||
request.guestId = id;
|
||||
reply.setCookie(GUEST_COOKIE, id, {
|
||||
httpOnly: true,
|
||||
secure: env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: GUEST_COOKIE_MAX_AGE,
|
||||
});
|
||||
});
|
||||
});
|
||||
13
apps/backend/src/plugins/multipart.ts
Normal file
13
apps/backend/src/plugins/multipart.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import fastifyMultipart from '@fastify/multipart';
|
||||
|
||||
export const MAX_UPLOAD_BYTES = 8 * 1024 * 1024; // 8 MB
|
||||
|
||||
export default fp(async (app) => {
|
||||
await app.register(fastifyMultipart, {
|
||||
limits: {
|
||||
fileSize: MAX_UPLOAD_BYTES,
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
18
apps/backend/src/plugins/prisma.ts
Normal file
18
apps/backend/src/plugins/prisma.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import fp from 'fastify-plugin';
|
||||
|
||||
export default fp(async (app) => {
|
||||
const prisma = new PrismaClient({
|
||||
log: [{ emit: 'event', level: 'error' }, { emit: 'event', level: 'warn' }],
|
||||
});
|
||||
|
||||
prisma.$on('error', (e) => app.log.error({ prisma: e }, 'prisma error'));
|
||||
prisma.$on('warn', (e) => app.log.warn({ prisma: e }, 'prisma warn'));
|
||||
|
||||
await prisma.$connect();
|
||||
app.decorate('prisma', prisma);
|
||||
|
||||
app.addHook('onClose', async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
});
|
||||
8
apps/backend/src/plugins/rate-limit.ts
Normal file
8
apps/backend/src/plugins/rate-limit.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import fastifyRateLimit from '@fastify/rate-limit';
|
||||
|
||||
export default fp(async (app) => {
|
||||
await app.register(fastifyRateLimit, {
|
||||
global: false,
|
||||
});
|
||||
});
|
||||
20
apps/backend/src/plugins/static.ts
Normal file
20
apps/backend/src/plugins/static.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import fp from 'fastify-plugin';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export default fp(async (app) => {
|
||||
const uploadsRoot = resolve(env.UPLOADS_DIR);
|
||||
mkdirSync(resolve(uploadsRoot, 'og'), { recursive: true });
|
||||
mkdirSync(resolve(uploadsRoot, 'upload'), { recursive: true });
|
||||
mkdirSync(resolve(uploadsRoot, 'avatar'), { recursive: true });
|
||||
|
||||
await app.register(fastifyStatic, {
|
||||
root: uploadsRoot,
|
||||
prefix: '/uploads/',
|
||||
decorateReply: false,
|
||||
index: false,
|
||||
list: false,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user