From cbadd140077ae9a8ce75923f23af8ae9cca54fc1 Mon Sep 17 00:00:00 2001 From: Pranshu Garg Date: Sun, 17 May 2026 12:08:06 +0530 Subject: [PATCH] feat: implement JWT authentication and authorization flow --- apps/backend/package.json | 2 + .../migration.sql | 1 + apps/backend/prisma/schema.prisma | 1 + apps/backend/src/__tests__/auth.test.ts | 181 ++++++++++++++ apps/backend/src/routes/auth.ts | 138 +++++++++-- apps/backend/src/routes/cards.ts | 8 +- apps/backend/src/routes/connect.ts | 2 +- apps/backend/src/routes/follow.ts | 4 +- apps/backend/src/routes/public.ts | 10 +- apps/backend/src/types/fastify.d.ts | 13 + apps/backend/src/utils/validators.ts | 19 ++ pnpm-lock.yaml | 226 ++++++++++++++++++ pnpm-workspace.yaml | 1 + 13 files changed, 570 insertions(+), 36 deletions(-) create mode 100644 apps/backend/prisma/migrations/20260517000000_add_password_auth/migration.sql create mode 100644 apps/backend/src/__tests__/auth.test.ts create mode 100644 apps/backend/src/types/fastify.d.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index b8d1141..df2057f 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -24,6 +24,7 @@ "@fastify/multipart": "^9.0.0", "@fastify/static": "^8.0.0", "@prisma/client": "^6.0.0", + "bcrypt": "^5.1.1", "dotenv": "^16.4.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.0", @@ -32,6 +33,7 @@ "zod": "^3.23.0" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/node": "^22.0.0", "@types/qrcode": "^1.5.0", "pino-pretty": "^13.1.3", diff --git a/apps/backend/prisma/migrations/20260517000000_add_password_auth/migration.sql b/apps/backend/prisma/migrations/20260517000000_add_password_auth/migration.sql new file mode 100644 index 0000000..617445a --- /dev/null +++ b/apps/backend/prisma/migrations/20260517000000_add_password_auth/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "password_hash" TEXT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 13dec57..0d61f74 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { accentColor String @default("#6366f1") @map("accent_color") provider String providerId String @map("provider_id") + passwordHash String? @map("password_hash") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/apps/backend/src/__tests__/auth.test.ts b/apps/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..48dd8e7 --- /dev/null +++ b/apps/backend/src/__tests__/auth.test.ts @@ -0,0 +1,181 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import jwt from '@fastify/jwt'; +import cookie from '@fastify/cookie'; +import { authRoutes } from '../routes/auth.js'; + +vi.mock('bcrypt', () => ({ + default: { + hash: vi.fn(async (password: string) => `hashed:${password}`), + compare: vi.fn(async (password: string, hash: string) => hash === `hashed:${password}`), + }, +})); + +const createdAt = new Date('2026-05-17T00:00:00.000Z'); + +const mockUser = { + id: 'user-1', + email: 'dev@example.com', + username: 'devuser', + displayName: 'Dev User', + bio: null, + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: '#6366f1', + provider: 'local', + providerId: 'dev@example.com', + passwordHash: 'hashed:password123', + createdAt, + updatedAt: createdAt, +}; + +const mockPrisma = { + user: { + create: vi.fn(), + findFirst: vi.fn(), + findUnique: vi.fn(), + }, +}; + +async function buildApp() { + const app = Fastify(); + app.register(jwt, { secret: 'test-secret' }); + app.register(cookie); + app.decorate('prisma', mockPrisma); + app.decorate('authenticate', async function (request: any, reply: any) { + try { + await request.jwtVerify(); + } catch { + reply.status(401).send({ error: 'Unauthorized' }); + } + }); + app.register(authRoutes, { prefix: '/auth' }); + await app.ready(); + return app; +} + +describe('local auth routes', () => { + beforeEach(() => vi.clearAllMocks()); + + it('registers a user with a hashed password and returns a JWT', async () => { + mockPrisma.user.findFirst.mockResolvedValue(null); + mockPrisma.user.create.mockResolvedValue({ + id: mockUser.id, + email: mockUser.email, + username: mockUser.username, + displayName: mockUser.displayName, + bio: null, + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: mockUser.accentColor, + createdAt, + }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: 'Dev@Example.com', + username: 'devuser', + displayName: 'Dev User', + password: 'password123', + }, + }); + + expect(res.statusCode).toBe(201); + expect(res.json().token).toBeTruthy(); + expect(res.json().user.passwordHash).toBeUndefined(); + expect(mockPrisma.user.create).toHaveBeenCalledWith({ + data: { + email: 'dev@example.com', + username: 'devuser', + displayName: 'Dev User', + provider: 'local', + providerId: 'dev@example.com', + passwordHash: 'hashed:password123', + }, + select: expect.any(Object), + }); + + await app.close(); + }); + + it('rejects duplicate emails during registration', async () => { + mockPrisma.user.findFirst.mockResolvedValue({ email: 'dev@example.com', username: 'other' }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: 'dev@example.com', + username: 'devuser', + displayName: 'Dev User', + password: 'password123', + }, + }); + + expect(res.statusCode).toBe(409); + expect(res.json().error).toBe('Email already registered'); + expect(mockPrisma.user.create).not.toHaveBeenCalled(); + + await app.close(); + }); + + it('logs in a local user with valid credentials', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { + email: 'dev@example.com', + password: 'password123', + }, + }); + + const body = res.json(); + expect(res.statusCode).toBe(200); + expect(body.token).toBeTruthy(); + expect(body.user.passwordHash).toBeUndefined(); + expect(body.user.provider).toBeUndefined(); + expect(body.user.providerId).toBeUndefined(); + + await app.close(); + }); + + it('rejects invalid login credentials', async () => { + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const app = await buildApp(); + const res = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { + email: 'dev@example.com', + password: 'wrong-password', + }, + }); + + expect(res.statusCode).toBe(401); + expect(res.json().error).toBe('Invalid email or password'); + + await app.close(); + }); + + it('protects authenticated routes with JWT middleware', async () => { + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/auth/me' }); + + expect(res.statusCode).toBe(401); + expect(res.json().error).toBe('Unauthorized'); + + await app.close(); + }); +}); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index e12f10a..d05a323 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -1,4 +1,6 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import bcrypt from 'bcrypt'; +import { loginSchema, registerSchema } from '../utils/validators.js'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -12,7 +14,77 @@ interface OAuthCallbackQuery { state?: string; } +const PASSWORD_SALT_ROUNDS = 12; + export async function authRoutes(app: FastifyInstance) { + app.post('/register', async (request: FastifyRequest, reply: FastifyReply) => { + const parsed = registerSchema.safeParse(request.body); + + if (!parsed.success) { + return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + } + + const { email, username, displayName, password } = parsed.data; + const existingUser = await app.prisma.user.findFirst({ + where: { + OR: [{ email }, { username }], + }, + select: { email: true, username: true }, + }); + + if (existingUser?.email === email) { + return reply.status(409).send({ error: 'Email already registered' }); + } + + if (existingUser?.username === username) { + return reply.status(409).send({ error: 'Username already taken' }); + } + + const passwordHash = await bcrypt.hash(password, PASSWORD_SALT_ROUNDS); + const user = await app.prisma.user.create({ + data: { + email, + username, + displayName, + provider: 'local', + providerId: email, + passwordHash, + }, + select: userSelect, + }); + + const token = signAuthToken(app, user); + setAuthCookie(reply, token); + + return reply.status(201).send({ token, user }); + }); + + app.post('/login', async (request: FastifyRequest, reply: FastifyReply) => { + const parsed = loginSchema.safeParse(request.body); + + if (!parsed.success) { + return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + } + + const { email, password } = parsed.data; + const user = await app.prisma.user.findUnique({ where: { email } }); + + if (!user?.passwordHash) { + return reply.status(401).send({ error: 'Invalid email or password' }); + } + + const passwordMatches = await bcrypt.compare(password, user.passwordHash); + if (!passwordMatches) { + return reply.status(401).send({ error: 'Invalid email or password' }); + } + + const token = signAuthToken(app, user); + setAuthCookie(reply, token); + + const { passwordHash, provider, providerId, ...safeUser } = user; + return { token, user: safeUser }; + }); + // ─── GitHub OAuth ─── app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => { @@ -112,10 +184,7 @@ export async function authRoutes(app: FastifyInstance) { }); // Generate JWT - const token = app.jwt.sign( - { id: user.id, username: user.username }, - { expiresIn: '30d' } - ); + const token = signAuthToken(app, user); // For mobile app: redirect with token as query param const mobileRedirect = process.env.MOBILE_REDIRECT_URI; @@ -124,17 +193,11 @@ export async function authRoutes(app: FastifyInstance) { } // For web: set cookie and redirect - reply.setCookie('token', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 30 * 24 * 60 * 60, // 30 days - }); + setAuthCookie(reply, token); return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('GitHub auth error:', err); + app.log.error({ err }, 'GitHub auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); @@ -215,27 +278,18 @@ export async function authRoutes(app: FastifyInstance) { }, }); - const token = app.jwt.sign( - { id: user.id, username: user.username }, - { expiresIn: '30d' } - ); + const token = signAuthToken(app, user); if (request.query.state?.startsWith('mobile_')) { const mobileRedirect = process.env.MOBILE_REDIRECT_URI; return reply.redirect(`${mobileRedirect}?token=${token}`); } - reply.setCookie('token', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 30 * 24 * 60 * 60, - }); + setAuthCookie(reply, token); return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('Google auth error:', err); + app.log.error({ err }, 'Google auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); @@ -289,3 +343,39 @@ export async function authRoutes(app: FastifyInstance) { function generateState(): string { return Math.random().toString(36).substring(2, 15); } + +const userSelect = { + id: true, + email: true, + username: true, + displayName: true, + bio: true, + pronouns: true, + role: true, + company: true, + avatarUrl: true, + accentColor: true, + createdAt: true, +}; + +type AuthUser = { + id: string; + username: string; +}; + +function signAuthToken(app: FastifyInstance, user: AuthUser): string { + return app.jwt.sign( + { id: user.id, username: user.username }, + { expiresIn: '30d' } + ); +} + +function setAuthCookie(reply: FastifyReply, token: string) { + reply.setCookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 30 * 24 * 60 * 60, + }); +} diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index f1af7b0..f265263 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -20,11 +20,11 @@ export async function cardRoutes(app: FastifyInstance) { orderBy: { createdAt: 'asc' }, }); - return cards.map((card) => ({ + return cards.map((card: any) => ({ id: card.id, title: card.title, isDefault: card.isDefault, - links: card.cardLinks.map((cl) => cl.platformLink), + links: card.cardLinks.map((cl: any) => cl.platformLink), })); }); @@ -65,7 +65,7 @@ export async function cardRoutes(app: FastifyInstance) { id: card.id, title: card.title, isDefault: card.isDefault, - links: card.cardLinks.map((cl) => cl.platformLink), + links: card.cardLinks.map((cl: any) => cl.platformLink), }); }); @@ -125,7 +125,7 @@ export async function cardRoutes(app: FastifyInstance) { id: updated!.id, title: updated!.title, isDefault: updated!.isDefault, - links: updated!.cardLinks.map((cl) => cl.platformLink), + links: updated!.cardLinks.map((cl: any) => cl.platformLink), }; }); diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 952e845..f707de4 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -125,7 +125,7 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?connected=github`); } catch (err) { - app.log.error('GitHub connect error:', err); + app.log.error({ err }, 'GitHub connect error'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=server_error`); } }); diff --git a/apps/backend/src/routes/follow.ts b/apps/backend/src/routes/follow.ts index aabc85b..e864319 100644 --- a/apps/backend/src/routes/follow.ts +++ b/apps/backend/src/routes/follow.ts @@ -53,7 +53,7 @@ export async function followRoutes(app: FastifyInstance) { status: 'success', layer: 'api', }, - }).catch(err => app.log.error('Failed to log follow:', err)); + }).catch((err: unknown) => app.log.error({ err }, 'Failed to log follow')); } return result; @@ -68,7 +68,7 @@ export async function followRoutes(app: FastifyInstance) { status: 'error', layer: 'api', }, - }).catch(e => app.log.error('Failed to log follow error:', e)); + }).catch((err: unknown) => app.log.error({ err }, 'Failed to log follow error')); return reply.status(500).send({ error: 'Follow action failed', message: err.message }); } diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index f60e613..e1dc9fe 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -107,7 +107,7 @@ export async function publicRoutes(app: FastifyInstance) { viewerAgent: request.headers['user-agent'] || null, source: (request.query as any)?.source || 'link', }, - }).catch(err => app.log.error('Failed to log view:', err)); + }).catch((err: unknown) => app.log.error({ err }, 'Failed to log view')); } const response: UsernamePublicProfileResponse = { @@ -119,7 +119,7 @@ export async function publicRoutes(app: FastifyInstance) { company: user.company, avatarUrl: user.avatarUrl, accentColor: user.accentColor, - links: user.platformLinks.map((link) => ({ + links: user.platformLinks.map((link: any) => ({ id: link.id, platform: link.platform, username: link.username, @@ -167,7 +167,7 @@ export async function publicRoutes(app: FastifyInstance) { avatarUrl: card.user.avatarUrl, accentColor: card.user.accentColor, }, - links: card.cardLinks.map((cl) => ({ + links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, @@ -230,7 +230,7 @@ export async function publicRoutes(app: FastifyInstance) { viewerAgent: request.headers['user-agent'] || null, source: (request.query as any)?.source || 'qr', }, - }).catch(err => app.log.error('Failed to log card view:', err)); + }).catch((err: unknown) => app.log.error({ err }, 'Failed to log card view')); } @@ -246,7 +246,7 @@ export async function publicRoutes(app: FastifyInstance) { avatarUrl: user.avatarUrl, accentColor: user.accentColor, }, - links: card.cardLinks.map((cl) => ({ + links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, diff --git a/apps/backend/src/types/fastify.d.ts b/apps/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..ba3dc54 --- /dev/null +++ b/apps/backend/src/types/fastify.d.ts @@ -0,0 +1,13 @@ +import 'fastify'; + +declare module 'fastify' { + interface FastifyInstance { + authenticate: any; + encryption: { + encrypt: (plaintext: string) => string; + decrypt: (encryptedBase64: string) => string; + }; + } +} + +export {}; diff --git a/apps/backend/src/utils/validators.ts b/apps/backend/src/utils/validators.ts index 80d2caa..1bc7b58 100644 --- a/apps/backend/src/utils/validators.ts +++ b/apps/backend/src/utils/validators.ts @@ -1,5 +1,24 @@ import { z } from 'zod'; +export const registerSchema = z.object({ + email: z.string().email().max(255).transform((value) => value.toLowerCase()), + username: z + .string() + .min(3) + .max(50) + .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, hyphens, and underscores'), + displayName: z.string().min(1).max(100), + password: z + .string() + .min(8, 'Password must be at least 8 characters') + .max(128, 'Password must be less than 128 characters'), +}); + +export const loginSchema = z.object({ + email: z.string().email().max(255).transform((value) => value.toLowerCase()), + password: z.string().min(1).max(128), +}); + export const updateProfileSchema = z.object({ displayName: z.string().min(1).max(100).optional(), username: z diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6818604..0b85823 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@prisma/client': specifier: ^6.0.0 version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 dotenv: specifier: ^16.4.0 version: 16.6.1 @@ -57,6 +60,9 @@ importers: specifier: ^3.23.0 version: 3.25.76 devDependencies: + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 '@types/node': specifier: ^22.0.0 version: 22.19.15 @@ -1419,6 +1425,10 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -1873,6 +1883,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@5.0.2': + resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1988,6 +2001,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -2021,6 +2035,9 @@ packages: '@vscode/sudo-prompt@9.3.2': resolution: {integrity: sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -2046,6 +2063,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2101,6 +2122,14 @@ packages: appdirsjs@1.2.7: resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -2241,6 +2270,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} + engines: {node: '>= 10.0.0'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2342,6 +2375,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + chrome-launcher@0.15.2: resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} engines: {node: '>=12.13.0'} @@ -2419,6 +2456,10 @@ packages: color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} @@ -2470,6 +2511,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2610,6 +2654,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -2629,6 +2676,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -3081,6 +3132,10 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3099,6 +3154,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3197,6 +3257,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3239,6 +3302,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3774,6 +3841,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -3922,10 +3993,22 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -3975,6 +4058,9 @@ packages: resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} engines: {node: '>=12.0.0'} + node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -4001,6 +4087,11 @@ packages: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4009,6 +4100,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -4836,6 +4931,11 @@ packages: resolution: {integrity: sha512-UcNfWzbrjvYXYSk+U2hME25kpb87oq6/WVLeBF4khyQrb3Ob/URVlN23khal+RbdCUTMfg4qWjI9KZjCNFtYMQ==} engines: {node: '>=18'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + terser@5.46.0: resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} engines: {node: '>=10'} @@ -5198,6 +5298,9 @@ packages: engines: {node: '>=8'} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5254,6 +5357,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -6626,6 +6732,21 @@ snapshots: '@lukeed/ms@2.0.2': {} + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.1.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -7213,6 +7334,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/bcrypt@5.0.2': + dependencies: + '@types/node': 22.19.15 + '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} @@ -7408,6 +7533,8 @@ snapshots: '@vscode/sudo-prompt@9.3.2': {} + abbrev@1.1.1: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -7430,6 +7557,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.18.0): @@ -7483,6 +7616,13 @@ snapshots: appdirsjs@1.2.7: {} + aproba@2.1.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -7683,6 +7823,14 @@ snapshots: baseline-browser-mapping@2.10.0: {} + bcrypt@5.1.1: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -7807,6 +7955,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@2.0.0: {} + chrome-launcher@0.15.2: dependencies: '@types/node': 22.19.15 @@ -7890,6 +8040,8 @@ snapshots: color-name: 1.1.4 simple-swizzle: 0.2.4 + color-support@1.1.3: {} + color@4.2.3: dependencies: color-convert: 2.0.1 @@ -7947,6 +8099,8 @@ snapshots: consola@3.4.2: {} + console-control-strings@1.1.0: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -8082,6 +8236,8 @@ snapshots: defu@6.1.4: {} + delegates@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -8092,6 +8248,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.1.2: {} + detect-newline@3.1.0: {} devalue@5.6.4: {} @@ -8717,6 +8875,10 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8735,6 +8897,18 @@ snapshots: functions-have-names@1.2.3: {} + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -8841,6 +9015,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-unicode@2.0.1: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8883,6 +9059,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -9601,6 +9784,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + make-dir@4.0.0: dependencies: semver: 7.7.4 @@ -9840,8 +10027,19 @@ snapshots: minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + mkdirp@1.0.4: {} mnemonist@0.40.0: @@ -9872,6 +10070,8 @@ snapshots: nocache@3.0.4: {} + node-addon-api@5.1.0: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -9891,12 +10091,23 @@ snapshots: node-stream-zip@1.15.0: {} + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + normalize-path@3.0.0: {} npm-run-path@4.0.1: dependencies: path-key: 3.1.1 + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -10887,6 +11098,15 @@ snapshots: magic-string: 0.30.21 zimmerframe: 1.1.4 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -11220,6 +11440,10 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -11255,6 +11479,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml@2.8.2: {} yargs-parser@18.1.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1192891..07e0302 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,5 +4,6 @@ packages: allowBuilds: '@prisma/client': true '@prisma/engines': true + bcrypt: true esbuild: true prisma: true