From e371301a80b1d9641553a8321d4ebe156a7a1bce Mon Sep 17 00:00:00 2001 From: chertik77 Date: Tue, 30 Jun 2026 20:29:27 +0100 Subject: [PATCH 1/7] feat(auth): migrate to better-auth lib --- app/app.ts | 7 +- app/auth.ts | 36 +++ app/config/env.config.ts | 12 +- app/controllers/auth.controller.ts | 333 -------------------------- app/controllers/index.ts | 1 - app/controllers/user.controller.ts | 122 +++++----- app/middlewares/authenticate.ts | 43 ---- app/middlewares/index.ts | 2 +- app/middlewares/require-auth.ts | 26 ++ app/prisma/prisma.client.ts | 2 +- app/routes/api/auth.ts | 33 --- app/routes/api/board.ts | 4 +- app/routes/api/column.ts | 4 +- app/routes/api/label.ts | 4 +- app/routes/api/task.ts | 4 +- app/routes/api/user.ts | 18 +- app/routes/index.ts | 2 - app/types/express.d.ts | 4 +- package.json | 4 +- prisma/models/account.prisma | 22 ++ prisma/models/session.prisma | 11 +- prisma/models/user.prisma | 22 +- prisma/models/verification.prisma | 12 + yarn.lock | 366 ++++++++++------------------- 24 files changed, 333 insertions(+), 761 deletions(-) create mode 100644 app/auth.ts delete mode 100644 app/controllers/auth.controller.ts delete mode 100644 app/middlewares/authenticate.ts create mode 100644 app/middlewares/require-auth.ts delete mode 100644 app/routes/api/auth.ts create mode 100644 prisma/models/account.prisma create mode 100644 prisma/models/verification.prisma diff --git a/app/app.ts b/app/app.ts index 27250ad..d55a5d0 100644 --- a/app/app.ts +++ b/app/app.ts @@ -1,10 +1,12 @@ import express from 'express' +import { toNodeHandler } from 'better-auth/node' import cookieParser from 'cookie-parser' import cors from 'cors' import helmet from 'helmet' import logger from 'morgan' import * as z from 'zod' +import { auth } from './auth' import { env, zodConfig } from './config' import { globalErrorHandler, notFoundHandler } from './middlewares' import { apiRouter } from './routes' @@ -13,8 +15,11 @@ export const app = express() z.config(zodConfig) -app.use(helmet()) app.use(cors({ origin: env.ALLOWED_ORIGINS, credentials: true })) + +app.all(`${env.API_PREFIX}/auth/*splat`, toNodeHandler(auth)) + +app.use(helmet()) app.use(logger(app.get('env') === 'development' ? 'dev' : 'combined')) app.use(cookieParser()) app.use(express.json()) diff --git a/app/auth.ts b/app/auth.ts new file mode 100644 index 0000000..501c024 --- /dev/null +++ b/app/auth.ts @@ -0,0 +1,36 @@ +import { Theme } from '@prisma/client' +import { betterAuth } from 'better-auth' +import { prismaAdapter } from 'better-auth/adapters/prisma' +import { openAPI } from 'better-auth/plugins' + +import { env } from './config' +import { prisma } from './prisma' + +export const auth = betterAuth({ + appName: 'Task Pro', + baseURL: env.BETTER_AUTH_URL, + database: prismaAdapter(prisma, { provider: 'mongodb' }), + advanced: { + cookiePrefix: 'taskpro', + database: { generateId: false }, + disableOriginCheck: env.NODE_ENV !== 'production' + }, + user: { + additionalFields: { + theme: { type: Object.values(Theme), input: false } + } + }, + emailAndPassword: { enabled: true, requireEmailVerification: false }, + socialProviders: { + google: { + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET + }, + microsoft: { + clientId: env.MICROSOFT_CLIENT_ID, + clientSecret: env.MICROSOFT_CLIENT_SECRET + } + }, + trustedOrigins: env.ALLOWED_ORIGINS, + plugins: [openAPI()] +}) diff --git a/app/config/env.config.ts b/app/config/env.config.ts index 8e9feae..01d7615 100644 --- a/app/config/env.config.ts +++ b/app/config/env.config.ts @@ -7,19 +7,15 @@ const envSchema = z.object({ CLOUDINARY_API_SECRET: z.string(), CLOUDINARY_CLOUD_NAME: z.string(), DATABASE_URL: z.string(), - FRONTEND_URL: z.url(), + BETTER_AUTH_URL: z.url(), GOOGLE_CLIENT_ID: z.string(), GOOGLE_CLIENT_SECRET: z.string(), - GOOGLE_REDIRECT_URI: z.url(), MICROSOFT_CLIENT_ID: z.string(), MICROSOFT_CLIENT_SECRET: z.string(), - MICROSOFT_REDIRECT_URI: z.url(), REDIS_USERNAME: z.string(), REDIS_PASSWORD: z.string(), REDIS_HOST: z.string(), REDIS_PORT: z.coerce.number().int().positive().min(1000).max(65535), - ACCESS_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)), - REFRESH_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)), PORT: z.coerce.number().int().positive().min(1000).max(65535), API_PREFIX: z.string(), NODE_ENV: z.enum(['development', 'production']).default('development'), @@ -28,11 +24,7 @@ const envSchema = z.object({ .transform(v => v.split(',')) .pipe(z.array(z.url())), RESEND_API_KEY: z.string(), - RESEND_RECEIVER: z.email(), - ACCESS_JWT_EXPIRES_IN: z.string(), - REFRESH_JWT_EXPIRES_IN: z.string(), - ACCESS_JWT_ALGORITHM: z.string(), - REFRESH_JWT_ALGORITHM: z.string() + RESEND_RECEIVER: z.email() }) // eslint-disable-next-line no-restricted-syntax diff --git a/app/controllers/auth.controller.ts b/app/controllers/auth.controller.ts deleted file mode 100644 index 2152f9c..0000000 --- a/app/controllers/auth.controller.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { randomBytes } from 'crypto' -import type { SigninSchema, SignupSchema } from '@/schemas' -import type { JwtPayload, MicrosoftProfile, TypedRequestBody } from '@/types' -import type { CookieOptions, NextFunction, Request, Response } from 'express' - -import { prisma } from '@/prisma' -import { ConfidentialClientApplication } from '@azure/msal-node' -import { hash, verify } from 'argon2' -import { OAuth2Client } from 'google-auth-library' -import { Conflict, Forbidden, Unauthorized } from 'http-errors' -import { jwtVerify, SignJWT } from 'jose' -import { JWTExpired } from 'jose/errors' - -import cloudinary, { env, redisClient } from '@/config' - -const { - ACCESS_JWT_EXPIRES_IN, - REFRESH_JWT_EXPIRES_IN, - REFRESH_JWT_SECRET, - ACCESS_JWT_SECRET, - ACCESS_JWT_ALGORITHM, - REFRESH_JWT_ALGORITHM, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI, - MICROSOFT_CLIENT_ID, - MICROSOFT_CLIENT_SECRET, - MICROSOFT_REDIRECT_URI, - FRONTEND_URL, - NODE_ENV -} = env - -class AuthController { - private googleClient = new OAuth2Client( - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI - ) - - private microsoftClient = new ConfidentialClientApplication({ - auth: { - clientId: MICROSOFT_CLIENT_ID, - clientSecret: MICROSOFT_CLIENT_SECRET, - authority: 'https://login.microsoftonline.com/consumers' - } - }) - - private readonly COOKIE_OPTIONS: CookieOptions = { - httpOnly: true, - secure: NODE_ENV === 'production', - sameSite: 'lax' - } - - private readonly ACCESS_TOKEN_NAME = 'accessToken' - private readonly REFRESH_TOKEN_NAME = 'refreshToken' - - private createSessionAndSetCookies = async ( - res: Response, - userId: string - ) => { - const session = await prisma.session.create({ data: { userId } }) - - const accessToken = await new SignJWT({ id: userId, sid: session.id }) - .setExpirationTime(ACCESS_JWT_EXPIRES_IN) - .setProtectedHeader({ alg: ACCESS_JWT_ALGORITHM }) - .sign(ACCESS_JWT_SECRET) - - const refreshToken = await new SignJWT({ id: userId, sid: session.id }) - .setExpirationTime(REFRESH_JWT_EXPIRES_IN) - .setProtectedHeader({ alg: REFRESH_JWT_ALGORITHM }) - .sign(REFRESH_JWT_SECRET) - - res.cookie(this.ACCESS_TOKEN_NAME, accessToken, { - ...this.COOKIE_OPTIONS, - maxAge: 1000 * 60 * 60 // 1 hour - }) - res.cookie(this.REFRESH_TOKEN_NAME, refreshToken, { - ...this.COOKIE_OPTIONS, - maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days - }) - } - - signup = async ( - { body }: TypedRequestBody, - res: Response, - next: NextFunction - ) => { - const isUserExists = await prisma.user.findFirst({ - where: { email: body.email } - }) - - if (isUserExists) return next(Conflict('Email already exist')) - - const user = await prisma.user.create({ - data: { - ...body, - password: await hash(body.password) - } - }) - - await this.createSessionAndSetCookies(res, user.id) - - res.json({ user }) - } - - signin = async ( - { body }: TypedRequestBody, - res: Response, - next: NextFunction - ) => { - const user = await prisma.user.findFirst({ - where: { email: body.email }, - omit: { password: false } - }) - - if (!user) return next(Unauthorized('Email or password invalid')) - - const { password, ...userWithoutPassword } = user - - if (!password) return next(Unauthorized('Email or password invalid')) - - const isPasswordMatch = await verify(password, body.password) - - if (!isPasswordMatch) return next(Unauthorized('Email or password invalid')) - - await this.createSessionAndSetCookies(res, user.id) - - res.json({ user: userWithoutPassword }) - } - - googleInitiate = async (_: Request, res: Response) => { - const state = randomBytes(32).toString('hex') - - await redisClient.set(`google_oauth_state:${state}`, 'true', 'EX', 5 * 60) - - const url = this.googleClient.generateAuthUrl({ - state, - access_type: 'offline', - scope: ['profile', 'email'] - }) - - res.redirect(url) - } - - googleCallback = async (req: Request, res: Response) => { - try { - const { code, state: receivedState, error } = req.query - - if (!code || (error && error === 'access_denied')) { - return res.redirect(`${FRONTEND_URL}?error=access_denied`) - } - - const redisStateKey = `google_oauth_state:${receivedState}` - - const storedState = await redisClient.get(redisStateKey) - - if (!storedState) { - return res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - - await redisClient.del(redisStateKey) - - const { - tokens: { id_token } - } = await this.googleClient.getToken(code as string) - - if (!id_token) { - return res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - - const ticket = await this.googleClient.verifyIdToken({ - idToken: id_token, - audience: GOOGLE_CLIENT_ID - }) - - const payload = ticket.getPayload() - - if (!payload || !payload.email) { - return res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - - const { email, name = 'Guest', picture = null } = payload - - let user = await prisma.user.findFirst({ where: { email } }) - - if (!user) { - user = await prisma.user.create({ - data: { name, email, avatar: picture } - }) - } - - await this.createSessionAndSetCookies(res, user.id) - - res.redirect(FRONTEND_URL) - } catch { - res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - } - - microsoftInitiate = async (_: Request, res: Response) => { - const state = randomBytes(32).toString('hex') - - await redisClient.set( - `microsoft_oauth_state:${state}`, - 'true', - 'EX', - 5 * 60 - ) - - const url = await this.microsoftClient.getAuthCodeUrl({ - scopes: ['openid', 'profile', 'email', 'User.Read'], - redirectUri: MICROSOFT_REDIRECT_URI, - prompt: 'select_account', - state - }) - - res.redirect(url) - } - - microsoftCallback = async (req: Request, res: Response) => { - try { - const { code, state: receivedState, error } = req.query - - if (!code || (error && error === 'access_denied')) { - return res.redirect(`${FRONTEND_URL}?error=access_denied`) - } - - const redisStateKey = `microsoft_oauth_state:${receivedState}` - - const storedState = await redisClient.get(redisStateKey) - - if (!storedState) { - return res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - - await redisClient.del(redisStateKey) - - const { idTokenClaims, accessToken } = - await this.microsoftClient.acquireTokenByCode({ - code: code as string, - scopes: ['openid', 'profile', 'email', 'User.Read'], - redirectUri: MICROSOFT_REDIRECT_URI - }) - - if (!idTokenClaims) { - return res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - - const profile = idTokenClaims as MicrosoftProfile - - const name = profile.name || 'Guest' - const email = profile.email || profile.preferred_username || profile.upn - - let user = await prisma.user.findFirst({ where: { email } }) - - if (!user) { - let avatar - - const avatarResponse = await fetch( - 'https://graph.microsoft.com/v1.0/me/photo/$value', - { headers: { Authorization: `Bearer ${accessToken}` } } - ) - - if (avatarResponse.ok) { - const buffer = Buffer.from(await avatarResponse.arrayBuffer()) - const base64 = buffer.toString('base64') - - const uploadedAvatar = await cloudinary.uploader.upload( - `data:image/jpeg;base64,${base64}`, - { folder: 'TaskPro/user_avatars' } - ) - - avatar = uploadedAvatar.url - } else { - avatar = null - } - - user = await prisma.user.create({ - data: { name, email, avatar } - }) - } - - await this.createSessionAndSetCookies(res, user.id) - - res.redirect(FRONTEND_URL) - } catch { - res.redirect(`${FRONTEND_URL}?error=oauth_error`) - } - } - - refresh = async (req: Request, res: Response, next: NextFunction) => { - const refreshToken = req.cookies[this.REFRESH_TOKEN_NAME] - - if (!refreshToken) return next(Forbidden()) - - try { - const { - payload: { id, sid } - } = await jwtVerify(refreshToken, REFRESH_JWT_SECRET) - - const user = await prisma.user.findUnique({ where: { id } }) - - if (!user) return next(Forbidden()) - - const currentSession = await prisma.session.findUnique({ - where: { id: sid } - }) - - if (!currentSession) return next(Forbidden()) - - await prisma.session.delete({ where: { id: currentSession.id } }) - - await this.createSessionAndSetCookies(res, user.id) - - res.json({ message: 'Tokens refreshed successfully' }) - } catch (error) { - if (error instanceof JWTExpired) return next(Forbidden(error.code)) - - return next(Forbidden()) - } - } - - logout = async ({ session }: Request, res: Response) => { - await prisma.session.delete({ where: { id: session } }) - - res.clearCookie(this.ACCESS_TOKEN_NAME, this.COOKIE_OPTIONS) - res.clearCookie(this.REFRESH_TOKEN_NAME, this.COOKIE_OPTIONS) - - res.sendStatus(204) - } -} - -export const authController = new AuthController() diff --git a/app/controllers/index.ts b/app/controllers/index.ts index 70971b9..b30a1fc 100644 --- a/app/controllers/index.ts +++ b/app/controllers/index.ts @@ -1,4 +1,3 @@ -export { authController } from './auth.controller' export { boardController } from './board.controller' export { taskController } from './task.controller' export { labelController } from './label.controller' diff --git a/app/controllers/user.controller.ts b/app/controllers/user.controller.ts index fbc061b..65d4227 100644 --- a/app/controllers/user.controller.ts +++ b/app/controllers/user.controller.ts @@ -1,6 +1,5 @@ -import type { EditUserSchema, NeedHelpSchema } from '@/schemas' +import type { NeedHelpSchema } from '@/schemas' import type { TypedRequestBody } from '@/types' -import type { User } from '@prisma/client' import type { NextFunction, Request, Response } from 'express' import { @@ -8,10 +7,9 @@ import { supportRequestUserTemplate } from '@/emails/templates' import { prisma } from '@/prisma' -import { hash } from 'argon2' -import { Conflict, InternalServerError, NotAcceptable } from 'http-errors' +import { InternalServerError } from 'http-errors' -import cloudinary, { env, redisClient, resend } from '@/config' +import { env, redisClient, resend } from '@/config' class UserController { me = async (req: Request, res: Response) => { @@ -40,63 +38,63 @@ class UserController { } } - update = async ( - { user, body, file }: TypedRequestBody, - res: Response, - next: NextFunction - ) => { - const { id, avatarPublicId, email: userEmail } = user - const { email, password } = body - - const isEmailExists = - email && (await prisma.user.findFirst({ where: { email } })) - - if (email && email !== userEmail && isEmailExists) { - return next(Conflict('Email already exist')) - } - - const updateData: Partial = body - - if (password) { - updateData.password = await hash(password) - } - - if (file) { - const extArr = ['jpeg', 'png'] - const ext = file.mimetype.split('/').pop() - - if (!extArr.includes(ext!)) { - return next(NotAcceptable('File must have .jpeg or .png extension')) - } - - try { - const newAvatar = await cloudinary.uploader.upload(file.path, { - folder: 'TaskPro/user_avatars' - }) - - if (avatarPublicId) { - await cloudinary.uploader.destroy(avatarPublicId, { - type: 'upload', - resource_type: 'image' - }) - } - - updateData.avatar = newAvatar.url - updateData.avatarPublicId = newAvatar.public_id - } catch { - return next(InternalServerError('Uploading avatar error')) - } - } - - const updatedUser = await prisma.user.update({ - where: { id }, - data: updateData - }) - - await redisClient.del(`user:${updatedUser.id}`) - - res.json(updatedUser) - } + // update = async ( + // { user, body, file }: TypedRequestBody, + // res: Response, + // next: NextFunction + // ) => { + // const { id, avatarPublicId, email: userEmail } = user + // const { email, password } = body + + // const isEmailExists = + // email && (await prisma.user.findFirst({ where: { email } })) + + // if (email && email !== userEmail && isEmailExists) { + // return next(Conflict('Email already exist')) + // } + + // const updateData: Partial = body + + // if (password) { + // updateData.password = await hash(password) + // } + + // if (file) { + // const extArr = ['jpeg', 'png'] + // const ext = file.mimetype.split('/').pop() + + // if (!extArr.includes(ext!)) { + // return next(NotAcceptable('File must have .jpeg or .png extension')) + // } + + // try { + // const newAvatar = await cloudinary.uploader.upload(file.path, { + // folder: 'TaskPro/user_avatars' + // }) + + // if (avatarPublicId) { + // await cloudinary.uploader.destroy(avatarPublicId, { + // type: 'upload', + // resource_type: 'image' + // }) + // } + + // updateData.avatar = newAvatar.url + // updateData.avatarPublicId = newAvatar.public_id + // } catch { + // return next(InternalServerError('Uploading avatar error')) + // } + // } + + // const updatedUser = await prisma.user.update({ + // where: { id }, + // data: updateData + // }) + + // await redisClient.del(`user:${updatedUser.id}`) + + // res.json(updatedUser) + // } help = async ( { body }: TypedRequestBody, diff --git a/app/middlewares/authenticate.ts b/app/middlewares/authenticate.ts deleted file mode 100644 index 77cbb58..0000000 --- a/app/middlewares/authenticate.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { JwtPayload } from '@/types' -import type { NextFunction, Request, Response } from 'express' - -import { prisma } from '@/prisma' -import { Unauthorized } from 'http-errors' -import { jwtVerify } from 'jose' -import { JWTExpired } from 'jose/errors' - -import { env } from '@/config' - -export const authenticate = async ( - req: Request, - _: Response, - next: NextFunction -) => { - const token: string = req.cookies.accessToken - - if (!token) return next(Unauthorized()) - - try { - const { - payload: { id, sid } - } = await jwtVerify(token, env.ACCESS_JWT_SECRET) - - const user = await prisma.user.findUnique({ - where: { id }, - omit: { password: false } - }) - - const session = await prisma.session.findUnique({ where: { id: sid } }) - - if (!user || !session) return next(Unauthorized()) - - req.user = user - req.session = session.id - - next() - } catch (e) { - if (e instanceof JWTExpired) return next(Unauthorized(e.code)) - - return next(Unauthorized()) - } -} diff --git a/app/middlewares/index.ts b/app/middlewares/index.ts index afb6da8..9b58ba6 100644 --- a/app/middlewares/index.ts +++ b/app/middlewares/index.ts @@ -1,4 +1,4 @@ -export { authenticate } from './authenticate' +export { requireAuth } from './require-auth' export { upload } from './multer' export { validateRequest } from './validate-request' export { notFoundHandler, globalErrorHandler } from './errorHandler' diff --git a/app/middlewares/require-auth.ts b/app/middlewares/require-auth.ts new file mode 100644 index 0000000..33cce50 --- /dev/null +++ b/app/middlewares/require-auth.ts @@ -0,0 +1,26 @@ +import type { NextFunction, Request, Response } from 'express' + +import { auth } from '@/auth' +import { fromNodeHeaders } from 'better-auth/node' +import { Unauthorized } from 'http-errors' + +export const requireAuth = async ( + req: Request, + _: Response, + next: NextFunction +) => { + try { + const session = await auth.api.getSession({ + headers: fromNodeHeaders(req.headers) + }) + + if (!session) return next(Unauthorized()) + + req.user = session.user + req.session = session.session + + next() + } catch { + return next(Unauthorized()) + } +} diff --git a/app/prisma/prisma.client.ts b/app/prisma/prisma.client.ts index 392b7d8..4542ddb 100644 --- a/app/prisma/prisma.client.ts +++ b/app/prisma/prisma.client.ts @@ -5,6 +5,6 @@ import { updateIgnoreNotFoundExtension } from './extensions' -export const prisma = new PrismaClient({ omit: { user: { password: true } } }) +export const prisma = new PrismaClient() .$extends(updateIgnoreNotFoundExtension) .$extends(deleteIgnoreNotFoundExtension) diff --git a/app/routes/api/auth.ts b/app/routes/api/auth.ts deleted file mode 100644 index 9d1d271..0000000 --- a/app/routes/api/auth.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from 'express' - -import { authController } from '@/controllers' - -import { authenticate, validateRequest } from '@/middlewares' - -import { SigninSchema, SignupSchema } from '@/schemas' - -export const authRouter = Router() - -authRouter.post( - '/signup', - validateRequest({ body: SignupSchema }), - authController.signup -) - -authRouter.post( - '/signin', - validateRequest({ body: SigninSchema }), - authController.signin -) - -authRouter.get('/google/initiate', authController.googleInitiate) - -authRouter.get('/google/callback', authController.googleCallback) - -authRouter.get('/microsoft/initiate', authController.microsoftInitiate) - -authRouter.get('/microsoft/callback', authController.microsoftCallback) - -authRouter.post('/refresh', authController.refresh) - -authRouter.post('/logout', authenticate, authController.logout) diff --git a/app/routes/api/board.ts b/app/routes/api/board.ts index 6172ed3..b232ad5 100644 --- a/app/routes/api/board.ts +++ b/app/routes/api/board.ts @@ -2,13 +2,13 @@ import { Router } from 'express' import { boardController } from '@/controllers' -import { authenticate, validateRequest } from '@/middlewares' +import { requireAuth, validateRequest } from '@/middlewares' import { AddBoardSchema, BoardParamsSchema, EditBoardSchema } from '@/schemas' export const boardRouter = Router() -boardRouter.use(authenticate) +boardRouter.use(requireAuth) boardRouter.get('/', boardController.getAll) diff --git a/app/routes/api/column.ts b/app/routes/api/column.ts index c8cdf5d..e235db3 100644 --- a/app/routes/api/column.ts +++ b/app/routes/api/column.ts @@ -2,7 +2,7 @@ import { Router } from 'express' import { columnController } from '@/controllers' -import { authenticate, validateRequest } from '@/middlewares' +import { requireAuth, validateRequest } from '@/middlewares' import { AddColumnSchema, @@ -14,7 +14,7 @@ import { export const columnRouter = Router() -columnRouter.use(authenticate) +columnRouter.use(requireAuth) columnRouter.post( '/:boardId', diff --git a/app/routes/api/label.ts b/app/routes/api/label.ts index 271cbc1..19973e4 100644 --- a/app/routes/api/label.ts +++ b/app/routes/api/label.ts @@ -2,13 +2,13 @@ import { Router } from 'express' import { labelController } from '@/controllers' -import { authenticate, validateRequest } from '@/middlewares' +import { requireAuth, validateRequest } from '@/middlewares' import { CreateLabelSchema } from '@/schemas' export const labelRouter = Router() -labelRouter.use(authenticate) +labelRouter.use(requireAuth) labelRouter.get('/', labelController.getAll) diff --git a/app/routes/api/task.ts b/app/routes/api/task.ts index c9f904d..05c9907 100644 --- a/app/routes/api/task.ts +++ b/app/routes/api/task.ts @@ -2,7 +2,7 @@ import { Router } from 'express' import { taskController } from '@/controllers' -import { authenticate, validateRequest } from '@/middlewares' +import { requireAuth, validateRequest } from '@/middlewares' import { AddTaskSchema, @@ -14,7 +14,7 @@ import { export const taskRouter = Router() -taskRouter.use(authenticate) +taskRouter.use(requireAuth) taskRouter.post( '/:columnId', diff --git a/app/routes/api/user.ts b/app/routes/api/user.ts index abc9dfb..05a1b2f 100644 --- a/app/routes/api/user.ts +++ b/app/routes/api/user.ts @@ -2,22 +2,22 @@ import { Router } from 'express' import { userController } from '@/controllers' -import { authenticate, upload, validateRequest } from '@/middlewares' +import { requireAuth, validateRequest } from '@/middlewares' -import { EditUserSchema, NeedHelpSchema } from '@/schemas' +import { NeedHelpSchema } from '@/schemas' export const userRouter = Router() -userRouter.use(authenticate) +userRouter.use(requireAuth) userRouter.get('/me', userController.me) -userRouter.patch( - '/', - upload.single('avatar'), - validateRequest({ body: EditUserSchema }), - userController.update -) +// userRouter.patch( +// '/', +// upload.single('avatar'), +// validateRequest({ body: EditUserSchema }), +// userController.update +// ) userRouter.post( '/help', diff --git a/app/routes/index.ts b/app/routes/index.ts index f7faa13..dedd80b 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -4,7 +4,6 @@ import path from 'path' import { Router } from 'express' import swaggerUi from 'swagger-ui-express' -import { authRouter } from './api/auth' import { boardRouter } from './api/board' import { columnRouter } from './api/column' import { labelRouter } from './api/label' @@ -22,7 +21,6 @@ apiRouter.use( swaggerUi.setup(swagger, { customSiteTitle: 'TaskPro API Docs' }) ) -apiRouter.use('/auth', authRouter) apiRouter.use('/user', userRouter) apiRouter.use('/board', boardRouter) apiRouter.use('/column', columnRouter) diff --git a/app/types/express.d.ts b/app/types/express.d.ts index 5517165..74d5731 100644 --- a/app/types/express.d.ts +++ b/app/types/express.d.ts @@ -1,10 +1,10 @@ -import type { User } from '@prisma/client' +import type { Session, User } from 'better-auth' declare global { namespace Express { interface Request { user: User - session: string + session: Session } } } diff --git a/package.json b/package.json index aa8b710..be2f347 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,13 @@ "schema": "./prisma" }, "dependencies": { - "@azure/msal-node": "^5.3.0", "@prisma/client": "^6.19.3", - "argon2": "^0.44.0", + "better-auth": "^1.6.23", "cloudinary": "^2.10.0", "cookie-parser": "^1.4.7", "cors": "2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "google-auth-library": "^10.9.0", "helmet": "^8.2.0", "http-errors": "^2.0.1", "ioredis": "^5.11.1", diff --git a/prisma/models/account.prisma b/prisma/models/account.prisma new file mode 100644 index 0000000..52a4cae --- /dev/null +++ b/prisma/models/account.prisma @@ -0,0 +1,22 @@ +model Account { + id String @id @default(auto()) @map("_id") @db.ObjectId + accountId String @map("account_id") + providerId String @map("provider_id") + scope String? + password String? + + accessToken String? @map("access_token") + refreshToken String? @map("refresh_token") + idToken String? @map("id_token") + + userId String @map("user_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + accessTokenExpiresAt DateTime? @map("access_token_expires_at") + refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([userId]) + @@map("accounts") +} diff --git a/prisma/models/session.prisma b/prisma/models/session.prisma index 763f174..0e33abb 100644 --- a/prisma/models/session.prisma +++ b/prisma/models/session.prisma @@ -1,9 +1,18 @@ model Session { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId + token String + + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + userId String @map("user_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + expiresAt DateTime @map("expires_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + @@unique([token]) + @@index([userId]) @@map("sessions") } diff --git a/prisma/models/user.prisma b/prisma/models/user.prisma index c953bff..580a878 100644 --- a/prisma/models/user.prisma +++ b/prisma/models/user.prisma @@ -1,19 +1,21 @@ model User { - id String @id @default(auto()) @map("_id") @db.ObjectId - name String - email String @unique - password String? - theme Theme @default(light) + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + email String + emailVerified Boolean @default(false) @map("email_verified") + image String? + imagePublicId String? @map("image_public_id") + theme Theme @default(light) - avatar String? - avatarPublicId String? @map("avatar_public_id") - - boards Board[] - labels Label[] + sessions Session[] + accounts Account[] + boards Board[] + labels Label[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + @@unique([email]) @@map("users") } diff --git a/prisma/models/verification.prisma b/prisma/models/verification.prisma new file mode 100644 index 0000000..4edd654 --- /dev/null +++ b/prisma/models/verification.prisma @@ -0,0 +1,12 @@ +model Verification { + id String @id @default(auto()) @map("_id") @db.ObjectId + identifier String + value String + + expiresAt DateTime @map("expires_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([identifier]) + @@map("verifications") +} diff --git a/yarn.lock b/yarn.lock index a6a6ac5..3d84fbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,19 +2,6 @@ # yarn lockfile v1 -"@azure/msal-common@16.10.0": - version "16.10.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-16.10.0.tgz#b270d164019e7c8d80692e9ceceecff003e6c1dc" - integrity sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w== - -"@azure/msal-node@^5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-5.3.0.tgz#538b1fe105828dd31b35c8d6c817d5ff96464b2c" - integrity sha512-fXtJX811pX8y8QlrQqBSH6+plvWyKZDI0IxkheAcyAw9OtcpXyFivmTC7eGUqutLWaDlKXuQ3yOESD4zAmkjHg== - dependencies: - "@azure/msal-common" "16.10.0" - jsonwebtoken "^9.0.0" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" @@ -87,6 +74,57 @@ "@babel/helper-string-parser" "^7.29.7" "@babel/helper-validator-identifier" "^7.29.7" +"@better-auth/core@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/core/-/core-1.6.23.tgz#1a6213ec81a3f7f084aae945736475d2b04c1b7a" + integrity sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ== + dependencies: + "@opentelemetry/semantic-conventions" "^1.39.0" + "@standard-schema/spec" "^1.1.0" + zod "^4.3.6" + +"@better-auth/drizzle-adapter@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.23.tgz#d9f9f1c974e0e9a1e5fdc6685fad4a267c9d467c" + integrity sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ== + +"@better-auth/kysely-adapter@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/kysely-adapter/-/kysely-adapter-1.6.23.tgz#845a5de0d8e9927e15011908a838951abbfad427" + integrity sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg== + +"@better-auth/memory-adapter@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/memory-adapter/-/memory-adapter-1.6.23.tgz#1295d3c8091cfa2a9beb2e7f9b29e44663dd09e5" + integrity sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ== + +"@better-auth/mongo-adapter@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/mongo-adapter/-/mongo-adapter-1.6.23.tgz#21637c529c8bc1b5ff27de8b96059d781acd151f" + integrity sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA== + +"@better-auth/prisma-adapter@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz#d141a4cc74785392bb0490d37002bd6d75b3b661" + integrity sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ== + +"@better-auth/telemetry@1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/telemetry/-/telemetry-1.6.23.tgz#a232e560c2e7f618c4ad19e01565c0ea8b84740e" + integrity sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA== + +"@better-auth/utils@0.4.2", "@better-auth/utils@^0.4.0": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@better-auth/utils/-/utils-0.4.2.tgz#265a37e2ceef035e6968a851b13400d1b044e070" + integrity sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A== + dependencies: + "@noble/hashes" "^2.0.1" + +"@better-fetch/fetch@1.3.1", "@better-fetch/fetch@^1.1.21": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@better-fetch/fetch/-/fetch-1.3.1.tgz#04a0d289de818a0b436bfda48e70b2b19227cc19" + integrity sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g== + "@borewit/text-codec@^0.2.1": version "0.2.2" resolved "https://registry.yarnpkg.com/@borewit/text-codec/-/text-codec-0.2.2.tgz#75025f735c0983b3a871668804a57387e3649375" @@ -250,11 +288,6 @@ "@simple-libs/stream-utils" "^1.2.0" semver "^7.5.2" -"@epic-web/invariant@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813" - integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA== - "@esbuild/aix-ppc64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz#7a289c158e29cbf59ea0afc83cc80f06d1c89402" @@ -624,10 +657,20 @@ "@napi-rs/nice-win32-ia32-msvc" "1.1.1" "@napi-rs/nice-win32-x64-msvc" "1.1.1" -"@phc/format@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4" - integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ== +"@noble/ciphers@^2.1.1": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.2.0.tgz#84fb45ac9332925d643b80f89ceb0ea2f21dba95" + integrity sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA== + +"@noble/hashes@^2.0.1": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" + integrity sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg== + +"@opentelemetry/semantic-conventions@^1.39.0": + version "1.41.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz#b04e7151c5913a7a006d4f465479da75efb98a7a" + integrity sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA== "@prisma/client@^6.19.3": version "6.19.3" @@ -773,7 +816,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be" integrity sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ== -"@standard-schema/spec@^1.0.0": +"@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== @@ -1277,11 +1320,6 @@ acorn@^8.16.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== -agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - ajv@^6.14.0: version "6.15.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" @@ -1331,16 +1369,6 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== -argon2@^0.44.0: - version "0.44.0" - resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.44.0.tgz#65a5ba662bba66af41407aa0457decf4af101742" - integrity sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig== - dependencies: - "@phc/format" "^1.0.0" - cross-env "^10.0.0" - node-addon-api "^8.5.0" - node-gyp-build "^4.8.4" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1371,7 +1399,7 @@ bare-events@^2.7.0: resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.3.tgz#ed26c87a24ece41c69dd4d2d0891c2c04a949e13" integrity sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw== -base64-js@^1.3.0, base64-js@^1.3.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1383,10 +1411,38 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" -bignumber.js@^9.0.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" - integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== +better-auth@^1.6.23: + version "1.6.23" + resolved "https://registry.yarnpkg.com/better-auth/-/better-auth-1.6.23.tgz#1af5228d94be69fb2e63b5bfd57d843c849cc28a" + integrity sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ== + dependencies: + "@better-auth/core" "1.6.23" + "@better-auth/drizzle-adapter" "1.6.23" + "@better-auth/kysely-adapter" "1.6.23" + "@better-auth/memory-adapter" "1.6.23" + "@better-auth/mongo-adapter" "1.6.23" + "@better-auth/prisma-adapter" "1.6.23" + "@better-auth/telemetry" "1.6.23" + "@better-auth/utils" "0.4.2" + "@better-fetch/fetch" "1.3.1" + "@noble/ciphers" "^2.1.1" + "@noble/hashes" "^2.0.1" + better-call "1.3.7" + defu "^6.1.4" + jose "^6.1.3" + kysely "^0.28.17 || ^0.29.0" + nanostores "^1.1.1" + zod "^4.3.6" + +better-call@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/better-call/-/better-call-1.3.7.tgz#2889806d909c1efb636b7e9009e7deba8927106f" + integrity sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w== + dependencies: + "@better-auth/utils" "^0.4.0" + "@better-fetch/fetch" "^1.1.21" + rou3 "^0.7.12" + set-cookie-parser "^3.0.1" binary-version-check@^6.1.0: version "6.1.0" @@ -1434,11 +1490,6 @@ brace-expansion@^5.0.5: dependencies: balanced-match "^4.0.2" -buffer-equal-constant-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1739,14 +1790,6 @@ cosmiconfig@^9.0.1: js-yaml "^4.1.0" parse-json "^5.2.0" -cross-env@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783" - integrity sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw== - dependencies: - "@epic-web/invariant" "^1.0.0" - cross-spawn "^7.0.6" - cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1756,11 +1799,6 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1768,7 +1806,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.4.3, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: +debug@4.4.3, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -1850,13 +1888,6 @@ dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2189,11 +2220,6 @@ ext-name@^5.0.0: ext-list "^2.0.0" sort-keys-length "^1.0.0" -extend@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - fast-check@^3.23.1: version "3.23.2" resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-3.23.2.tgz#0129f1eb7e4f500f58e8290edc83c670e4a574a2" @@ -2236,14 +2262,6 @@ fdir@^6.5.0: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - figures@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" @@ -2326,13 +2344,6 @@ form-data-encoder@^4.0.2: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-4.1.0.tgz#497cedc94810bd5d53b99b5d4f6c152d5cbc9db2" integrity sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw== -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -2358,24 +2369,6 @@ function-timeout@^1.0.1: resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.2.tgz#e5a7b6ffa523756ff20e1231bbe37b5f373aadd5" integrity sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA== -gaxios@^7.0.0, gaxios@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-7.1.4.tgz#33a5b78e2c5c01cf5a5d17f58dd188839867fc9c" - integrity sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA== - dependencies: - extend "^3.0.2" - https-proxy-agent "^7.0.1" - node-fetch "^3.3.2" - -gcp-metadata@8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz#e62e3373ddf41fc727ccc31c55c687b798bee898" - integrity sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg== - dependencies: - gaxios "^7.0.0" - google-logging-utils "^1.0.0" - json-bigint "^1.0.0" - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -2462,23 +2455,6 @@ globals@^17.7.0: resolved "https://registry.yarnpkg.com/globals/-/globals-17.7.0.tgz#553d55090b4dde8209ec2da42580d6e7e7d8b10d" integrity sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg== -google-auth-library@^10.9.0: - version "10.9.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-10.9.0.tgz#61e742af957818f572c3a5bce634c68355241d64" - integrity sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg== - dependencies: - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - gaxios "^7.1.4" - gcp-metadata "8.1.2" - google-logging-utils "1.1.3" - jws "^4.0.0" - -google-logging-utils@1.1.3, google-logging-utils@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz#17b71f1f95d266d2ddd356b8f00178433f041b17" - integrity sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA== - gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -2590,14 +2566,6 @@ http2-wrapper@^2.2.1: quick-lru "^5.1.1" resolve-alpn "^1.2.0" -https-proxy-agent@^7.0.1: - version "7.0.6" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" - human-signals@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" @@ -2767,7 +2735,7 @@ jiti@^2.4.2, jiti@^2.7.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== -jose@^6.2.3: +jose@^6.1.3, jose@^6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.3.tgz#0975197ad973251221c658a3cddc4b951a250c2d" integrity sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw== @@ -2789,13 +2757,6 @@ jsesc@^3.0.2: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -2821,39 +2782,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -jsonwebtoken@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" - integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== - dependencies: - jws "^4.0.1" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - -jwa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" - integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== - dependencies: - buffer-equal-constant-time "^1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^4.0.0, jws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" - integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== - dependencies: - jwa "^2.0.1" - safe-buffer "^5.0.1" - keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -2873,6 +2801,11 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +"kysely@^0.28.17 || ^0.29.0": + version "0.29.2" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.29.2.tgz#4a07da18db3bba2911693c2b3bd4af415104a85f" + integrity sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -2916,41 +2849,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - lodash@^4.17.23: version "4.18.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" @@ -3127,7 +3025,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@^2.1.1, ms@^2.1.3: +ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3142,6 +3040,11 @@ multer@^2.2.0: concat-stream "^2.0.0" type-is "^1.6.18" +nanostores@^1.1.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-1.4.0.tgz#9acc8a6026533dc0410c37cb3b076db00163d277" + integrity sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3152,35 +3055,11 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== -node-addon-api@^8.5.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.8.0.tgz#b742be05007b37ebc1741bbaf370d22bf25d208a" - integrity sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA== - -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - node-fetch-native@^1.6.6: version "1.6.7" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== -node-fetch@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - -node-gyp-build@^4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" - integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== - normalize-url@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.1.1.tgz#751a20c8520e5725404c06015fea21d7567f25ef" @@ -3587,6 +3466,11 @@ rfdc@^1.4.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== +rou3@^0.7.12: + version "0.7.12" + resolved "https://registry.yarnpkg.com/rou3/-/rou3-0.7.12.tgz#cac17425c04abddba854a42385cabfe0b971a179" + integrity sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg== + router@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" @@ -3603,7 +3487,7 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3632,7 +3516,7 @@ semver-truncate@^3.0.0: dependencies: semver "^7.3.5" -semver@^7.3.5, semver@^7.3.8, semver@^7.5.2, semver@^7.5.4, semver@^7.6.0, semver@^7.7.3: +semver@^7.3.5, semver@^7.3.8, semver@^7.5.2, semver@^7.6.0, semver@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.1.tgz#bf4970b5e70fda0686363cc18bfe8805d5ed957e" integrity sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg== @@ -3664,6 +3548,11 @@ serve-static@^2.2.0: parseurl "^1.3.3" send "^1.2.0" +set-cookie-parser@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz#5d80652208e37545aba30ea613d4f797ea405af8" + integrity sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA== + setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -4177,11 +4066,6 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - web-worker@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5" @@ -4266,7 +4150,7 @@ yoctocolors@^2.1.1: resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a" integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug== -zod@^4.4.3: +zod@^4.3.6, zod@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== From 662fe11570feb30f1a781ec8fbbd59ff9f5cc284 Mon Sep 17 00:00:00 2001 From: chertik77 Date: Tue, 30 Jun 2026 22:10:17 +0100 Subject: [PATCH 2/7] refactor(auth): move auth to lib and add Cloudinary upload --- app/app.ts | 2 +- app/{ => lib}/auth.ts | 14 +++++++++++--- app/lib/index.ts | 2 ++ app/lib/upload-to-cloudinary.ts | 29 +++++++++++++++++++++++++++++ app/middlewares/require-auth.ts | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) rename app/{ => lib}/auth.ts (67%) create mode 100644 app/lib/index.ts create mode 100644 app/lib/upload-to-cloudinary.ts diff --git a/app/app.ts b/app/app.ts index d55a5d0..4670d8d 100644 --- a/app/app.ts +++ b/app/app.ts @@ -6,8 +6,8 @@ import helmet from 'helmet' import logger from 'morgan' import * as z from 'zod' -import { auth } from './auth' import { env, zodConfig } from './config' +import { auth } from './lib' import { globalErrorHandler, notFoundHandler } from './middlewares' import { apiRouter } from './routes' diff --git a/app/auth.ts b/app/lib/auth.ts similarity index 67% rename from app/auth.ts rename to app/lib/auth.ts index 501c024..80f2439 100644 --- a/app/auth.ts +++ b/app/lib/auth.ts @@ -3,8 +3,9 @@ import { betterAuth } from 'better-auth' import { prismaAdapter } from 'better-auth/adapters/prisma' import { openAPI } from 'better-auth/plugins' -import { env } from './config' -import { prisma } from './prisma' +import { env } from '../config' +import { prisma } from '../prisma' +import { uploadToCloudinary } from './upload-to-cloudinary' export const auth = betterAuth({ appName: 'Task Pro', @@ -15,6 +16,7 @@ export const auth = betterAuth({ database: { generateId: false }, disableOriginCheck: env.NODE_ENV !== 'production' }, + account: { accountLinking: { trustedProviders: ['google', 'microsoft'] } }, user: { additionalFields: { theme: { type: Object.values(Theme), input: false } @@ -28,7 +30,13 @@ export const auth = betterAuth({ }, microsoft: { clientId: env.MICROSOFT_CLIENT_ID, - clientSecret: env.MICROSOFT_CLIENT_SECRET + clientSecret: env.MICROSOFT_CLIENT_SECRET, + prompt: 'select_account', + async mapProfileToUser(profile) { + const imgUrl = await uploadToCloudinary({ file: profile.picture }) + + return { image: imgUrl } + } } }, trustedOrigins: env.ALLOWED_ORIGINS, diff --git a/app/lib/index.ts b/app/lib/index.ts new file mode 100644 index 0000000..2dcc036 --- /dev/null +++ b/app/lib/index.ts @@ -0,0 +1,2 @@ +export { uploadToCloudinary } from './upload-to-cloudinary' +export { auth } from './auth' diff --git a/app/lib/upload-to-cloudinary.ts b/app/lib/upload-to-cloudinary.ts new file mode 100644 index 0000000..d442d8b --- /dev/null +++ b/app/lib/upload-to-cloudinary.ts @@ -0,0 +1,29 @@ +import cloudinary from '@/config' + +type UploadInput = { + file: string + folder?: string + public_id?: string +} + +export async function uploadToCloudinary({ + file, + folder = 'uploads', + public_id +}: UploadInput) { + const result = await cloudinary.uploader.upload( + file, + { folder, public_id, resource_type: 'auto' }, + err => { + if (err) throw new Error('Cloudinary upload failed') + } + ) + + return { + url: result.secure_url, + public_id: result.public_id, + width: result.width, + height: result.height, + format: result.format + } +} diff --git a/app/middlewares/require-auth.ts b/app/middlewares/require-auth.ts index 33cce50..cbd38d3 100644 --- a/app/middlewares/require-auth.ts +++ b/app/middlewares/require-auth.ts @@ -1,6 +1,6 @@ import type { NextFunction, Request, Response } from 'express' -import { auth } from '@/auth' +import { auth } from '@/lib' import { fromNodeHeaders } from 'better-auth/node' import { Unauthorized } from 'http-errors' From c50d1f81f98ff63acfb16617d93f2a2da11ded53 Mon Sep 17 00:00:00 2001 From: chertik77 Date: Wed, 1 Jul 2026 00:24:07 +0100 Subject: [PATCH 3/7] feat(api): update swagger docs and switch to bearerAuth --- app/routes/index.ts | 19 +- package.json | 2 + prisma/models/passkey.prisma | 20 ++ swagger.json | 386 +++-------------------------------- yarn.lock | 102 ++++++++- 5 files changed, 161 insertions(+), 368 deletions(-) create mode 100644 prisma/models/passkey.prisma diff --git a/app/routes/index.ts b/app/routes/index.ts index dedd80b..2187114 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -1,5 +1,6 @@ import fs from 'fs' import path from 'path' +import type { SwaggerUiOptions } from 'swagger-ui-express' import { Router } from 'express' import swaggerUi from 'swagger-ui-express' @@ -15,10 +16,26 @@ export const apiRouter = Router() const swaggerPath = path.join(process.cwd(), 'swagger.json') const swagger = JSON.parse(fs.readFileSync(swaggerPath, 'utf-8')) +apiRouter.get('/swagger.json', (_, res) => { + res.json(swagger) +}) + +const swaggerOptions: SwaggerUiOptions = { + customSiteTitle: 'TaskPro API Docs', + explorer: true, + swaggerOptions: { + urls: [ + { url: '/api/swagger.json', name: 'Main API' }, + { url: '/api/auth/open-api/generate-schema', name: 'Auth' } + ], + 'urls.primaryName': 'Main API' + } +} + apiRouter.use( '/docs', swaggerUi.serve, - swaggerUi.setup(swagger, { customSiteTitle: 'TaskPro API Docs' }) + swaggerUi.setup(swagger, swaggerOptions) ) apiRouter.use('/user', userRouter) diff --git a/package.json b/package.json index be2f347..747d35e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@prisma/client": "^6.19.3", + "@scalar/express-api-reference": "^0.10.7", "better-auth": "^1.6.23", "cloudinary": "^2.10.0", "cookie-parser": "^1.4.7", @@ -32,6 +33,7 @@ "morgan": "1.11.0", "multer": "^2.2.0", "resend": "^6.16.0", + "swagger-merger": "^1.5.4", "swagger-ui-express": "^5.0.1", "zod": "^4.4.3" }, diff --git a/prisma/models/passkey.prisma b/prisma/models/passkey.prisma new file mode 100644 index 0000000..8861333 --- /dev/null +++ b/prisma/models/passkey.prisma @@ -0,0 +1,20 @@ +model Passkey { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String? + counter Int + transports String? + aaguid String? + deviceType String @map("device_type") + backedUp Boolean @map("backed_up") + publicKey String @map("public_key") + credentialID String @map("credential_id") + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime? @map("created_at") + + @@index([userId]) + @@index([credentialID]) + @@map("passkeys") +} diff --git a/swagger.json b/swagger.json index 922af64..1cbdcc2 100644 --- a/swagger.json +++ b/swagger.json @@ -3,7 +3,7 @@ "info": { "version": "2.2.0", "title": "Task Pro API", - "description": "The Task Pro API provides endpoints to manage projects and user assignments, ensuring secure and efficient request handling.", + "description": "Task Pro API provides endpoints for managing projects, tasks, and user assignments with secure authentication and efficient workflows.", "contact": { "name": "TaskPro", "url": "https://www.taskpro.qzz.io" @@ -14,191 +14,11 @@ { "url": "https://api.taskpro.qzz.io" } ], "paths": { - "/auth/signup": { - "post": { - "tags": ["Auth"], - "summary": "Create a new user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["name", "email", "password"], - "properties": { - "name": { - "type": "string", - "minLength": 2, - "example": "John Doe" - }, - "email": { - "type": "string", - "format": "email", - "example": "user@example.com" - }, - "password": { - "type": "string", - "minLength": 8, - "maxLength": 64, - "example": "qwerty123" - } - } - } - } - } - }, - "responses": { - "201": { "$ref": "#/components/responses/AuthResponse" }, - "400": { "$ref": "#/components/responses/BadRequestError" }, - "409": { "$ref": "#/components/responses/Conflict" } - } - } - }, - "/auth/signin": { - "post": { - "tags": ["Auth"], - "summary": "Signin a user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email", "password"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "user@example.com" - }, - "password": { - "type": "string", - "example": "qwerty123" - } - } - } - } - } - }, - "responses": { - "201": { "$ref": "#/components/responses/AuthResponse" }, - "400": { "$ref": "#/components/responses/BadRequestError" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/auth/google/initiate": { - "get": { - "tags": ["Auth"], - "summary": "Redirect user to Google OAuth", - "responses": { - "302": { - "headers": { - "Location": { - "description": "Google OAuth URL", - "schema": { "type": "string" } - } - } - } - } - } - }, - "/auth/google/callback": { - "get": { - "tags": ["Auth"], - "summary": "Handles Google's OAuth redirect", - "responses": { - "302": { - "description": "Redirects to frontend with success or error", - "headers": { - "Location": { - "description": "Frontend redirect URL", - "schema": { - "type": "string", - "example": "https://taskpro.qzz.io?error=oauth_error" - } - } - } - } - } - } - }, - "/auth/microsoft/initiate": { - "get": { - "tags": ["Auth"], - "summary": "Redirect user to Microsoft OAuth", - "responses": { - "302": { - "headers": { - "Location": { - "description": "Microsoft OAuth URL", - "schema": { "type": "string" } - } - } - } - } - } - }, - "/auth/microsoft/callback": { - "get": { - "tags": ["Auth"], - "summary": "Handles Microsoft's OAuth redirect", - "responses": { - "302": { - "description": "Redirects to frontend with success or error", - "headers": { - "Location": { - "description": "Frontend redirect URL", - "schema": { - "type": "string", - "example": "https://taskpro.qzz.io?error=oauth_error" - } - } - } - } - } - } - }, - "/auth/refresh": { - "post": { - "tags": ["Auth"], - "summary": "Get new access and refresh tokens", - "security": [{ "CookieAuth": [] }], - "responses": { - "200": { "$ref": "#/components/responses/RefreshResponse" }, - "400": { "$ref": "#/components/responses/BadRequestError" }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "403": { "$ref": "#/components/responses/Forbidden" } - } - } - }, - "/auth/logout": { - "post": { - "tags": ["Auth"], - "summary": "Logout a user", - "security": [{ "CookieAuth": [] }], - "responses": { - "204": { "description": "The user was logged out successfully." }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/user/me": { - "get": { - "tags": ["User"], - "summary": "Get current user", - "security": [{ "CookieAuth": [] }], - "responses": { - "200": { "$ref": "#/components/responses/UserResponse" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, "/user/help": { "post": { "tags": ["User"], "summary": "Send email need help", - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -242,65 +62,11 @@ } } }, - "/user": { - "patch": { - "tags": ["User"], - "summary": "Edit user profile", - "security": [{ "CookieAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 2, - "example": "Jane Doe" - }, - "email": { - "type": "string", - "format": "email", - "example": "jane.doe@example.com" - }, - "password": { - "type": "string", - "minLength": 8, - "example": "newsecurepassword123" - }, - "theme": { - "type": "string", - "enum": ["light", "dark", "system"], - "example": "dark" - } - } - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "avatar": { "type": "string", "format": "binary" } - } - } - } - } - }, - "responses": { - "200": { "$ref": "#/components/responses/UserResponse" }, - "400": { "$ref": "#/components/responses/BadRequestError" }, - "406": { "$ref": "#/components/responses/NotAcceptable" }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "409": { "$ref": "#/components/responses/Conflict" } - } - } - }, "/board": { "get": { "tags": ["Board"], "summary": "Get all boards", - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/AllBoardsResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" } @@ -309,7 +75,7 @@ "post": { "tags": ["Board"], "summary": "Add new board", - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -368,7 +134,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/BoardByIdResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -389,7 +155,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -446,7 +212,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -469,7 +235,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -511,7 +277,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -562,7 +328,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -601,7 +367,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -624,7 +390,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -680,7 +446,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -728,7 +494,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -781,7 +547,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -793,7 +559,7 @@ "get": { "tags": ["Label"], "summary": "Get all labels", - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "200": { "$ref": "#/components/responses/AllLabelsResponse" }, "401": { "$ref": "#/components/responses/Unauthorized" } @@ -802,7 +568,7 @@ "post": { "tags": ["Label"], "summary": "Add new label", - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -847,7 +613,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -886,7 +652,7 @@ } } ], - "security": [{ "CookieAuth": [] }], + "security": [{ "bearerAuth": [] }], "responses": { "204": { "description": "The resource was deleted successfully." }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -897,53 +663,6 @@ }, "components": { "schemas": { - "User": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", - "example": "674723719b47fd08e6a2d517" - }, - "name": { "type": "string", "example": "John Doe" }, - "email": { - "type": "string", - "format": "email", - "example": "user@example.com" - }, - "theme": { - "type": "string", - "enum": ["light", "dark", "system"], - "example": "light" - }, - "avatar": { - "type": "string", - "format": "url", - "example": "https://res.cloudinary.com/dmbnnewoy/image/upload/v1706958682/TaskPro/user_avatar_default/user_light.png" - }, - "avatarPublicId": { - "type": "string", - "nullable": true, - "example": null - }, - "createdAt": { - "type": "string", - "format": "date-time", - "example": "2026-06-29T14:30:00.000Z" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "example": "2026-06-29T14:30:00.000Z" - } - } - }, - "AuthResponseSchema": { - "type": "object", - "properties": { - "user": { "$ref": "#/components/schemas/User" } - } - }, "BoardBackground": { "type": "object", "properties": { @@ -1198,64 +917,6 @@ } } }, - "NotAcceptable": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse", - "example": { - "statusCode": 406, - "message": "File must have .jpeg or .png extension" - } - } - } - } - }, - "AuthResponse": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AuthResponseSchema" } - } - }, - "headers": { - "Set-Cookie": { - "schema": { - "type": "string", - "example": "accessToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure; refreshToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure;" - } - } - } - }, - "RefreshResponse": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "Tokens refreshed successfully" - } - } - } - } - }, - "headers": { - "Set-Cookie": { - "schema": { - "type": "string", - "example": "accessToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure; refreshToken=eyJhbGci...; Path=/; HttpOnly; SameSite=Lax; Secure;" - } - } - } - }, - "UserResponse": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/User" } - } - } - }, "UpdateOrderBadRequest": { "content": { "application/json": { @@ -1438,11 +1099,10 @@ } }, "securitySchemes": { - "CookieAuth": { - "type": "apiKey", - "in": "cookie", - "name": "accessToken", - "description": "Access token is sent as an HTTP-only cookie." + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" } } } diff --git a/yarn.lock b/yarn.lock index 3d84fbd..2b50d5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -723,6 +723,50 @@ dependencies: "@prisma/debug" "6.19.3" +"@scalar/client-side-rendering@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@scalar/client-side-rendering/-/client-side-rendering-0.3.0.tgz#1c5abd495e5cb77524cc2a2e583fe7cda24e6297" + integrity sha512-FFpLokNEyKvpKAJesa+eDh+2e+4qdhoexAJp6alhhxUA6SGqhDfTIV4P+0nZBMdMD6WPu4YeVJHa1hRrGKUVMA== + dependencies: + "@scalar/schemas" "0.7.0" + "@scalar/types" "0.16.0" + "@scalar/validation" "0.6.0" + +"@scalar/express-api-reference@^0.10.7": + version "0.10.7" + resolved "https://registry.yarnpkg.com/@scalar/express-api-reference/-/express-api-reference-0.10.7.tgz#5791a04c6de984ec95b7187775e2a54c36189d7e" + integrity sha512-RJ335AAA25g/Byq9voBMS98WtiRgIb6MkZurINmKo5Y9RdYWL/uJl4bBzYPFyxKuRVZ6+Hp/PK0nntzKo86p+w== + dependencies: + "@scalar/client-side-rendering" "0.3.0" + +"@scalar/helpers@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@scalar/helpers/-/helpers-0.9.0.tgz#b3db11361209b4b927333487523e1e84c4ffd40f" + integrity sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ== + +"@scalar/schemas@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@scalar/schemas/-/schemas-0.7.0.tgz#e01f1a0fd7deedcf913d6dc9c554459177441244" + integrity sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA== + dependencies: + "@scalar/helpers" "0.9.0" + "@scalar/validation" "0.6.0" + +"@scalar/types@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@scalar/types/-/types-0.16.0.tgz#6dfae29d2f0fe6e6329510b0e7955dd95a8e209d" + integrity sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw== + dependencies: + "@scalar/helpers" "0.9.0" + nanoid "^5.1.6" + type-fest "^5.3.1" + zod "^4.3.5" + +"@scalar/validation@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@scalar/validation/-/validation-0.6.0.tgz#70e8b8a7eec6b86fd76f569b614e22bb41209f06" + integrity sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA== + "@scarf/scarf@=1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" @@ -1379,6 +1423,11 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + b4a@^1.6.4: version "1.8.1" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.8.1.tgz#7f16334ca80127aeb26064a28841acbf174840a4" @@ -1652,6 +1701,11 @@ cluster-key-slot@1.1.1: resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43" integrity sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw== +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -1667,6 +1721,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.0.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -2339,6 +2398,14 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== +fmtconv@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fmtconv/-/fmtconv-1.1.0.tgz#9ec73916f89773c130f988d282cbe791f63a8d7a" + integrity sha512-UV++K2xqeZBNRsVrhNjepbrHfMw3zfLArbciTs+eQmMPl635xbWaxnoOAZsvvsssAsfUnuL2S8Z7b+rC8obICw== + dependencies: + commander "^9.0.0" + js-yaml "^4.1.0" + form-data-encoder@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-4.1.0.tgz#497cedc94810bd5d53b99b5d4f6c152d5cbc9db2" @@ -3040,6 +3107,11 @@ multer@^2.2.0: concat-stream "^2.0.0" type-is "^1.6.18" +nanoid@^5.1.6: + version "5.1.16" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.16.tgz#fe345c0a1f9007c32fbb5c139e1208bfd3f41ef7" + integrity sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ== + nanostores@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-1.4.0.tgz#9acc8a6026533dc0410c37cb3b076db00163d277" @@ -3799,10 +3871,20 @@ supports-hyperlinks@^2.2.0: has-flag "^4.0.0" supports-color "^7.0.0" +swagger-merger@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/swagger-merger/-/swagger-merger-1.5.4.tgz#c1c3356a9cbc824cbffd04222eed694a53b2cd92" + integrity sha512-hdNDtg3mecF1e4/3q5P9CquHrez+rQdklwN8itQ8gVVWiugmzlRwoLu/kgNytSlkIYQ4ajjiA7L86XqRfW/QQQ== + dependencies: + async "^3.2.3" + co "^4.6.0" + commander "^9.0.0" + fmtconv "^1.1.0" + swagger-ui-dist@>=5.0.0: - version "5.32.6" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz#4a6a56786915ea4bfea43f00ff17890df8124d4c" - integrity sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA== + version "5.32.8" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz#14a5e6ba016875dfb31de26cff5c3bea725a3c89" + integrity sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA== dependencies: "@scarf/scarf" "=1.4.0" @@ -3818,6 +3900,11 @@ system-architecture@^1.0.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-1.0.0.tgz#0e71d7678d3d3b2fabfac0e00ea0306b14f8a532" integrity sha512-0OJWD12D7XX3KUg1DYkMaTTjSTo2k/mhIYI3HlBlceXSMcJhW/1qO735fPKS5prcyjvn57Ub151vvASYXpQrEw== +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tar-stream@3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" @@ -3930,6 +4017,13 @@ type-fest@^4.26.1, type-fest@^4.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== +type-fest@^5.3.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" + integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== + dependencies: + tagged-tag "^1.0.0" + type-is@^1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -4150,7 +4244,7 @@ yoctocolors@^2.1.1: resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a" integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug== -zod@^4.3.6, zod@^4.4.3: +zod@^4.3.5, zod@^4.3.6, zod@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== From 481379b803ff1e30d12a2c8ea98cfe31e21ecfaa Mon Sep 17 00:00:00 2001 From: chertik77 Date: Fri, 3 Jul 2026 21:45:11 +0100 Subject: [PATCH 4/7] feat(auth): configure redis storage and microsoft oauth --- README.md | 2 +- app/config/env.config.ts | 1 + app/controllers/user.controller.ts | 84 +++++++------------ app/lib/auth.ts | 33 +++++--- app/lib/index.ts | 1 + app/lib/map-microsoft-profile-to-user.ts | 36 ++++++++ app/lib/upload-to-cloudinary.ts | 2 +- app/routes/api/user.ts | 2 - app/schemas/auth.schema.ts | 11 --- app/schemas/index.ts | 1 - package.json | 3 +- yarn.lock | 101 ++--------------------- 12 files changed, 102 insertions(+), 175 deletions(-) create mode 100644 app/lib/map-microsoft-profile-to-user.ts delete mode 100644 app/schemas/auth.schema.ts diff --git a/README.md b/README.md index e5e62ab..0bb3831 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ _Link to the frontend repo:_ https://github.com/chertik77/TaskPro-frontend ## Languages and Tools -![Languages and Tools](https://skills.syvixor.com/api/icons?i=nodejs,ts,expressjs,prisma,mongodb,swagger,cloudinary,redis,zod,gcp,resend,azure,githubactions,yarn,commitlint,eslint,prettier,postman,vscode&perline=10) +![Languages and Tools](https://skills.syvixor.com/api/icons?i=nodejs,ts,expressjs,prisma,mongodb,swagger,cloudinary,redis,zod,betterauth,resend,githubactions,yarn,commitlint,eslint,prettier,postman,vscode&perline=10) diff --git a/app/config/env.config.ts b/app/config/env.config.ts index 01d7615..79d1043 100644 --- a/app/config/env.config.ts +++ b/app/config/env.config.ts @@ -7,6 +7,7 @@ const envSchema = z.object({ CLOUDINARY_API_SECRET: z.string(), CLOUDINARY_CLOUD_NAME: z.string(), DATABASE_URL: z.string(), + BETTER_AUTH_SECRET: z.string(), BETTER_AUTH_URL: z.url(), GOOGLE_CLIENT_ID: z.string(), GOOGLE_CLIENT_SECRET: z.string(), diff --git a/app/controllers/user.controller.ts b/app/controllers/user.controller.ts index 65d4227..34d6646 100644 --- a/app/controllers/user.controller.ts +++ b/app/controllers/user.controller.ts @@ -1,97 +1,77 @@ import type { NeedHelpSchema } from '@/schemas' import type { TypedRequestBody } from '@/types' -import type { NextFunction, Request, Response } from 'express' +import type { NextFunction, Response } from 'express' import { supportRequestAdminTemplate, supportRequestUserTemplate } from '@/emails/templates' -import { prisma } from '@/prisma' import { InternalServerError } from 'http-errors' -import { env, redisClient, resend } from '@/config' +import { env, resend } from '@/config' class UserController { - me = async (req: Request, res: Response) => { - // Disable caching for this route to prevent stale data - res.setHeader( - 'Cache-Control', - 'no-store, no-cache, must-revalidate, proxy-revalidate' - ) - res.setHeader('Pragma', 'no-cache') - res.setHeader('Expires', '0') - - const cacheKey = `user:${req.user.id}` - - const cachedUser = await redisClient.get(cacheKey) - - if (cachedUser) { - res.json(JSON.parse(cachedUser)) - } else { - const user = await prisma.user.findUnique({ - where: { id: req.user.id } - }) - - await redisClient.set(cacheKey, JSON.stringify(user), 'EX', 5 * 60) - - res.json(user) - } - } - // update = async ( - // { user, body, file }: TypedRequestBody, + // { user, body, headers, file }: TypedRequestBody, // res: Response, // next: NextFunction // ) => { - // const { id, avatarPublicId, email: userEmail } = user - // const { email, password } = body + // // @ts-expect-error it exists + // const { imagePublicId } = user + // const { email, currentPassword, newPassword, ...data } = body - // const isEmailExists = - // email && (await prisma.user.findFirst({ where: { email } })) + // if (email) auth.api.changeEmail({ body: { newEmail: email } }) - // if (email && email !== userEmail && isEmailExists) { - // return next(Conflict('Email already exist')) + // if (currentPassword && newPassword) { + // auth.api.changePassword({ + // body: { currentPassword, newPassword } + // }) // } - // const updateData: Partial = body - - // if (password) { - // updateData.password = await hash(password) - // } + // const updateData: Partial = data // if (file) { - // const extArr = ['jpeg', 'png'] + // const extArr = ['jpeg', 'png', 'jpg', 'webp'] // const ext = file.mimetype.split('/').pop() // if (!extArr.includes(ext!)) { - // return next(NotAcceptable('File must have .jpeg or .png extension')) + // return next( + // NotAcceptable( + // 'File must have .jpeg or .png or .jpg or .webp extension' + // ) + // ) // } // try { - // const newAvatar = await cloudinary.uploader.upload(file.path, { + // const newImage = await uploadToCloudinary({ + // file: file.path, // folder: 'TaskPro/user_avatars' // }) - // if (avatarPublicId) { - // await cloudinary.uploader.destroy(avatarPublicId, { + // if (imagePublicId) { + // await cloudinary.uploader.destroy(imagePublicId, { // type: 'upload', // resource_type: 'image' // }) // } - // updateData.avatar = newAvatar.url - // updateData.avatarPublicId = newAvatar.public_id + // updateData.image = newImage.url + // // @ts-expect-error it exists + // updateData.imagePublicId = newImage.public_id // } catch { // return next(InternalServerError('Uploading avatar error')) // } // } - // const updatedUser = await prisma.user.update({ - // where: { id }, - // data: updateData + // console.log(updateData) + + // const updatedUser = await auth.api.updateUser({ + // headers: fromNodeHeaders(headers), + // body: updateData, + // asResponse: true // }) - // await redisClient.del(`user:${updatedUser.id}`) + // console.log(updatedUser) // res.json(updatedUser) // } diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 80f2439..754b0dc 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -1,25 +1,38 @@ +import { redisStorage } from '@better-auth/redis-storage' import { Theme } from '@prisma/client' import { betterAuth } from 'better-auth' import { prismaAdapter } from 'better-auth/adapters/prisma' import { openAPI } from 'better-auth/plugins' -import { env } from '../config' +import { env, redisClient } from '../config' import { prisma } from '../prisma' -import { uploadToCloudinary } from './upload-to-cloudinary' +import { mapMicrosoftProfileToUser } from './map-microsoft-profile-to-user' export const auth = betterAuth({ appName: 'Task Pro', baseURL: env.BETTER_AUTH_URL, + secret: env.BETTER_AUTH_SECRET, database: prismaAdapter(prisma, { provider: 'mongodb' }), advanced: { cookiePrefix: 'taskpro', database: { generateId: false }, disableOriginCheck: env.NODE_ENV !== 'production' }, - account: { accountLinking: { trustedProviders: ['google', 'microsoft'] } }, + secondaryStorage: redisStorage({ + client: redisClient, + keyPrefix: 'taskpro:' + }), + databaseHooks: { + user: { + create: { + before: async user => ({ data: { ...user, emailVerified: true } }) + } + } + }, user: { additionalFields: { - theme: { type: Object.values(Theme), input: false } + theme: { type: Object.values(Theme), input: true }, + imagePublicId: { type: 'string', input: true } } }, emailAndPassword: { enabled: true, requireEmailVerification: false }, @@ -31,14 +44,14 @@ export const auth = betterAuth({ microsoft: { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET, + profilePhotoSize: 96, + tenantId: 'consumers', prompt: 'select_account', - async mapProfileToUser(profile) { - const imgUrl = await uploadToCloudinary({ file: profile.picture }) - - return { image: imgUrl } - } + mapProfileToUser: async profile => + await mapMicrosoftProfileToUser(profile) } }, trustedOrigins: env.ALLOWED_ORIGINS, - plugins: [openAPI()] + disabledPaths: ['/verify-email', '/send-verification-email'], + plugins: [openAPI({ disableDefaultReference: true })] }) diff --git a/app/lib/index.ts b/app/lib/index.ts index 2dcc036..4dc209f 100644 --- a/app/lib/index.ts +++ b/app/lib/index.ts @@ -1,2 +1,3 @@ +export { mapMicrosoftProfileToUser } from './map-microsoft-profile-to-user' export { uploadToCloudinary } from './upload-to-cloudinary' export { auth } from './auth' diff --git a/app/lib/map-microsoft-profile-to-user.ts b/app/lib/map-microsoft-profile-to-user.ts new file mode 100644 index 0000000..c85db06 --- /dev/null +++ b/app/lib/map-microsoft-profile-to-user.ts @@ -0,0 +1,36 @@ +import type { MicrosoftEntraIDProfile, User } from 'better-auth' + +import { prisma } from '@/prisma' + +import cloudinary from '@/config' + +import { uploadToCloudinary } from './upload-to-cloudinary' + +export const mapMicrosoftProfileToUser = async ( + profile: MicrosoftEntraIDProfile +): Promise> => { + const user = await prisma.user.findUnique({ + where: { email: profile.email } + }) + + const cleanedBase64 = profile.picture.trim().replace(/\s/g, '') + + const uploadedImage = await uploadToCloudinary({ + file: cleanedBase64, + folder: 'TaskPro/user_avatars' + }) + + if (user?.imagePublicId) { + await cloudinary.uploader.destroy(user.imagePublicId, { + type: 'upload', + resource_type: 'image' + }) + } + + return { + email: profile.email ?? profile.preferred_username ?? profile.upn, + image: uploadedImage.url, + // @ts-expect-error it exists + imagePublicId: uploadedImage.public_id + } +} diff --git a/app/lib/upload-to-cloudinary.ts b/app/lib/upload-to-cloudinary.ts index d442d8b..d1f0310 100644 --- a/app/lib/upload-to-cloudinary.ts +++ b/app/lib/upload-to-cloudinary.ts @@ -15,7 +15,7 @@ export async function uploadToCloudinary({ file, { folder, public_id, resource_type: 'auto' }, err => { - if (err) throw new Error('Cloudinary upload failed') + if (err) throw new Error(err.message) } ) diff --git a/app/routes/api/user.ts b/app/routes/api/user.ts index 05a1b2f..7414214 100644 --- a/app/routes/api/user.ts +++ b/app/routes/api/user.ts @@ -10,8 +10,6 @@ export const userRouter = Router() userRouter.use(requireAuth) -userRouter.get('/me', userController.me) - // userRouter.patch( // '/', // upload.single('avatar'), diff --git a/app/schemas/auth.schema.ts b/app/schemas/auth.schema.ts deleted file mode 100644 index feb4b67..0000000 --- a/app/schemas/auth.schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as z from 'zod' - -export const SigninSchema = z.object({ - email: z.email(), - password: z.string().min(8).max(64) -}) - -export const SignupSchema = z.object({ - ...SigninSchema.shape, - name: z.string().min(2) -}) diff --git a/app/schemas/index.ts b/app/schemas/index.ts index 6bf4cda..766f058 100644 --- a/app/schemas/index.ts +++ b/app/schemas/index.ts @@ -20,5 +20,4 @@ export { EditLabelSchema, LabelParamsSchema } from './label.schema' -export { SigninSchema, SignupSchema } from './auth.schema' export { EditUserSchema, NeedHelpSchema } from './user.schema' diff --git a/package.json b/package.json index 747d35e..cd81df9 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "schema": "./prisma" }, "dependencies": { + "@better-auth/redis-storage": "^1.6.23", "@prisma/client": "^6.19.3", - "@scalar/express-api-reference": "^0.10.7", "better-auth": "^1.6.23", "cloudinary": "^2.10.0", "cookie-parser": "^1.4.7", @@ -33,7 +33,6 @@ "morgan": "1.11.0", "multer": "^2.2.0", "resend": "^6.16.0", - "swagger-merger": "^1.5.4", "swagger-ui-express": "^5.0.1", "zod": "^4.4.3" }, diff --git a/yarn.lock b/yarn.lock index 2b50d5b..fcfba92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,6 +108,11 @@ resolved "https://registry.yarnpkg.com/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz#d141a4cc74785392bb0490d37002bd6d75b3b661" integrity sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ== +"@better-auth/redis-storage@^1.6.23": + version "1.6.23" + resolved "https://registry.yarnpkg.com/@better-auth/redis-storage/-/redis-storage-1.6.23.tgz#a9db43f26a5b74298035e1b0cd8c19285592b0d9" + integrity sha512-rkAbFEYBZNXHltqkSJeCerb9v/y5M5JnHSQP+7r25TjuWHlZsEIvG160ewXg8cgD7H3xW6h5FRShzW6H9dCWDQ== + "@better-auth/telemetry@1.6.23": version "1.6.23" resolved "https://registry.yarnpkg.com/@better-auth/telemetry/-/telemetry-1.6.23.tgz#a232e560c2e7f618c4ad19e01565c0ea8b84740e" @@ -723,50 +728,6 @@ dependencies: "@prisma/debug" "6.19.3" -"@scalar/client-side-rendering@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@scalar/client-side-rendering/-/client-side-rendering-0.3.0.tgz#1c5abd495e5cb77524cc2a2e583fe7cda24e6297" - integrity sha512-FFpLokNEyKvpKAJesa+eDh+2e+4qdhoexAJp6alhhxUA6SGqhDfTIV4P+0nZBMdMD6WPu4YeVJHa1hRrGKUVMA== - dependencies: - "@scalar/schemas" "0.7.0" - "@scalar/types" "0.16.0" - "@scalar/validation" "0.6.0" - -"@scalar/express-api-reference@^0.10.7": - version "0.10.7" - resolved "https://registry.yarnpkg.com/@scalar/express-api-reference/-/express-api-reference-0.10.7.tgz#5791a04c6de984ec95b7187775e2a54c36189d7e" - integrity sha512-RJ335AAA25g/Byq9voBMS98WtiRgIb6MkZurINmKo5Y9RdYWL/uJl4bBzYPFyxKuRVZ6+Hp/PK0nntzKo86p+w== - dependencies: - "@scalar/client-side-rendering" "0.3.0" - -"@scalar/helpers@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@scalar/helpers/-/helpers-0.9.0.tgz#b3db11361209b4b927333487523e1e84c4ffd40f" - integrity sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ== - -"@scalar/schemas@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@scalar/schemas/-/schemas-0.7.0.tgz#e01f1a0fd7deedcf913d6dc9c554459177441244" - integrity sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA== - dependencies: - "@scalar/helpers" "0.9.0" - "@scalar/validation" "0.6.0" - -"@scalar/types@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@scalar/types/-/types-0.16.0.tgz#6dfae29d2f0fe6e6329510b0e7955dd95a8e209d" - integrity sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw== - dependencies: - "@scalar/helpers" "0.9.0" - nanoid "^5.1.6" - type-fest "^5.3.1" - zod "^4.3.5" - -"@scalar/validation@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@scalar/validation/-/validation-0.6.0.tgz#70e8b8a7eec6b86fd76f569b614e22bb41209f06" - integrity sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA== - "@scarf/scarf@=1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" @@ -1423,11 +1384,6 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - b4a@^1.6.4: version "1.8.1" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.8.1.tgz#7f16334ca80127aeb26064a28841acbf174840a4" @@ -1701,11 +1657,6 @@ cluster-key-slot@1.1.1: resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43" integrity sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw== -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -1721,11 +1672,6 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@^9.0.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -2398,14 +2344,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== -fmtconv@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fmtconv/-/fmtconv-1.1.0.tgz#9ec73916f89773c130f988d282cbe791f63a8d7a" - integrity sha512-UV++K2xqeZBNRsVrhNjepbrHfMw3zfLArbciTs+eQmMPl635xbWaxnoOAZsvvsssAsfUnuL2S8Z7b+rC8obICw== - dependencies: - commander "^9.0.0" - js-yaml "^4.1.0" - form-data-encoder@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-4.1.0.tgz#497cedc94810bd5d53b99b5d4f6c152d5cbc9db2" @@ -3107,11 +3045,6 @@ multer@^2.2.0: concat-stream "^2.0.0" type-is "^1.6.18" -nanoid@^5.1.6: - version "5.1.16" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.16.tgz#fe345c0a1f9007c32fbb5c139e1208bfd3f41ef7" - integrity sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ== - nanostores@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-1.4.0.tgz#9acc8a6026533dc0410c37cb3b076db00163d277" @@ -3871,16 +3804,6 @@ supports-hyperlinks@^2.2.0: has-flag "^4.0.0" supports-color "^7.0.0" -swagger-merger@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/swagger-merger/-/swagger-merger-1.5.4.tgz#c1c3356a9cbc824cbffd04222eed694a53b2cd92" - integrity sha512-hdNDtg3mecF1e4/3q5P9CquHrez+rQdklwN8itQ8gVVWiugmzlRwoLu/kgNytSlkIYQ4ajjiA7L86XqRfW/QQQ== - dependencies: - async "^3.2.3" - co "^4.6.0" - commander "^9.0.0" - fmtconv "^1.1.0" - swagger-ui-dist@>=5.0.0: version "5.32.8" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz#14a5e6ba016875dfb31de26cff5c3bea725a3c89" @@ -3900,11 +3823,6 @@ system-architecture@^1.0.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-1.0.0.tgz#0e71d7678d3d3b2fabfac0e00ea0306b14f8a532" integrity sha512-0OJWD12D7XX3KUg1DYkMaTTjSTo2k/mhIYI3HlBlceXSMcJhW/1qO735fPKS5prcyjvn57Ub151vvASYXpQrEw== -tagged-tag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" - integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== - tar-stream@3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" @@ -4017,13 +3935,6 @@ type-fest@^4.26.1, type-fest@^4.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== -type-fest@^5.3.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" - integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== - dependencies: - tagged-tag "^1.0.0" - type-is@^1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -4244,7 +4155,7 @@ yoctocolors@^2.1.1: resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a" integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug== -zod@^4.3.5, zod@^4.3.6, zod@^4.4.3: +zod@^4.3.6, zod@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== From 0c1a4799186f3a6d75bf86af8694562af083208e Mon Sep 17 00:00:00 2001 From: chertik77 Date: Sat, 4 Jul 2026 18:03:49 +0100 Subject: [PATCH 5/7] chore: update openapi docs and naming --- app/controllers/board.controller.ts | 14 +- app/controllers/column.controller.ts | 16 +- app/controllers/label.controller.ts | 10 +- app/controllers/task.controller.ts | 16 +- app/controllers/user.controller.ts | 2 +- app/routes/api/board.ts | 12 +- app/routes/api/column.ts | 12 +- app/routes/api/task.ts | 12 +- app/routes/api/user.ts | 2 +- app/routes/index.ts | 6 +- app/schemas/board.schema.ts | 4 +- app/schemas/column.schema.ts | 4 +- app/schemas/index.ts | 16 +- app/schemas/label.schema.ts | 2 +- app/schemas/task.schema.ts | 6 +- app/schemas/user.schema.ts | 2 +- swagger.json => openapi.json | 343 +++++++++++++++------------ package.json | 2 +- 18 files changed, 276 insertions(+), 205 deletions(-) rename swagger.json => openapi.json (82%) diff --git a/app/controllers/board.controller.ts b/app/controllers/board.controller.ts index 86f9d03..f5c0544 100644 --- a/app/controllers/board.controller.ts +++ b/app/controllers/board.controller.ts @@ -1,7 +1,7 @@ import type { - AddBoardSchema, BoardParamsSchema, - EditBoardSchema + CreateBoardSchema, + UpdateBoardSchema } from '@/schemas' import type { TypedRequest, @@ -66,8 +66,8 @@ class BoardController { } } - add = async ( - { body, user }: TypedRequestBody, + create = async ( + { body, user }: TypedRequestBody, res: Response ) => { const newBoard = await prisma.board.create({ @@ -91,7 +91,11 @@ class BoardController { body, params, user - }: TypedRequest, + }: TypedRequest< + typeof BoardParamsSchema, + ZodType, + typeof UpdateBoardSchema + >, res: Response, next: NextFunction ) => { diff --git a/app/controllers/column.controller.ts b/app/controllers/column.controller.ts index b33cd2a..4b7c371 100644 --- a/app/controllers/column.controller.ts +++ b/app/controllers/column.controller.ts @@ -1,9 +1,9 @@ import type { - AddBoardSchema, BoardParamsSchema, ColumnParamsSchema, - EditColumnSchema, - UpdateColumnOrderSchema + CreateBoardSchema, + UpdateColumnOrderSchema, + UpdateColumnSchema } from '@/schemas' import type { TypedRequest, TypedRequestParams } from '@/types' import type { NextFunction, Response } from 'express' @@ -15,12 +15,16 @@ import { BadRequest, NotFound } from 'http-errors' import { redisClient } from '@/config' class ColumnController { - add = async ( + create = async ( { params, user, body - }: TypedRequest, + }: TypedRequest< + typeof BoardParamsSchema, + ZodType, + typeof CreateBoardSchema + >, res: Response, next: NextFunction ) => { @@ -54,7 +58,7 @@ class ColumnController { }: TypedRequest< typeof ColumnParamsSchema, ZodType, - typeof EditColumnSchema + typeof UpdateColumnSchema >, res: Response, next: NextFunction diff --git a/app/controllers/label.controller.ts b/app/controllers/label.controller.ts index 16d2555..50b0f45 100644 --- a/app/controllers/label.controller.ts +++ b/app/controllers/label.controller.ts @@ -1,7 +1,7 @@ import type { CreateLabelSchema, - EditLabelSchema, - LabelParamsSchema + LabelParamsSchema, + UpdateLabelSchema } from '@/schemas' import type { TypedRequest, @@ -53,7 +53,11 @@ class LabelController { params, body, user - }: TypedRequest, + }: TypedRequest< + typeof LabelParamsSchema, + ZodType, + typeof UpdateLabelSchema + >, res: Response, next: NextFunction ) => { diff --git a/app/controllers/task.controller.ts b/app/controllers/task.controller.ts index 7fe6b2a..f9fa866 100644 --- a/app/controllers/task.controller.ts +++ b/app/controllers/task.controller.ts @@ -1,9 +1,9 @@ import type { - AddTaskSchema, ColumnParamsSchema, - EditTaskSchema, + CreateTaskSchema, TaskParamsSchema, - UpdateTaskOrderSchema + UpdateTaskOrderSchema, + UpdateTaskSchema } from '@/schemas' import type { TypedRequest, TypedRequestParams } from '@/types' import type { NextFunction, Response } from 'express' @@ -15,11 +15,15 @@ import { BadRequest, NotFound } from 'http-errors' import { redisClient } from '@/config' class TaskController { - add = async ( + create = async ( { params, body - }: TypedRequest, + }: TypedRequest< + typeof ColumnParamsSchema, + ZodType, + typeof CreateTaskSchema + >, res: Response, next: NextFunction ) => { @@ -51,7 +55,7 @@ class TaskController { { params, body - }: TypedRequest, + }: TypedRequest, res: Response, next: NextFunction ) => { diff --git a/app/controllers/user.controller.ts b/app/controllers/user.controller.ts index 34d6646..a558b39 100644 --- a/app/controllers/user.controller.ts +++ b/app/controllers/user.controller.ts @@ -12,7 +12,7 @@ import { env, resend } from '@/config' class UserController { // update = async ( - // { user, body, headers, file }: TypedRequestBody, + // { user, body, headers, file }: TypedRequestBody, // res: Response, // next: NextFunction // ) => { diff --git a/app/routes/api/board.ts b/app/routes/api/board.ts index b232ad5..facd4da 100644 --- a/app/routes/api/board.ts +++ b/app/routes/api/board.ts @@ -4,7 +4,11 @@ import { boardController } from '@/controllers' import { requireAuth, validateRequest } from '@/middlewares' -import { AddBoardSchema, BoardParamsSchema, EditBoardSchema } from '@/schemas' +import { + BoardParamsSchema, + CreateBoardSchema, + UpdateBoardSchema +} from '@/schemas' export const boardRouter = Router() @@ -20,13 +24,13 @@ boardRouter.get( boardRouter.post( '/', - validateRequest({ body: AddBoardSchema }), - boardController.add + validateRequest({ body: CreateBoardSchema }), + boardController.create ) boardRouter.patch( '/:boardId', - validateRequest({ body: EditBoardSchema, params: BoardParamsSchema }), + validateRequest({ body: UpdateBoardSchema, params: BoardParamsSchema }), boardController.updateById ) diff --git a/app/routes/api/column.ts b/app/routes/api/column.ts index e235db3..8b3884b 100644 --- a/app/routes/api/column.ts +++ b/app/routes/api/column.ts @@ -5,11 +5,11 @@ import { columnController } from '@/controllers' import { requireAuth, validateRequest } from '@/middlewares' import { - AddColumnSchema, BoardParamsSchema, ColumnParamsSchema, - EditColumnSchema, - UpdateColumnOrderSchema + CreateColumnSchema, + UpdateColumnOrderSchema, + UpdateColumnSchema } from '@/schemas' export const columnRouter = Router() @@ -18,13 +18,13 @@ columnRouter.use(requireAuth) columnRouter.post( '/:boardId', - validateRequest({ body: AddColumnSchema, params: BoardParamsSchema }), - columnController.add + validateRequest({ body: CreateColumnSchema, params: BoardParamsSchema }), + columnController.create ) columnRouter.patch( '/:columnId', - validateRequest({ body: EditColumnSchema, params: ColumnParamsSchema }), + validateRequest({ body: UpdateColumnSchema, params: ColumnParamsSchema }), columnController.updateById ) diff --git a/app/routes/api/task.ts b/app/routes/api/task.ts index 05c9907..bdad23b 100644 --- a/app/routes/api/task.ts +++ b/app/routes/api/task.ts @@ -5,11 +5,11 @@ import { taskController } from '@/controllers' import { requireAuth, validateRequest } from '@/middlewares' import { - AddTaskSchema, ColumnParamsSchema, - EditTaskSchema, + CreateTaskSchema, TaskParamsSchema, - UpdateTaskOrderSchema + UpdateTaskOrderSchema, + UpdateTaskSchema } from '@/schemas' export const taskRouter = Router() @@ -18,13 +18,13 @@ taskRouter.use(requireAuth) taskRouter.post( '/:columnId', - validateRequest({ body: AddTaskSchema, params: ColumnParamsSchema }), - taskController.add + validateRequest({ body: CreateTaskSchema, params: ColumnParamsSchema }), + taskController.create ) taskRouter.patch( '/:taskId', - validateRequest({ body: EditTaskSchema, params: TaskParamsSchema }), + validateRequest({ body: UpdateTaskSchema, params: TaskParamsSchema }), taskController.updateById ) diff --git a/app/routes/api/user.ts b/app/routes/api/user.ts index 7414214..9c57b8f 100644 --- a/app/routes/api/user.ts +++ b/app/routes/api/user.ts @@ -13,7 +13,7 @@ userRouter.use(requireAuth) // userRouter.patch( // '/', // upload.single('avatar'), -// validateRequest({ body: EditUserSchema }), +// validateRequest({ body: UpdateUserSchema }), // userController.update // ) diff --git a/app/routes/index.ts b/app/routes/index.ts index 2187114..d570b54 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -13,10 +13,10 @@ import { userRouter } from './api/user' export const apiRouter = Router() -const swaggerPath = path.join(process.cwd(), 'swagger.json') +const swaggerPath = path.join(process.cwd(), 'openapi.json') const swagger = JSON.parse(fs.readFileSync(swaggerPath, 'utf-8')) -apiRouter.get('/swagger.json', (_, res) => { +apiRouter.get('/openapi.json', (_, res) => { res.json(swagger) }) @@ -25,7 +25,7 @@ const swaggerOptions: SwaggerUiOptions = { explorer: true, swaggerOptions: { urls: [ - { url: '/api/swagger.json', name: 'Main API' }, + { url: '/api/openapi.json', name: 'Main API' }, { url: '/api/auth/open-api/generate-schema', name: 'Auth' } ], 'urls.primaryName': 'Main API' diff --git a/app/schemas/board.schema.ts b/app/schemas/board.schema.ts index b9e9ace..3fc2e25 100644 --- a/app/schemas/board.schema.ts +++ b/app/schemas/board.schema.ts @@ -13,12 +13,12 @@ const zObjectKeys = >(obj: T) => { ) } -export const AddBoardSchema = z.object({ +export const CreateBoardSchema = z.object({ title: z.string().min(3), icon: z.enum(Icon), background: zObjectKeys(boardImages) }) -export const EditBoardSchema = AddBoardSchema.partial() +export const UpdateBoardSchema = CreateBoardSchema.partial() export const BoardParamsSchema = z.object({ boardId: objectIdSchema() }) diff --git a/app/schemas/column.schema.ts b/app/schemas/column.schema.ts index db690ed..b0ef8c0 100644 --- a/app/schemas/column.schema.ts +++ b/app/schemas/column.schema.ts @@ -2,11 +2,11 @@ import * as z from 'zod' import { objectIdSchema } from './object-id.schema' -export const AddColumnSchema = z.object({ +export const CreateColumnSchema = z.object({ title: z.string().min(3) }) -export const EditColumnSchema = AddColumnSchema.partial() +export const UpdateColumnSchema = CreateColumnSchema.partial() export const ColumnParamsSchema = z.object({ columnId: objectIdSchema() }) diff --git a/app/schemas/index.ts b/app/schemas/index.ts index 766f058..8354e7e 100644 --- a/app/schemas/index.ts +++ b/app/schemas/index.ts @@ -1,23 +1,23 @@ export { - AddBoardSchema, - EditBoardSchema, + CreateBoardSchema, + UpdateBoardSchema, BoardParamsSchema } from './board.schema' export { - AddTaskSchema, - EditTaskSchema, + CreateTaskSchema, + UpdateTaskSchema, TaskParamsSchema, UpdateTaskOrderSchema } from './task.schema' export { - AddColumnSchema, - EditColumnSchema, + CreateColumnSchema, + UpdateColumnSchema, ColumnParamsSchema, UpdateColumnOrderSchema } from './column.schema' export { CreateLabelSchema, - EditLabelSchema, + UpdateLabelSchema, LabelParamsSchema } from './label.schema' -export { EditUserSchema, NeedHelpSchema } from './user.schema' +export { UpdateUserSchema, NeedHelpSchema } from './user.schema' diff --git a/app/schemas/label.schema.ts b/app/schemas/label.schema.ts index 07d255f..67f0e6b 100644 --- a/app/schemas/label.schema.ts +++ b/app/schemas/label.schema.ts @@ -8,6 +8,6 @@ export const CreateLabelSchema = z.object({ color: z.enum(LabelColor) }) -export const EditLabelSchema = CreateLabelSchema.partial() +export const UpdateLabelSchema = CreateLabelSchema.partial() export const LabelParamsSchema = z.object({ labelId: objectIdSchema() }) diff --git a/app/schemas/task.schema.ts b/app/schemas/task.schema.ts index d6ade1c..69d7633 100644 --- a/app/schemas/task.schema.ts +++ b/app/schemas/task.schema.ts @@ -3,7 +3,7 @@ import * as z from 'zod' import { objectIdSchema } from './object-id.schema' -export const AddTaskSchema = z.object({ +export const CreateTaskSchema = z.object({ title: z.string().min(3), description: z.optional(z.string().min(3)), priority: z.enum(Priority), @@ -24,9 +24,9 @@ export const AddTaskSchema = z.object({ .transform(v => (v ? new Date(v) : null)) }) -export const EditTaskSchema = z +export const UpdateTaskSchema = z .object({ - ...AddTaskSchema.shape, + ...CreateTaskSchema.shape, completed: z.boolean(), description: z.nullable(z.string().min(3)), deadline: z.nullable(z.iso.datetime()), diff --git a/app/schemas/user.schema.ts b/app/schemas/user.schema.ts index 43aca94..224be5a 100644 --- a/app/schemas/user.schema.ts +++ b/app/schemas/user.schema.ts @@ -6,7 +6,7 @@ export const NeedHelpSchema = z.object({ comment: z.string().min(5) }) -export const EditUserSchema = z +export const UpdateUserSchema = z .object({ name: z.string().min(2), email: z.email(), diff --git a/swagger.json b/openapi.json similarity index 82% rename from swagger.json rename to openapi.json index 1cbdcc2..ef291fd 100644 --- a/swagger.json +++ b/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.1.0", "info": { - "version": "2.2.0", + "version": "2.3.0", "title": "Task Pro API", "description": "Task Pro API provides endpoints for managing projects, tasks, and user assignments with secure authentication and efficient workflows.", "contact": { @@ -17,6 +17,7 @@ "/user/help": { "post": { "tags": ["User"], + "operationId": "help", "summary": "Send email need help", "security": [{ "bearerAuth": [] }], "requestBody": { @@ -65,6 +66,7 @@ "/board": { "get": { "tags": ["Board"], + "operationId": "getAllBoards", "summary": "Get all boards", "security": [{ "bearerAuth": [] }], "responses": { @@ -74,7 +76,8 @@ }, "post": { "tags": ["Board"], - "summary": "Add new board", + "operationId": "createBoard", + "summary": "Create new board", "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, @@ -82,30 +85,15 @@ "application/json": { "schema": { "type": "object", - "required": ["title", "icon"], "properties": { "title": { "type": "string", "minLength": 3, "example": "My New Project" }, - "icon": { - "type": "string", - "enum": [ - "project", - "star", - "loading", - "puzzle", - "container", - "lightning", - "colors", - "hexagon" - ], - "example": "project" - }, + "icon": { "$ref": "#/components/schemas/BoardIcon" }, "background": { - "type": "string", - "example": "blue-ball" + "$ref": "#/components/schemas/BoardBackgroundId" } } } @@ -122,6 +110,7 @@ "/board/{boardId}": { "get": { "tags": ["Board"], + "operationId": "getBoardById", "summary": "Get board by id", "parameters": [ { @@ -129,8 +118,7 @@ "name": "boardId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -143,15 +131,15 @@ }, "patch": { "tags": ["Board"], - "summary": "Edit board by id", + "operationId": "updateBoard", + "summary": "Update board by id", "parameters": [ { "in": "path", "name": "boardId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -168,23 +156,9 @@ "minLength": 3, "example": "Updated Project Office" }, - "icon": { - "type": "string", - "enum": [ - "project", - "star", - "loading", - "puzzle", - "container", - "lightning", - "colors", - "hexagon" - ], - "example": "star" - }, + "icon": { "$ref": "#/components/schemas/BoardIcon" }, "background": { - "type": "string", - "example": "flowers" + "$ref": "#/components/schemas/BoardBackgroundId" } } } @@ -200,6 +174,7 @@ }, "delete": { "tags": ["Board"], + "operationId": "deleteBoard", "summary": "Delete board by id", "parameters": [ { @@ -207,8 +182,7 @@ "name": "boardId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -223,15 +197,15 @@ "/column/{boardId}": { "post": { "tags": ["Column"], - "summary": "Add new column", + "operationId": "createColumn", + "summary": "Create new column", "parameters": [ { "in": "path", "name": "boardId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -265,6 +239,7 @@ "/column/{boardId}/order": { "patch": { "tags": ["Column"], + "operationId": "updateColumnsOrder", "summary": "Update columns order", "parameters": [ { @@ -272,8 +247,7 @@ "name": "boardId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -289,8 +263,7 @@ "ids": { "type": "array", "items": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" }, "example": [ "67470731ed1b2ff3a2e5bd56", @@ -316,15 +289,15 @@ "/column/{columnId}": { "patch": { "tags": ["Column"], - "summary": "Edit column by id", + "operationId": "updateColumn", + "summary": "Update column by id", "parameters": [ { "in": "path", "name": "columnId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -355,6 +328,7 @@ }, "delete": { "tags": ["Column"], + "operationId": "deleteColumn", "summary": "Delete column by id", "parameters": [ { @@ -362,8 +336,7 @@ "name": "columnId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -378,15 +351,15 @@ "/task/{columnId}": { "post": { "tags": ["Task"], - "summary": "Add new task", + "operationId": "createTask", + "summary": "Create new task", "parameters": [ { "in": "path", "name": "columnId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -406,6 +379,7 @@ }, "description": { "type": "string", + "minLength": 3, "example": "Review the project materials: Familiarize yourself with the project's content, including text, images, and any supplementary materials." }, "deadline": { @@ -413,10 +387,12 @@ "format": "date", "example": "2025-10-15" }, - "priority": { - "type": "string", - "enum": ["without", "low", "medium", "high"], - "example": "High" + "priority": { "$ref": "#/components/schemas/TaskPriority" }, + "labels": { + "type": "array", + "description": "Array of Label ids to attach to the task", + "items": { "type": "string" }, + "example": ["6672fdccc07147fc7ae1bb93"] } } } @@ -434,6 +410,7 @@ "/task/{columnId}/order": { "patch": { "tags": ["Task"], + "operationId": "updateTaskOrder", "summary": "Update tasks order", "parameters": [ { @@ -441,8 +418,7 @@ "name": "columnId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -458,8 +434,7 @@ "ids": { "type": "array", "items": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" }, "example": [ "6672fdccc07147fc7ae1bb93", @@ -482,15 +457,15 @@ "/task/{taskId}": { "patch": { "tags": ["Task"], - "summary": "Edit task by id", + "operationId": "updateTask", + "summary": "Update task by id", "parameters": [ { "in": "path", "name": "taskId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -508,18 +483,28 @@ "example": "Final Project Review" }, "description": { - "type": "string", + "type": ["string", "null"], + "minLength": 3, "example": "Review and finalize all project documentation." }, "deadline": { - "type": "string", + "type": ["string", "null"], "format": "date", "example": "2025-11-01" }, "priority": { - "type": "string", - "enum": ["without", "low", "medium", "high"], - "example": "Low" + "allOf": [{ "$ref": "#/components/schemas/TaskPriority" }], + "example": "low" + }, + "completed": { + "type": "boolean", + "example": true + }, + "labels": { + "type": "array", + "description": "Array of Label ids to attach to the task", + "items": { "type": "string" }, + "example": ["6672fdccc07147fc7ae1bb93"] } } } @@ -535,6 +520,7 @@ }, "delete": { "tags": ["Task"], + "operationId": "deleteTask", "summary": "Delete task by id", "parameters": [ { @@ -542,8 +528,7 @@ "name": "taskId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -558,6 +543,7 @@ "/label": { "get": { "tags": ["Label"], + "operationId": "getAllLabels", "summary": "Get all labels", "security": [{ "bearerAuth": [] }], "responses": { @@ -567,7 +553,8 @@ }, "post": { "tags": ["Label"], - "summary": "Add new label", + "operationId": "createLabel", + "summary": "Create new label", "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, @@ -582,10 +569,7 @@ "minLength": 2, "example": "Bug" }, - "color": { - "type": "string", - "example": "red" - } + "color": { "$ref": "#/components/schemas/LabelColor" } } } } @@ -601,15 +585,15 @@ "/label/{labelId}": { "patch": { "tags": ["Label"], - "summary": "Edit label by id", + "operationId": "updateLabel", + "summary": "Update label by id", "parameters": [ { "in": "path", "name": "labelId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -625,7 +609,8 @@ "type": "string", "minLength": 2, "example": "Bug" - } + }, + "color": { "$ref": "#/components/schemas/LabelColor" } } } } @@ -640,6 +625,7 @@ }, "delete": { "tags": ["Label"], + "operationId": "deleteLabel", "summary": "Delete label by id", "parameters": [ { @@ -647,8 +633,7 @@ "name": "labelId", "required": true, "schema": { - "type": "string", - "pattern": "^[0-9a-fA-F]{24}$" + "type": "string" } } ], @@ -663,46 +648,36 @@ }, "components": { "schemas": { - "BoardBackground": { - "type": "object", - "properties": { - "identifier": { "type": "string", "example": "flowers" }, - "url": { - "type": "string", - "format": "url", - "example": "https://res.cloudinary.com/dmbnnewoy/image/upload/v1707099093/TaskPro/board_bg_images/desk/nfxep55xgvpq7xitemq1.jpg" - } - } - }, "Board": { "type": "object", + "required": [ + "id", + "title", + "icon", + "background", + "userId", + "createdAt", + "updatedAt" + ], "properties": { "id": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6672f1d2f02c8b04130c718c" }, - "title": { "type": "string", "example": "Project office" }, - "icon": { + "title": { "type": "string", - "enum": [ - "project", - "star", - "loading", - "puzzle", - "container", - "lightning", - "colors", - "hexagon" - ], - "example": "project" + "example": "Project office" }, + "icon": { "$ref": "#/components/schemas/BoardIcon" }, "background": { "$ref": "#/components/schemas/BoardBackground" }, "userId": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6671743db85c96453e04a684" }, + "columns": { + "type": "array", + "items": { "$ref": "#/components/schemas/Column" } + }, "createdAt": { "type": "string", "format": "date-time", @@ -715,19 +690,74 @@ } } }, + "BoardBackground": { + "type": "object", + "required": ["identifier", "url"], + "properties": { + "identifier": { "$ref": "#/components/schemas/BoardBackgroundId" }, + "url": { + "type": ["string", "null"], + "format": "url", + "example": "https://res.cloudinary.com/dmbnnewoy/image/upload/v1707099093/TaskPro/board_bg_images/desk/nfxep55xgvpq7xitemq1.jpg" + } + } + }, + "BoardBackgroundId": { + "type": "string", + "enum": [ + "default", + "flowers", + "mountains-night", + "sakura", + "night-sky", + "tropics", + "light-sky", + "sea-rocks", + "blue-ball", + "red-moon", + "sea-boat", + "air-balloons", + "gorge", + "sea-boat-2", + "air-balloons-2", + "northern-lights" + ], + "example": "flowers" + }, + "BoardIcon": { + "type": "string", + "enum": [ + "project", + "star", + "loading", + "puzzle", + "container", + "lightning", + "colors", + "hexagon" + ], + "example": "project" + }, "Column": { "type": "object", + "required": [ + "id", + "title", + "order", + "boardId", + "tasks", + "createdAt", + "updatedAt" + ], "properties": { "id": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6672f604c07147fc7ae1bb81" }, "title": { "type": "string", "example": "In progress" }, "order": { "type": "integer", "example": 1 }, "boardId": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "67470718ed1b2ff3a2e5bd55" }, "tasks": { @@ -748,10 +778,22 @@ }, "Task": { "type": "object", + "required": [ + "id", + "title", + "description", + "priority", + "deadline", + "completed", + "columnId", + "order", + "completedAt", + "createdAt", + "updatedAt" + ], "properties": { "id": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6672fdccc07147fc7ae1bb93" }, "title": { @@ -759,32 +801,30 @@ "example": "Publication of the project" }, "description": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "example": "Review the project materials: Familiarize yourself with the project's content, including text, images, and any supplementary materials." }, - "priority": { - "type": "string", - "enum": ["without", "low", "medium", "high"], - "example": "high" - }, + "priority": { "$ref": "#/components/schemas/TaskPriority" }, "deadline": { - "type": "string", + "type": ["string", "null"], "format": "date-time", "example": "2025-10-15T00:00:00.000Z" }, "order": { "type": "integer", "example": 1 }, "completed": { "type": "boolean", "example": false }, "completedAt": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "example": "2026-06-29T14:30:00.000Z" + "example": null }, "columnId": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6671743db85c96453e04a684" }, + "labels": { + "type": "array", + "items": { "$ref": "#/components/schemas/Label" } + }, "createdAt": { "type": "string", "format": "date-time", @@ -797,41 +837,38 @@ } } }, + "TaskPriority": { + "type": "string", + "enum": ["without", "low", "medium", "high"], + "example": "high" + }, "Label": { "type": "object", + "required": [ + "id", + "name", + "color", + "userId", + "taskId", + "createdAt", + "updatedAt" + ], "properties": { "id": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6672fdccc07147fc7ae1bb93" }, "name": { "type": "string", "example": "Bug" }, - "color": { - "type": "string", - "enum": [ - "blue", - "purple", - "red", - "gray", - "cyan", - "yellow", - "indigo", - "green" - ], - "example": "red" - }, + "color": { "$ref": "#/components/schemas/LabelColor" }, "userId": { "type": "string", - "pattern": "^[0-9a-fA-F]{24}$", "example": "6671743db85c96453e04a684" }, "taskId": { - "type": "string", - "nullable": true, - "pattern": "^[0-9a-fA-F]{24}$", + "type": ["string", "null"], "example": null }, "createdAt": { @@ -846,6 +883,20 @@ } } }, + "LabelColor": { + "type": "string", + "enum": [ + "blue", + "purple", + "red", + "gray", + "cyan", + "yellow", + "indigo", + "green" + ], + "example": "red" + }, "ErrorResponse": { "type": "object", "properties": { @@ -1090,7 +1141,7 @@ "example": { "statusCode": 400, "message": { - "color": "The field must be one of the following values: blue, violet, rose, gray, cyan, amber, indigo, emerald" + "color": "The field must be one of the following values: blue, purple, red, gray, cyan, yellow, indigo, green" } } } diff --git a/package.json b/package.json index cd81df9..24d0db3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "start": "node dist/app/server.js", "build": "swc app -d dist --copy-files", - "dev": "tsx watch --include ./swagger.json app/server.ts", + "dev": "tsx watch --include ./openapi.json app/server.ts", "lint": "eslint --fix app/**/*.ts", "format": "prettier --write ./app", "format:check": "prettier --check ./app", From 5b2fd4dea55d311aecb255d2e4ee849f0800eb2d Mon Sep 17 00:00:00 2001 From: chertik77 Date: Sun, 5 Jul 2026 15:36:29 +0100 Subject: [PATCH 6/7] feat(api): add columnId to task schema --- openapi.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openapi.json b/openapi.json index ef291fd..82d6c73 100644 --- a/openapi.json +++ b/openapi.json @@ -500,6 +500,11 @@ "type": "boolean", "example": true }, + "columnId": { + "type": "string", + "example": "6672fdccc07147fc7ae1bb93", + "description": "Id of the column to move the task to" + }, "labels": { "type": "array", "description": "Array of Label ids to attach to the task", From c075021e65e632819acf843a91ea4decf71d3024 Mon Sep 17 00:00:00 2001 From: chertik77 Date: Sun, 5 Jul 2026 15:51:21 +0100 Subject: [PATCH 7/7] refactor(env): reorder and clean .env.example variables --- .env.example | 29 +++++++++++------------------ README.md | 2 +- app/middlewares/require-auth.ts | 18 +++++++----------- prisma/models/passkey.prisma | 20 -------------------- 4 files changed, 19 insertions(+), 50 deletions(-) delete mode 100644 prisma/models/passkey.prisma diff --git a/.env.example b/.env.example index d737f02..2a3ed07 100644 --- a/.env.example +++ b/.env.example @@ -1,27 +1,20 @@ +DATABASE_URL= +PORT= +API_PREFIX= +NODE_ENV= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= +BETTER_AUTH_SECRET= +BETTER_AUTH_URL= CLOUDINARY_API_KEY= CLOUDINARY_API_SECRET= CLOUDINARY_CLOUD_NAME= -DATABASE_URL= -FRONTEND_URL= -NODE_ENV= +ALLOWED_ORIGINS= RESEND_API_KEY= RESEND_RECEIVER= REDIS_USERNAME= REDIS_PASSWORD= REDIS_HOST= REDIS_PORT= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -GOOGLE_REDIRECT_URI= -MICROSOFT_CLIENT_ID= -MICROSOFT_CLIENT_SECRET= -MICROSOFT_REDIRECT_URI= -PORT= -API_PREFIX= -ALLOWED_ORIGINS= -ACCESS_JWT_SECRET= -REFRESH_JWT_SECRET= -ACCESS_JWT_EXPIRES_IN= -REFRESH_JWT_EXPIRES_IN= -ACCESS_JWT_ALGORITHM= -REFRESH_JWT_ALGORITHM= diff --git a/README.md b/README.md index 0bb3831..e4c8913 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ _Link to the frontend repo:_ https://github.com/chertik77/TaskPro-frontend ## Languages and Tools -![Languages and Tools](https://skills.syvixor.com/api/icons?i=nodejs,ts,expressjs,prisma,mongodb,swagger,cloudinary,redis,zod,betterauth,resend,githubactions,yarn,commitlint,eslint,prettier,postman,vscode&perline=10) +![Languages and Tools](https://skills.syvixor.com/api/icons?i=nodejs,ts,expressjs,prisma,mongodb,swagger,cloudinary,redis,zod,betterauth,resend,githubactions,yarn,commitlint,eslint,prettier,postman,vscode&perline=9) diff --git a/app/middlewares/require-auth.ts b/app/middlewares/require-auth.ts index cbd38d3..6b4e038 100644 --- a/app/middlewares/require-auth.ts +++ b/app/middlewares/require-auth.ts @@ -9,18 +9,14 @@ export const requireAuth = async ( _: Response, next: NextFunction ) => { - try { - const session = await auth.api.getSession({ - headers: fromNodeHeaders(req.headers) - }) + const session = await auth.api.getSession({ + headers: fromNodeHeaders(req.headers) + }) - if (!session) return next(Unauthorized()) + if (!session) return next(Unauthorized()) - req.user = session.user - req.session = session.session + req.user = session.user + req.session = session.session - next() - } catch { - return next(Unauthorized()) - } + next() } diff --git a/prisma/models/passkey.prisma b/prisma/models/passkey.prisma deleted file mode 100644 index 8861333..0000000 --- a/prisma/models/passkey.prisma +++ /dev/null @@ -1,20 +0,0 @@ -model Passkey { - id String @id @default(auto()) @map("_id") @db.ObjectId - name String? - counter Int - transports String? - aaguid String? - deviceType String @map("device_type") - backedUp Boolean @map("backed_up") - publicKey String @map("public_key") - credentialID String @map("credential_id") - - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime? @map("created_at") - - @@index([userId]) - @@index([credentialID]) - @@map("passkeys") -}