From fe475db2cdefa9176f9bed713414b3c5256f0913 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Mon, 16 Dec 2024 11:42:31 +0700 Subject: [PATCH 01/13] feat: change formatter --- .prettierignore | 1 + .prettierrc | 8 ++++++++ biome.json | 36 ------------------------------------ eslintrc.js | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 biome.json create mode 100644 eslintrc.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1d93637 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +drizzle \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..af218d8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "arrowParens": "always", + "semi": true, + "tabWidth": 2, + "trailingComma": "all", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/biome.json b/biome.json deleted file mode 100644 index f421905..0000000 --- a/biome.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": [] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "indentWidth": 2 - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "indentStyle": "tab", - "indentWidth": 2, - "arrowParentheses": "always", - "semicolons": "always", - "trailingCommas": "all" - } - } -} diff --git a/eslintrc.js b/eslintrc.js new file mode 100644 index 0000000..4c2a6bc --- /dev/null +++ b/eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + extends: ['plugin:prettier/recommended'], + ignorePatterns: ['.eslintrc.js', 'node_modules/', 'drizzle/'], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, + }, + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/consistent-type-imports': 'off', + }, +}; From f4ae1e18170975b407ba9e5e338d1df3f4083010 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Mon, 16 Dec 2024 14:40:35 +0700 Subject: [PATCH 02/13] fix: prettier & eslint config --- .eslintrc.js | 33 +++++++++++++++++++++++++++++++++ bun.lockb | Bin 80064 -> 185077 bytes package.json | 20 ++++++++++++++------ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 .eslintrc.js mode change 100644 => 100755 bun.lockb diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..894563e --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + env: { + browser: true, + node: true, + es2021: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + ignorePatterns: ['.eslintrc.js', 'node_modules/', 'drizzle/'], + overrides: [ + { + files: ['.eslintrc.{js,cjs}'], + env: { + node: true, + }, + parserOptions: { + sourceType: 'script', + }, + }, + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'no-console': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'prettier/prettier': 'error', + }, +}; \ No newline at end of file diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index c8e8e18fa9829d8508ec6c2332c05e4e96b6b085..fd9d4d82761c4143f8fe6defd83d3dfeaf71f894 GIT binary patch literal 185077 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

cU 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 T`X#8Sheaj}sQ>zZ`p^FXa9P6l delta 13707 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 z Date: Mon, 16 Dec 2024 14:41:01 +0700 Subject: [PATCH 03/13] style: formatting --- eslintrc.js | 26 - src/configs/env.config.ts | 50 +- src/controllers/auth.controller.ts | 536 ++++++++++----------- src/controllers/competition.controller.ts | 72 +-- src/controllers/health.controller.ts | 12 +- src/controllers/media.controller.ts | 23 +- src/controllers/team-member.controller.ts | 116 ++--- src/controllers/user.controller.ts | 40 +- src/db/helper.ts | 7 +- src/db/migrate-trigger.ts | 60 +-- src/db/schema/auth.schema.ts | 54 +-- src/db/schema/competition.schema.ts | 177 ++++--- src/db/schema/media.schema.ts | 42 +- src/db/schema/team-member.schema.ts | 100 ++-- src/db/schema/team.schema.ts | 48 +- src/db/schema/user.schema.ts | 66 +-- src/index.ts | 65 ++- src/lib/nodemailer.ts | 34 +- src/lib/s3.ts | 18 +- src/middlewares/role-access.middleware.ts | 20 +- src/repositories/auth.repository.ts | 58 +-- src/repositories/competition.repository.ts | 72 +-- src/repositories/media.repository.ts | 27 +- src/repositories/team-member.repository.ts | 184 +++---- src/repositories/team.repository.ts | 1 - src/repositories/user.repository.ts | 22 +- src/routes/auth.route.ts | 313 ++++++------ src/routes/competition.route.ts | 96 ++-- src/routes/health.route.ts | 36 +- src/routes/media.route.ts | 42 +- src/routes/team-member.route.ts | 143 +++--- src/routes/team.route.ts | 340 ++++++------- src/routes/user.route.ts | 88 ++-- src/types/auth.type.ts | 116 ++--- src/types/competition.type.ts | 20 +- src/types/media.type.ts | 46 +- src/types/responses.type.ts | 20 +- src/types/team-member.type.ts | 92 ++-- src/types/team.type.ts | 38 +- src/types/user.type.ts | 14 +- src/utils/drizzle-schema-util.ts | 2 +- src/utils/error-response-factory.ts | 32 +- src/utils/router-factory.ts | 55 ++- 43 files changed, 1684 insertions(+), 1739 deletions(-) delete mode 100644 eslintrc.js diff --git a/eslintrc.js b/eslintrc.js deleted file mode 100644 index 4c2a6bc..0000000 --- a/eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = { - extends: ['plugin:prettier/recommended'], - ignorePatterns: ['.eslintrc.js', 'node_modules/', 'drizzle/'], - overrides: [ - { - env: { - node: true, - }, - files: ['.eslintrc.{js,cjs}'], - parserOptions: { - sourceType: 'script', - }, - }, - ], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - rules: { - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/strict-boolean-expressions': 'off', - '@typescript-eslint/consistent-type-imports': 'off', - }, -}; diff --git a/src/configs/env.config.ts b/src/configs/env.config.ts index 05ea1f8..240904b 100644 --- a/src/configs/env.config.ts +++ b/src/configs/env.config.ts @@ -1,35 +1,35 @@ import { z } from 'zod'; const EnvSchema = z.object({ - PORT: z.coerce.number().default(5000), - DATABASE_URL: z.string().url(), - ALLOWED_ORIGINS: z - .string() - .default('["http://localhost:5173"]') - .transform((value) => JSON.parse(value)) - .pipe(z.array(z.string().url())), - ACCESS_TOKEN_SECRET: z.string(), - ACCESS_TOKEN_EXPIRATION: z.coerce.number(), - REFRESH_TOKEN_SECRET: z.string(), - REFRESH_TOKEN_EXPIRATION: z.coerce.number(), - SMTP_HOST: z.string(), - SMTP_USER: z.string(), - SMTP_PASSWORD: z.string(), - SMTP_PORT: z.coerce.number().default(465), - SMTP_SECURE: z.coerce.boolean().default(true), - GOOGLE_CLIENT_ID: z.string(), - GOOGLE_CLIENT_SECRET: z.string(), - GOOGLE_CALLBACK_URL: z.string(), - S3_ENDPOINT: z.string(), - S3_ACCESS_KEY_ID: z.string(), - S3_SECRET_ACCESS_KEY: z.string(), + PORT: z.coerce.number().default(5000), + DATABASE_URL: z.string().url(), + ALLOWED_ORIGINS: z + .string() + .default('["http://localhost:5173"]') + .transform((value) => JSON.parse(value)) + .pipe(z.array(z.string().url())), + ACCESS_TOKEN_SECRET: z.string(), + ACCESS_TOKEN_EXPIRATION: z.coerce.number(), + REFRESH_TOKEN_SECRET: z.string(), + REFRESH_TOKEN_EXPIRATION: z.coerce.number(), + SMTP_HOST: z.string(), + SMTP_USER: z.string(), + SMTP_PASSWORD: z.string(), + SMTP_PORT: z.coerce.number().default(465), + SMTP_SECURE: z.coerce.boolean().default(true), + GOOGLE_CLIENT_ID: z.string(), + GOOGLE_CLIENT_SECRET: z.string(), + GOOGLE_CALLBACK_URL: z.string(), + S3_ENDPOINT: z.string(), + S3_ACCESS_KEY_ID: z.string(), + S3_SECRET_ACCESS_KEY: z.string(), }); const result = EnvSchema.safeParse(process.env); if (!result.success) { - console.error('Invalid environment variables: '); - console.error(result.error.flatten().fieldErrors); - process.exit(1); + console.error('Invalid environment variables: '); + console.error(result.error.flatten().fieldErrors); + process.exit(1); } export const env = result.data; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index e62f7a1..604d2f9 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -7,26 +7,26 @@ import type { UserIdentity } from '~/db/schema/auth.schema'; import type { User } from '~/db/schema/user.schema'; import { sendVerificationEmail } from '~/lib/nodemailer'; import { - createUserIdentity, - findUserIdentityByEmail, - findUserIdentityById, - updateUserIdentity, - updateUserVerification, + createUserIdentity, + findUserIdentityByEmail, + findUserIdentityById, + updateUserIdentity, + updateUserVerification, } from '~/repositories/auth.repository'; import { - findUserByEmail, - findUserById, - updateUser, + findUserByEmail, + findUserById, + updateUser, } from '~/repositories/user.repository'; import { - basicLoginRoute, - basicRegisterRoute, - basicVerifyAccountRoute, - googleAuthCallbackRoute, - googleAuthRoute, - logoutRoute, - refreshRoute, - selfRoute, + basicLoginRoute, + basicRegisterRoute, + basicVerifyAccountRoute, + googleAuthCallbackRoute, + googleAuthRoute, + logoutRoute, + refreshRoute, + selfRoute, } from '~/routes/auth.route'; import { GoogleTokenDataSchema, GoogleUserSchema } from '~/types/auth.type'; import { UserSchema } from '~/types/user.type'; @@ -39,290 +39,290 @@ export const authRouter = createRouter(); export const authProtectedRouter = createAuthRouter(); const generateAccessToken = async (user: User, userIdentity: UserIdentity) => { - const payload = { - ...user, - provider: userIdentity.provider, - exp: Math.floor(Date.now() / 1000) + env.ACCESS_TOKEN_EXPIRATION, - }; - const token = await jwt.sign(payload, env.ACCESS_TOKEN_SECRET); - return token; + const payload = { + ...user, + provider: userIdentity.provider, + exp: Math.floor(Date.now() / 1000) + env.ACCESS_TOKEN_EXPIRATION, + }; + const token = await jwt.sign(payload, env.ACCESS_TOKEN_SECRET); + return token; }; const generateRefreshToken = async (user: User) => { - const payload = { - userId: user.id, - exp: Math.floor(Date.now() / 1000) + env.REFRESH_TOKEN_EXPIRATION, - }; - const token = await jwt.sign(payload, env.REFRESH_TOKEN_SECRET); - return token; + const payload = { + userId: user.id, + exp: Math.floor(Date.now() / 1000) + env.REFRESH_TOKEN_EXPIRATION, + }; + const token = await jwt.sign(payload, env.REFRESH_TOKEN_SECRET); + return token; }; const setCookiesToken = async ( - c: Context, - user: User, - userIdentity: UserIdentity, + c: Context, + user: User, + userIdentity: UserIdentity, ) => { - const accessToken = await generateAccessToken(user, userIdentity); - const refreshToken = await generateRefreshToken(user); - - await updateUserIdentity(db, user.id, { - refreshToken, - }); - - setCookie(c, 'khongguan', accessToken, { - path: '/', - secure: true, - httpOnly: true, - maxAge: env.ACCESS_TOKEN_EXPIRATION, - sameSite: 'None', - }); - - setCookie(c, 'saltcheese', refreshToken, { - path: '/', - secure: true, - httpOnly: true, - maxAge: env.REFRESH_TOKEN_EXPIRATION, - sameSite: 'None', - }); - - return { accessToken, refreshToken }; + const accessToken = await generateAccessToken(user, userIdentity); + const refreshToken = await generateRefreshToken(user); + + await updateUserIdentity(db, user.id, { + refreshToken, + }); + + setCookie(c, 'khongguan', accessToken, { + path: '/', + secure: true, + httpOnly: true, + maxAge: env.ACCESS_TOKEN_EXPIRATION, + sameSite: 'None', + }); + + setCookie(c, 'saltcheese', refreshToken, { + path: '/', + secure: true, + httpOnly: true, + maxAge: env.REFRESH_TOKEN_EXPIRATION, + sameSite: 'None', + }); + + return { accessToken, refreshToken }; }; /** BASIC AUTHENTICATION ROUTES (Email & Password) */ authRouter.openapi(basicRegisterRoute, async (c) => { - const { email, password } = c.req.valid('json'); - - const passwordHash = await argon2.hash(password); - const verifyTokenExpiration = new Date( - new Date().getTime() + VERIFICATION_TOKEN_EXPIRATION_TIME, - ); - const verifyToken = await argon2.hash( - `${email}${new Date()}${verifyTokenExpiration.toISOString()}`, - ); - - const user = await findUserIdentityByEmail(db, email); - if (user) { - if ( - !user.isVerified && - new Date() > new Date(user.verificationTokenExpiration) - ) { - // If email already exists and old token expired, regenerate token - // TODO: Maybe add penalty if regenerate token? wait 1 min, 2 min, 10 min, 60 min - await updateUserIdentity(db, user.id, { - verificationToken: verifyToken, - verificationTokenExpiration: verifyTokenExpiration, - }); - } else return c.json({ message: 'User already exist' }, 400); - } - - const newUser = await createUserIdentity(db, { - email: email, - hash: passwordHash, - provider: 'basic', - isVerified: false, - verificationToken: verifyToken, - verificationTokenExpiration: verifyTokenExpiration, - }); - - await sendVerificationEmail(email, verifyToken, newUser.id); - return c.json({}, 204); + const { email, password } = c.req.valid('json'); + + const passwordHash = await argon2.hash(password); + const verifyTokenExpiration = new Date( + new Date().getTime() + VERIFICATION_TOKEN_EXPIRATION_TIME, + ); + const verifyToken = await argon2.hash( + `${email}${new Date()}${verifyTokenExpiration.toISOString()}`, + ); + + const user = await findUserIdentityByEmail(db, email); + if (user) { + if ( + !user.isVerified && + new Date() > new Date(user.verificationTokenExpiration) + ) { + // If email already exists and old token expired, regenerate token + // TODO: Maybe add penalty if regenerate token? wait 1 min, 2 min, 10 min, 60 min + await updateUserIdentity(db, user.id, { + verificationToken: verifyToken, + verificationTokenExpiration: verifyTokenExpiration, + }); + } else return c.json({ message: 'User already exist' }, 400); + } + + const newUser = await createUserIdentity(db, { + email: email, + hash: passwordHash, + provider: 'basic', + isVerified: false, + verificationToken: verifyToken, + verificationTokenExpiration: verifyTokenExpiration, + }); + + await sendVerificationEmail(email, verifyToken, newUser.id); + return c.json({}, 204); }); authRouter.openapi(basicVerifyAccountRoute, async (c) => { - const userIdentity = await findUserIdentityById( - db, - c.req.valid('query').user, - ); - const user = await findUserByEmail(db, userIdentity?.email as string); - - if (!userIdentity || !user) - return c.json({ message: "User doesn't exists" }, 400); - if (new Date() > new Date(userIdentity.verificationTokenExpiration)) - return c.json({ message: 'Token has expired' }, 400); - if (userIdentity.verificationToken !== c.req.valid('query').token) - return c.json({ message: 'Wrong token' }, 400); - - if (!(await updateUserVerification(db, c.req.valid('query').user))) - return c.json({ message: 'Something went wrong' }, 500); - - // Login user - const { accessToken, refreshToken } = await setCookiesToken( - c, - user, - userIdentity, - ); - return c.json( - { - accessToken, - refreshToken, - }, - 200, - ); + const userIdentity = await findUserIdentityById( + db, + c.req.valid('query').user, + ); + const user = await findUserByEmail(db, userIdentity?.email as string); + + if (!userIdentity || !user) + return c.json({ message: "User doesn't exists" }, 400); + if (new Date() > new Date(userIdentity.verificationTokenExpiration)) + return c.json({ message: 'Token has expired' }, 400); + if (userIdentity.verificationToken !== c.req.valid('query').token) + return c.json({ message: 'Wrong token' }, 400); + + if (!(await updateUserVerification(db, c.req.valid('query').user))) + return c.json({ message: 'Something went wrong' }, 500); + + // Login user + const { accessToken, refreshToken } = await setCookiesToken( + c, + user, + userIdentity, + ); + return c.json( + { + accessToken, + refreshToken, + }, + 200, + ); }); authRouter.openapi(basicLoginRoute, async (c) => { - const { email, password } = c.req.valid('json'); - - const userIdentity = await findUserIdentityByEmail(db, email); - const user = await findUserByEmail(db, email); - - if (!userIdentity || !user) - return c.json({ message: 'Email not found' }, 400); - if (!(await argon2.verify(userIdentity.hash, password))) - return c.json({ message: 'Wrong password' }, 400); - if (!userIdentity.isVerified) - return c.json({ message: "User isn't verified" }, 400); - - // Login user - const { accessToken, refreshToken } = await setCookiesToken( - c, - user, - userIdentity, - ); - return c.json( - { - accessToken, - refreshToken, - }, - 200, - ); + const { email, password } = c.req.valid('json'); + + const userIdentity = await findUserIdentityByEmail(db, email); + const user = await findUserByEmail(db, email); + + if (!userIdentity || !user) + return c.json({ message: 'Email not found' }, 400); + if (!(await argon2.verify(userIdentity.hash, password))) + return c.json({ message: 'Wrong password' }, 400); + if (!userIdentity.isVerified) + return c.json({ message: "User isn't verified" }, 400); + + // Login user + const { accessToken, refreshToken } = await setCookiesToken( + c, + user, + userIdentity, + ); + return c.json( + { + accessToken, + refreshToken, + }, + 200, + ); }); /** GOOGLE AUTHENTICATION ROUTES */ authRouter.openapi(googleAuthRoute, async (c) => { - const authorizationUrl = new URL( - 'https://accounts.google.com/o/oauth2/v2/auth', - ); - - authorizationUrl.searchParams.set('client_id', env.GOOGLE_CLIENT_ID || ''); - authorizationUrl.searchParams.set( - 'redirect_uri', - env.GOOGLE_CALLBACK_URL || '', - ); - authorizationUrl.searchParams.set('prompt', 'consent'); - authorizationUrl.searchParams.set('response_type', 'code'); - authorizationUrl.searchParams.set('scope', 'email profile'); - authorizationUrl.searchParams.set('access_type', 'offline'); - - // Redirect the user to Google Login - return c.redirect(authorizationUrl.toString(), 302); + const authorizationUrl = new URL( + 'https://accounts.google.com/o/oauth2/v2/auth', + ); + + authorizationUrl.searchParams.set('client_id', env.GOOGLE_CLIENT_ID || ''); + authorizationUrl.searchParams.set( + 'redirect_uri', + env.GOOGLE_CALLBACK_URL || '', + ); + authorizationUrl.searchParams.set('prompt', 'consent'); + authorizationUrl.searchParams.set('response_type', 'code'); + authorizationUrl.searchParams.set('scope', 'email profile'); + authorizationUrl.searchParams.set('access_type', 'offline'); + + // Redirect the user to Google Login + return c.redirect(authorizationUrl.toString(), 302); }); authRouter.openapi(googleAuthCallbackRoute, async (c) => { - const { code } = c.req.valid('query'); - - const tokenEndpoint = new URL('https://accounts.google.com/o/oauth2/token'); - tokenEndpoint.searchParams.set('code', code); - tokenEndpoint.searchParams.set('grant_type', 'authorization_code'); - - // Make sure you define all of the google env in your .env file - tokenEndpoint.searchParams.set('client_id', env.GOOGLE_CLIENT_ID || ''); - tokenEndpoint.searchParams.set( - 'client_secret', - env.GOOGLE_CLIENT_SECRET || '', - ); - tokenEndpoint.searchParams.set('redirect_uri', env.GOOGLE_CALLBACK_URL || ''); - - // Fetch Token from Google Token endpoint and parse it into GoogleTokenDataSchema - const tokenResponse = await fetch( - tokenEndpoint.origin + tokenEndpoint.pathname, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: tokenEndpoint.searchParams.toString(), - }, - ); - const tokenData = GoogleTokenDataSchema.parse(await tokenResponse.json()); - - // Fetch User Info from Google User Info endpoint and parse it into GoogleUserSchema - const userInfoResponse = await fetch( - 'https://www.googleapis.com/oauth2/v2/userinfo', - { - headers: { - Authorization: `Bearer ${tokenData.access_token}`, - }, - }, - ); - const userInfo = GoogleUserSchema.parse(await userInfoResponse.json()); - const userIdentity = await findUserIdentityByEmail(db, userInfo.email); - if (!userIdentity) { - // If user is not registered, then register it - const googleDataHash = await argon2.hash(JSON.stringify(userInfo)); - const newUser = await createUserIdentity(db, { - email: userInfo.email, - hash: googleDataHash, - provider: 'google', - isVerified: true, - verificationToken: 'google', - verificationTokenExpiration: new Date(), - }); - - await updateUser(db, newUser.id, { fullName: userInfo.name }); - } - - const existingUserIdentity = (await findUserIdentityByEmail( - db, - userInfo.email, - )) as UserIdentity; - const existingUser = (await findUserByEmail(db, userInfo.email)) as User; - - const { accessToken, refreshToken } = await setCookiesToken( - c, - existingUser, - existingUserIdentity, - ); - return c.json( - { - accessToken, - refreshToken, - }, - 200, - ); + const { code } = c.req.valid('query'); + + const tokenEndpoint = new URL('https://accounts.google.com/o/oauth2/token'); + tokenEndpoint.searchParams.set('code', code); + tokenEndpoint.searchParams.set('grant_type', 'authorization_code'); + + // Make sure you define all of the google env in your .env file + tokenEndpoint.searchParams.set('client_id', env.GOOGLE_CLIENT_ID || ''); + tokenEndpoint.searchParams.set( + 'client_secret', + env.GOOGLE_CLIENT_SECRET || '', + ); + tokenEndpoint.searchParams.set('redirect_uri', env.GOOGLE_CALLBACK_URL || ''); + + // Fetch Token from Google Token endpoint and parse it into GoogleTokenDataSchema + const tokenResponse = await fetch( + tokenEndpoint.origin + tokenEndpoint.pathname, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: tokenEndpoint.searchParams.toString(), + }, + ); + const tokenData = GoogleTokenDataSchema.parse(await tokenResponse.json()); + + // Fetch User Info from Google User Info endpoint and parse it into GoogleUserSchema + const userInfoResponse = await fetch( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + }, + }, + ); + const userInfo = GoogleUserSchema.parse(await userInfoResponse.json()); + const userIdentity = await findUserIdentityByEmail(db, userInfo.email); + if (!userIdentity) { + // If user is not registered, then register it + const googleDataHash = await argon2.hash(JSON.stringify(userInfo)); + const newUser = await createUserIdentity(db, { + email: userInfo.email, + hash: googleDataHash, + provider: 'google', + isVerified: true, + verificationToken: 'google', + verificationTokenExpiration: new Date(), + }); + + await updateUser(db, newUser.id, { fullName: userInfo.name }); + } + + const existingUserIdentity = (await findUserIdentityByEmail( + db, + userInfo.email, + )) as UserIdentity; + const existingUser = (await findUserByEmail(db, userInfo.email)) as User; + + const { accessToken, refreshToken } = await setCookiesToken( + c, + existingUser, + existingUserIdentity, + ); + return c.json( + { + accessToken, + refreshToken, + }, + 200, + ); }); /** BOTH AUTH */ authProtectedRouter.openapi(logoutRoute, async (c) => { - deleteCookie(c, 'khongguan'); - deleteCookie(c, 'saltcheese'); - await updateUserIdentity(db, c.var.user.id, { - refreshToken: null, - }); - return c.json({}, 204); + deleteCookie(c, 'khongguan'); + deleteCookie(c, 'saltcheese'); + await updateUserIdentity(db, c.var.user.id, { + refreshToken: null, + }); + return c.json({}, 204); }); authProtectedRouter.openapi(selfRoute, async (c) => { - const user = await UserSchema.parseAsync(c.var.user); - return c.json(user, 200); + const user = await UserSchema.parseAsync(c.var.user); + return c.json(user, 200); }); authRouter.openapi(refreshRoute, async (c) => { - const decoded = await jwt.verify( - c.req.valid('query').token, - env.REFRESH_TOKEN_SECRET, - ); - - const userIdentity = await findUserIdentityById(db, decoded.userId as string); - const user = await findUserById(db, decoded.userId as string); - - if (!userIdentity || !user) return c.json({ message: 'User not found' }, 400); - if (userIdentity.refreshToken !== c.req.valid('query').token) - return c.json({ message: "Token doesn't match!" }, 400); - if (!userIdentity.isVerified) - return c.json({ message: "User isn't verified" }, 400); - - // Login user - const { accessToken, refreshToken } = await setCookiesToken( - c, - user, - userIdentity, - ); - return c.json( - { - accessToken, - refreshToken, - }, - 200, - ); + const decoded = await jwt.verify( + c.req.valid('query').token, + env.REFRESH_TOKEN_SECRET, + ); + + const userIdentity = await findUserIdentityById(db, decoded.userId as string); + const user = await findUserById(db, decoded.userId as string); + + if (!userIdentity || !user) return c.json({ message: 'User not found' }, 400); + if (userIdentity.refreshToken !== c.req.valid('query').token) + return c.json({ message: "Token doesn't match!" }, 400); + if (!userIdentity.isVerified) + return c.json({ message: "User isn't verified" }, 400); + + // Login user + const { accessToken, refreshToken } = await setCookiesToken( + c, + user, + userIdentity, + ); + return c.json( + { + accessToken, + refreshToken, + }, + 200, + ); }); diff --git a/src/controllers/competition.controller.ts b/src/controllers/competition.controller.ts index 417fea6..d228adf 100644 --- a/src/controllers/competition.controller.ts +++ b/src/controllers/competition.controller.ts @@ -2,58 +2,58 @@ import { db } from '~/db/drizzle'; import { createAuthRouter } from '~/utils/router-factory'; import { roleMiddleware } from '~/middlewares/role-access.middleware'; import { - getAnnouncementsByCompetitionId, - getCompetition, - postAnnouncement, + getAnnouncementsByCompetitionId, + getCompetition, + postAnnouncement, } from '~/repositories/competition.repository'; import { - getAdminCompAnnouncementRoute, - postAdminCompAnnouncementRoute, + getAdminCompAnnouncementRoute, + postAdminCompAnnouncementRoute, } from '~/routes/competition.route'; export const competitionProtectedRouter = createAuthRouter(); competitionProtectedRouter.get( - getAdminCompAnnouncementRoute.getRoutingPath(), - roleMiddleware('admin'), + getAdminCompAnnouncementRoute.getRoutingPath(), + roleMiddleware('admin'), ); competitionProtectedRouter.openapi(getAdminCompAnnouncementRoute, async (c) => { - const { competitionId } = c.req.valid('param'); + const { competitionId } = c.req.valid('param'); - // Check if competition exists - const competition = await getCompetition(db, competitionId); - if (!competition) return c.json({ error: "Competition doesn't exist!" }, 400); + // Check if competition exists + const competition = await getCompetition(db, competitionId); + if (!competition) return c.json({ error: "Competition doesn't exist!" }, 400); - const announcements = await getAnnouncementsByCompetitionId( - db, - competitionId, - ); - return c.json(announcements, 200); + const announcements = await getAnnouncementsByCompetitionId( + db, + competitionId, + ); + return c.json(announcements, 200); }); competitionProtectedRouter.post( - postAdminCompAnnouncementRoute.getRoutingPath(), - roleMiddleware('admin'), + postAdminCompAnnouncementRoute.getRoutingPath(), + roleMiddleware('admin'), ); competitionProtectedRouter.openapi( - postAdminCompAnnouncementRoute, - async (c) => { - const { competitionId } = c.req.valid('param'); - const body = c.req.valid('json'); + postAdminCompAnnouncementRoute, + async (c) => { + const { competitionId } = c.req.valid('param'); + const body = c.req.valid('json'); - // Check if competition exists - const competition = await getCompetition(db, competitionId); - if (!competition) - return c.json({ error: "Competition doesn't exist!" }, 400); + // Check if competition exists + const competition = await getCompetition(db, competitionId); + if (!competition) + return c.json({ error: "Competition doesn't exist!" }, 400); - // Create announcement - const user = c.var.user; - const announcement = await postAnnouncement( - db, - competitionId, - user.id, - body, - ); - return c.json(announcement, 200); - }, + // Create announcement + const user = c.var.user; + const announcement = await postAnnouncement( + db, + competitionId, + user.id, + body, + ); + return c.json(announcement, 200); + }, ); diff --git a/src/controllers/health.controller.ts b/src/controllers/health.controller.ts index 500c1c5..4661d77 100644 --- a/src/controllers/health.controller.ts +++ b/src/controllers/health.controller.ts @@ -4,10 +4,10 @@ import { createRouter } from '../utils/router-factory'; export const healthRouter = createRouter(); healthRouter.openapi(getHealthStatusRoute, async (c) => { - return c.json( - { - message: 'API is running sucesfully!', - }, - 200, - ); + return c.json( + { + message: 'API is running sucesfully!', + }, + 200, + ); }); diff --git a/src/controllers/media.controller.ts b/src/controllers/media.controller.ts index b7a3d7c..324e0dc 100644 --- a/src/controllers/media.controller.ts +++ b/src/controllers/media.controller.ts @@ -1,6 +1,5 @@ import { createId } from '@paralleldrive/cuid2'; import { env } from '~/configs/env.config'; -import { media } from '~/db/schema/media.schema'; import { createPutObjectPresignedUrl } from '~/lib/s3'; import { getPresignedLink } from '~/routes/media.route'; import { createAuthRouter } from '~/utils/router-factory'; @@ -8,16 +7,16 @@ import { createAuthRouter } from '~/utils/router-factory'; export const mediaRouter = createAuthRouter(); mediaRouter.openapi(getPresignedLink, async (c) => { - const { filename, bucket } = c.req.valid('query'); - const key = `${createId()}-${filename}`; + const { filename, bucket } = c.req.valid('query'); + const key = `${createId()}-${filename}`; - const expiresIn = 60; - return c.json( - { - presignedUrl: await createPutObjectPresignedUrl(key, bucket, expiresIn), - mediaUrl: `${env.S3_ENDPOINT}/${bucket}/${key}`, - expiresIn, - }, - 200, - ); + const expiresIn = 60; + return c.json( + { + presignedUrl: await createPutObjectPresignedUrl(key, bucket, expiresIn), + mediaUrl: `${env.S3_ENDPOINT}/${bucket}/${key}`, + expiresIn, + }, + 200, + ); }); diff --git a/src/controllers/team-member.controller.ts b/src/controllers/team-member.controller.ts index 8e27aeb..47caa96 100644 --- a/src/controllers/team-member.controller.ts +++ b/src/controllers/team-member.controller.ts @@ -1,90 +1,90 @@ import { db } from '~/db/drizzle'; import { roleMiddleware } from '~/middlewares/role-access.middleware'; import { - getTeamMemberById, - updateTeamMemberDocument, - updateTeamMemberVerification, + getTeamMemberById, + updateTeamMemberDocument, + updateTeamMemberVerification, } from '~/repositories/team-member.repository'; import { getTeamById } from '~/repositories/team.repository'; import { - getTeamMemberRoute, - postTeamMemberDocumentRoute, - postTeamMemberVerificationRoute, + getTeamMemberRoute, + postTeamMemberDocumentRoute, + postTeamMemberVerificationRoute, } from '~/routes/team-member.route'; import { createAuthRouter } from '~/utils/router-factory'; export const teamMemberProtectedRouter = createAuthRouter(); teamMemberProtectedRouter.openapi(getTeamMemberRoute, async (c) => { - return c.json( - await getTeamMemberById(db, c.req.valid('param').teamId, c.var.user.id, { - nisn: true, - user: true, - poster: true, - twibbon: true, - kartu: true, - }), - 200, - ); + return c.json( + await getTeamMemberById(db, c.req.valid('param').teamId, c.var.user.id, { + nisn: true, + user: true, + poster: true, + twibbon: true, + kartu: true, + }), + 200, + ); }); teamMemberProtectedRouter.openapi(postTeamMemberDocumentRoute, async (c) => { - const { teamId } = c.req.valid('param'); + const { teamId } = c.req.valid('param'); - // Check if team exists - const team = await getTeamById(db, teamId, { teamMember: true }); - if (!team) return c.json({ error: "Team doesn't exist!" }, 400); + // Check if team exists + const team = await getTeamById(db, teamId, { teamMember: true }); + if (!team) return c.json({ error: "Team doesn't exist!" }, 400); - // Check if user is in team - const user = c.var.user; - const teamMember = team.teamMembers.find((el) => el.userId === user.id); - console.log(team); - console.log(user.id, teamMember); - console.log(user); - if (!teamMember) return c.json({ error: "User isn't inside team!" }, 403); + // Check if user is in team + const user = c.var.user; + const teamMember = team.teamMembers.find((el) => el.userId === user.id); + console.log(team); + console.log(user.id, teamMember); + console.log(user); + if (!teamMember) return c.json({ error: "User isn't inside team!" }, 403); - // Check if user member hasn't been verified yet - if (teamMember.isVerified) - return c.json({ error: 'You are already verified!' }, 403); + // Check if user member hasn't been verified yet + if (teamMember.isVerified) + return c.json({ error: 'You are already verified!' }, 403); - const updatedTeamMember = await updateTeamMemberDocument( - db, - teamId, - user.id, - c.req.valid('json'), - ); + const updatedTeamMember = await updateTeamMemberDocument( + db, + teamId, + user.id, + c.req.valid('json'), + ); - return c.json(updatedTeamMember, 200); + return c.json(updatedTeamMember, 200); }); teamMemberProtectedRouter.post( - postTeamMemberVerificationRoute.getRoutingPath(), - roleMiddleware('admin'), + postTeamMemberVerificationRoute.getRoutingPath(), + roleMiddleware('admin'), ); teamMemberProtectedRouter.openapi( - postTeamMemberVerificationRoute, - async (c) => { - const { competitionId, teamId, userId } = c.req.valid('param'); + postTeamMemberVerificationRoute, + async (c) => { + const { competitionId, teamId, userId } = c.req.valid('param'); - const team = await getTeamById(db, teamId, { - competition: true, - teamMember: true, - }); - if (!team) return c.json({ error: "Team doesn't exist!" }, 400); + const team = await getTeamById(db, teamId, { + competition: true, + teamMember: true, + }); + if (!team) return c.json({ error: "Team doesn't exist!" }, 400); - if (team.competition.id !== competitionId) - return c.json({ error: "Team and competition don't match!" }, 400); + if (team.competition.id !== competitionId) + return c.json({ error: "Team and competition don't match!" }, 400); - if (!team.teamMembers.find((el) => el.userId === userId)) - return c.json({ error: "User isn't inside team!" }, 403); + if (!team.teamMembers.find((el) => el.userId === userId)) + return c.json({ error: "User isn't inside team!" }, 403); - const body = c.req.valid('json'); + const body = c.req.valid('json'); - await updateTeamMemberVerification(db, teamId, userId, body); + await updateTeamMemberVerification(db, teamId, userId, body); - return c.json( - { message: 'Successfully updated document verification!' }, - 200, - ); - }, + return c.json( + { message: 'Successfully updated document verification!' }, + 200, + ); + }, ); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 8eaf880..cf0ff04 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -6,29 +6,29 @@ import { createAuthRouter } from '~/utils/router-factory'; export const userProtectedRouter = createAuthRouter(); userProtectedRouter.openapi(getUserRoute, async (c) => { - const user = c.var.user; - const findUser = await findUserById(db, user.id); - if (!findUser) return c.json({ message: 'User not found!' }, 400); - return c.json(findUser, 200); + const user = c.var.user; + const findUser = await findUserById(db, user.id); + if (!findUser) return c.json({ message: 'User not found!' }, 400); + return c.json(findUser, 200); }); userProtectedRouter.openapi(updateUserRoute, async (c) => { - const body = c.req.valid('json'); - const user = await findUserById(db, c.var.user.id); - - if (!user) return c.json({ message: 'User not found!' }, 400); - // Kalau udah 'isRegistrationComplete' consent gak boleh diubah - if (user.isRegistrationComplete && typeof body.consent === 'boolean') - return c.json({ message: 'You cannot change consent.' }, 400); + const body = c.req.valid('json'); + const user = await findUserById(db, c.var.user.id); - const values = { - ...body, - isRegistrationComplete: !user.isRegistrationComplete - ? true - : user.isRegistrationComplete, - consent: !user.isRegistrationComplete ? body.consent : user.consent, - }; + if (!user) return c.json({ message: 'User not found!' }, 400); + // Kalau udah 'isRegistrationComplete' consent gak boleh diubah + if (user.isRegistrationComplete && typeof body.consent === 'boolean') + return c.json({ message: 'You cannot change consent.' }, 400); - const updatedUser = await updateUser(db, user.id, values); - return c.json(updatedUser, 200); + const values = { + ...body, + isRegistrationComplete: !user.isRegistrationComplete + ? true + : user.isRegistrationComplete, + consent: !user.isRegistrationComplete ? body.consent : user.consent, + }; + + const updatedUser = await updateUser(db, user.id, values); + return c.json(updatedUser, 200); }); diff --git a/src/db/helper.ts b/src/db/helper.ts index eaa9a56..428b033 100644 --- a/src/db/helper.ts +++ b/src/db/helper.ts @@ -1,10 +1,7 @@ -import { InferSelectModel } from 'drizzle-orm'; -import { z } from 'zod'; - export function first(items: T[]): T | undefined { - return items[0]; + return items[0]; } export function firstSure(items: T[]): T { - return items[0]; + return items[0]; } diff --git a/src/db/migrate-trigger.ts b/src/db/migrate-trigger.ts index ea6b1fa..a3c5d63 100644 --- a/src/db/migrate-trigger.ts +++ b/src/db/migrate-trigger.ts @@ -3,44 +3,44 @@ import postgres from 'postgres'; import path from 'node:path'; if (!process.env.DATABASE_URL) { - throw new Error('DATABASE_URL is required'); + throw new Error('DATABASE_URL is required'); } const sql = postgres(process.env.DATABASE_URL); async function runGenerateIdentityTrigger() { - const migrationPath = path.join( - import.meta.dir, - '../../drizzle/generate_identity_trigger.sql', - ); + const migrationPath = path.join( + import.meta.dir, + '../../drizzle/generate_identity_trigger.sql', + ); - const migrationFile = Bun.file(migrationPath); - if (!(await migrationFile.exists())) - throw new Error( - 'Ensure there is the SQL migration file in drizzle/generate_identity_trigger.sql', - ); + const migrationFile = Bun.file(migrationPath); + if (!(await migrationFile.exists())) + throw new Error( + 'Ensure there is the SQL migration file in drizzle/generate_identity_trigger.sql', + ); - const migration = await migrationFile.text(); - try { - await sql.unsafe(migration); - console.log('\nTrigger and function created successfully.'); - } catch (err) { - if (err instanceof postgres.PostgresError && err.code === '42723') { - console.log('\nTrigger and function already exists!'); - process.exit(0); - } - console.error('Failed to execute migration:', err); - throw err; - } + const migration = await migrationFile.text(); + try { + await sql.unsafe(migration); + console.log('\nTrigger and function created successfully.'); + } catch (err) { + if (err instanceof postgres.PostgresError && err.code === '42723') { + console.log('\nTrigger and function already exists!'); + process.exit(0); + } + console.error('Failed to execute migration:', err); + throw err; + } } if (require.main === module) { - (async () => { - try { - await runGenerateIdentityTrigger(); - } catch (err) { - console.error('Migration failed:', err); - process.exit(1); - } - })(); + (async () => { + try { + await runGenerateIdentityTrigger(); + } catch (err) { + console.error('Migration failed:', err); + process.exit(1); + } + })(); } diff --git a/src/db/schema/auth.schema.ts b/src/db/schema/auth.schema.ts index a680bf5..ec39949 100644 --- a/src/db/schema/auth.schema.ts +++ b/src/db/schema/auth.schema.ts @@ -1,52 +1,52 @@ import { - type InferInsertModel, - type InferSelectModel, - relations, + type InferInsertModel, + type InferSelectModel, + relations, } from 'drizzle-orm'; import { boolean, pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; import { createId, getNow } from '../../utils/drizzle-schema-util'; import { user } from './user.schema'; export const userIdentityProviderEnum = pgEnum('user_identity_provider_enum', [ - 'google', - 'basic', + 'google', + 'basic', ]); export const userIdentityRoleEnum = pgEnum('user_identity_role_enum', [ - 'admin', - 'user', + 'admin', + 'user', ]); export const userIdentity = pgTable('user_identity', { - id: text('id').primaryKey().$defaultFn(createId), - email: text('email').unique().notNull(), - provider: userIdentityProviderEnum('provider').notNull(), - hash: text('hash').notNull(), + id: text('id').primaryKey().$defaultFn(createId), + email: text('email').unique().notNull(), + provider: userIdentityProviderEnum('provider').notNull(), + hash: text('hash').notNull(), - isVerified: boolean('is_verified').default(false).notNull(), - verificationToken: text('verification_token').notNull(), - verificationTokenExpiration: timestamp( - 'verification_token_expiration', - ).notNull(), + isVerified: boolean('is_verified').default(false).notNull(), + verificationToken: text('verification_token').notNull(), + verificationTokenExpiration: timestamp( + 'verification_token_expiration', + ).notNull(), - passwordRecoveryToken: text('password_recovery_token'), - passwordRecoveryTokenExpiration: timestamp( - 'password_recovery_token_expiration', - ), + passwordRecoveryToken: text('password_recovery_token'), + passwordRecoveryTokenExpiration: timestamp( + 'password_recovery_token_expiration', + ), - refreshToken: text('refresh_token'), + refreshToken: text('refresh_token'), - role: userIdentityRoleEnum('role').default('user').notNull(), + role: userIdentityRoleEnum('role').default('user').notNull(), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').$onUpdate(getNow), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').$onUpdate(getNow), }); export const userIdentityRelations = relations(userIdentity, ({ one }) => ({ - user: one(user), + user: one(user), })); export type UserIdentity = InferSelectModel; export type UserIdentityInsert = InferInsertModel; -export type UserIdentityRolesEnum = (typeof userIdentityRoleEnum.enumValues)[number]; - +export type UserIdentityRolesEnum = + (typeof userIdentityRoleEnum.enumValues)[number]; diff --git a/src/db/schema/competition.schema.ts b/src/db/schema/competition.schema.ts index 610e11c..7436dd7 100644 --- a/src/db/schema/competition.schema.ts +++ b/src/db/schema/competition.schema.ts @@ -1,13 +1,12 @@ -import { type InferSelectModel, relations, sql } from 'drizzle-orm'; +import { relations } from 'drizzle-orm'; import { - boolean, - date, - integer, - pgEnum, - pgTable, - primaryKey, - text, - timestamp, + boolean, + integer, + pgEnum, + pgTable, + primaryKey, + text, + timestamp, } from 'drizzle-orm/pg-core'; import { createId, getNow } from '../../utils/drizzle-schema-util'; import { team } from './team.schema'; @@ -16,111 +15,111 @@ import { media } from './media.schema'; /** Main Compeitition Table */ export const competition = pgTable('competition', { - id: text('id').primaryKey().$defaultFn(createId), - title: text('title').notNull().notNull(), - description: text('description').notNull(), - maxParticipants: integer('max_participants').notNull(), - maxTeamMember: integer('max_team_member').notNull(), - guidebookUrl: text('guide_book_url'), + id: text('id').primaryKey().$defaultFn(createId), + title: text('title').notNull().notNull(), + description: text('description').notNull(), + maxParticipants: integer('max_participants').notNull(), + maxTeamMember: integer('max_team_member').notNull(), + guidebookUrl: text('guide_book_url'), }); export const competitionRelations = relations(competition, ({ many }) => ({ - team: many(team), - announcement: many(competitionAnnouncement), - submission: many(competitionSubmission), - timeline: many(competitionTimeline), + team: many(team), + announcement: many(competitionAnnouncement), + submission: many(competitionSubmission), + timeline: many(competitionTimeline), })); /** Competition Announcements Table */ export const competitionAnnouncement = pgTable('competition_announcement', { - id: text('id').primaryKey().$defaultFn(createId), - competitionId: text('competition_id') - .notNull() - .references(() => competition.id), - authorId: text('author_id') - .notNull() - .references(() => user.id), - title: text('title').notNull().notNull(), - description: text('description').notNull(), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').$onUpdate(getNow), + id: text('id').primaryKey().$defaultFn(createId), + competitionId: text('competition_id') + .notNull() + .references(() => competition.id), + authorId: text('author_id') + .notNull() + .references(() => user.id), + title: text('title').notNull().notNull(), + description: text('description').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').$onUpdate(getNow), }); export const competitionAnnouncementRelations = relations( - competitionAnnouncement, - ({ one }) => ({ - competition: one(competition, { - fields: [competitionAnnouncement.competitionId], - references: [competition.id], - }), - author: one(user, { - fields: [competitionAnnouncement.authorId], - references: [user.id], - }), - }), + competitionAnnouncement, + ({ one }) => ({ + competition: one(competition, { + fields: [competitionAnnouncement.competitionId], + references: [competition.id], + }), + author: one(user, { + fields: [competitionAnnouncement.authorId], + references: [user.id], + }), + }), ); /** Competition Submissions Table */ export const competitionSubmissionTypeEnum = pgEnum( - 'competition_submission_type_enum', - ['uiux_poster'], + 'competition_submission_type_enum', + ['uiux_poster'], ); export const competitionSubmission = pgTable( - 'competition_submission', - { - teamId: text('team_id') - .notNull() - .references(() => team.id), - competitionId: text('competition_id') - .notNull() - .references(() => competition.id), - type: competitionSubmissionTypeEnum('type').notNull(), - mediaId: text('media_id').notNull(), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').$onUpdate(getNow), - }, - (t) => ({ - pk: primaryKey(t.teamId, t.type), - }), + 'competition_submission', + { + teamId: text('team_id') + .notNull() + .references(() => team.id), + competitionId: text('competition_id') + .notNull() + .references(() => competition.id), + type: competitionSubmissionTypeEnum('type').notNull(), + mediaId: text('media_id').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').$onUpdate(getNow), + }, + (t) => ({ + pk: primaryKey(t.teamId, t.type), + }), ); export const competitionSubmissionRelations = relations( - competitionSubmission, - ({ one }) => ({ - competition: one(competition, { - fields: [competitionSubmission.competitionId], - references: [competition.id], - }), - team: one(team, { - fields: [competitionSubmission.teamId], - references: [team.id], - }), - file: one(media, { - fields: [competitionSubmission.mediaId], - references: [media.id], - }), - }), + competitionSubmission, + ({ one }) => ({ + competition: one(competition, { + fields: [competitionSubmission.competitionId], + references: [competition.id], + }), + team: one(team, { + fields: [competitionSubmission.teamId], + references: [team.id], + }), + file: one(media, { + fields: [competitionSubmission.mediaId], + references: [media.id], + }), + }), ); /** Competition Timeline Table */ export const competitionTimeline = pgTable('competition_timeline', { - id: text('id').primaryKey().$defaultFn(createId), - competitionId: text('competition_id') - .notNull() - .references(() => competition.id), - title: text('title').notNull().notNull(), - date: timestamp('date').notNull(), - showOnLanding: boolean('show_on_landing').notNull().default(false), - showTime: boolean('show_tile').notNull().default(false), + id: text('id').primaryKey().$defaultFn(createId), + competitionId: text('competition_id') + .notNull() + .references(() => competition.id), + title: text('title').notNull().notNull(), + date: timestamp('date').notNull(), + showOnLanding: boolean('show_on_landing').notNull().default(false), + showTime: boolean('show_tile').notNull().default(false), }); export const competitionTimelineRelations = relations( - competitionTimeline, - ({ one }) => ({ - competition: one(competition, { - fields: [competitionTimeline.competitionId], - references: [competition.id], - }), - }), + competitionTimeline, + ({ one }) => ({ + competition: one(competition, { + fields: [competitionTimeline.competitionId], + references: [competition.id], + }), + }), ); diff --git a/src/db/schema/media.schema.ts b/src/db/schema/media.schema.ts index dafbf9b..4d23ce9 100644 --- a/src/db/schema/media.schema.ts +++ b/src/db/schema/media.schema.ts @@ -1,35 +1,21 @@ -import { type InferSelectModel, relations, sql } from 'drizzle-orm'; -import { - type AnyPgColumn, - boolean, - date, - index, - integer, - json, - pgEnum, - pgTable, - primaryKey, - text, - timestamp, - unique, -} from 'drizzle-orm/pg-core'; -import { createId, getNow } from '../../utils/drizzle-schema-util'; +import { pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; +import { createId } from '../../utils/drizzle-schema-util'; import { user } from './user.schema'; export const mediaBucketEnum = pgEnum('media_bucket_enum', [ - 'competition-registration', + 'competition-registration', ]); export const media = pgTable('media', { - id: text('id').primaryKey().$defaultFn(createId), - creatorId: text('creator_id') - .references(() => user.id, { onDelete: 'cascade' }) - .notNull(), - name: text('name').unique().notNull(), - bucket: text('bucket').notNull(), - type: text('type').notNull(), - url: text('url').notNull(), - createdAt: timestamp('created_at', { withTimezone: true }) - .notNull() - .defaultNow(), + id: text('id').primaryKey().$defaultFn(createId), + creatorId: text('creator_id') + .references(() => user.id, { onDelete: 'cascade' }) + .notNull(), + name: text('name').unique().notNull(), + bucket: text('bucket').notNull(), + type: text('type').notNull(), + url: text('url').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), }); diff --git a/src/db/schema/team-member.schema.ts b/src/db/schema/team-member.schema.ts index 4c54aeb..5e971d2 100644 --- a/src/db/schema/team-member.schema.ts +++ b/src/db/schema/team-member.schema.ts @@ -1,65 +1,63 @@ import { relations } from 'drizzle-orm'; -import { boolean, pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; -import { createId, getNow } from '../../utils/drizzle-schema-util'; -import { competition } from './competition.schema'; +import { boolean, pgEnum, pgTable, text } from 'drizzle-orm/pg-core'; import { media } from './media.schema'; import { team } from './team.schema'; import { user } from './user.schema'; export const teamMemberRoleEnum = pgEnum('team_member_role_renum', [ - 'leader', - 'member', + 'leader', + 'member', ]); export const teamMember = pgTable('team_member', { - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - teamId: text('team_id') - .notNull() - .references(() => team.id, { onDelete: 'cascade' }), - role: teamMemberRoleEnum('role').notNull(), - nisnMediaId: text('nisn_media_id').references(() => media.id, { - onDelete: 'cascade', - }), - kartuMediaId: text('kartu_media_id').references(() => media.id, { - // bisa KTM or Kartu Pelajar - onDelete: 'cascade', - }), - posterMediaId: text('poster_media_id').references(() => media.id, { - onDelete: 'cascade', - }), - twibbonMediaId: text('twibbon_media_id').references(() => media.id, { - onDelete: 'cascade', - }), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + teamId: text('team_id') + .notNull() + .references(() => team.id, { onDelete: 'cascade' }), + role: teamMemberRoleEnum('role').notNull(), + nisnMediaId: text('nisn_media_id').references(() => media.id, { + onDelete: 'cascade', + }), + kartuMediaId: text('kartu_media_id').references(() => media.id, { + // bisa KTM or Kartu Pelajar + onDelete: 'cascade', + }), + posterMediaId: text('poster_media_id').references(() => media.id, { + onDelete: 'cascade', + }), + twibbonMediaId: text('twibbon_media_id').references(() => media.id, { + onDelete: 'cascade', + }), - isVerified: boolean('is_verified').default(false).notNull(), - verificationError: text('verification_error'), + isVerified: boolean('is_verified').default(false).notNull(), + verificationError: text('verification_error'), }); export const teamMemberRelations = relations(teamMember, ({ one }) => ({ - user: one(user, { - fields: [teamMember.userId], - references: [user.id], - }), - team: one(team, { - fields: [teamMember.teamId], - references: [team.id], - }), - nisn: one(media, { - fields: [teamMember.nisnMediaId], - references: [media.id], - }), - kartu: one(media, { - fields: [teamMember.kartuMediaId], - references: [media.id], - }), - poster: one(media, { - fields: [teamMember.posterMediaId], - references: [media.id], - }), - twibbon: one(media, { - fields: [teamMember.twibbonMediaId], - references: [media.id], - }), + user: one(user, { + fields: [teamMember.userId], + references: [user.id], + }), + team: one(team, { + fields: [teamMember.teamId], + references: [team.id], + }), + nisn: one(media, { + fields: [teamMember.nisnMediaId], + references: [media.id], + }), + kartu: one(media, { + fields: [teamMember.kartuMediaId], + references: [media.id], + }), + poster: one(media, { + fields: [teamMember.posterMediaId], + references: [media.id], + }), + twibbon: one(media, { + fields: [teamMember.twibbonMediaId], + references: [media.id], + }), })); diff --git a/src/db/schema/team.schema.ts b/src/db/schema/team.schema.ts index 3fbb428..ecaad86 100644 --- a/src/db/schema/team.schema.ts +++ b/src/db/schema/team.schema.ts @@ -6,33 +6,33 @@ import { media } from './media.schema'; import { teamMember } from './team-member.schema'; export const team = pgTable('team', { - id: text('id').primaryKey().$defaultFn(createId), - competitionId: text('competition_id') - .notNull() - .references(() => competition.id, { onDelete: 'cascade' }), // Add reference to competition - name: text('team_name').notNull(), - joinCode: text('team_code').notNull().$defaultFn(createId).unique(), // Add unique constraint - paymentProofMediaId: text('payment_proof_media_id').references( - () => media.id, - { onDelete: 'cascade' }, - ), // Picture of payment proof + id: text('id').primaryKey().$defaultFn(createId), + competitionId: text('competition_id') + .notNull() + .references(() => competition.id, { onDelete: 'cascade' }), // Add reference to competition + name: text('team_name').notNull(), + joinCode: text('team_code').notNull().$defaultFn(createId).unique(), // Add unique constraint + paymentProofMediaId: text('payment_proof_media_id').references( + () => media.id, + { onDelete: 'cascade' }, + ), // Picture of payment proof - isVerified: boolean('is_verified').default(false).notNull(), - verificationError: text('verification_error'), + isVerified: boolean('is_verified').default(false).notNull(), + verificationError: text('verification_error'), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').$onUpdate(getNow), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').$onUpdate(getNow), }); export const teamRelations = relations(team, ({ one, many }) => ({ - teamMembers: many(teamMember), - competition: one(competition, { - fields: [team.competitionId], - references: [competition.id], - }), - paymentProof: one(media, { - fields: [team.paymentProofMediaId], - references: [media.id], - }), - submission: many(competitionSubmission), + teamMembers: many(teamMember), + competition: one(competition, { + fields: [team.competitionId], + references: [competition.id], + }), + paymentProof: one(media, { + fields: [team.paymentProofMediaId], + references: [media.id], + }), + submission: many(competitionSubmission), })); diff --git a/src/db/schema/user.schema.ts b/src/db/schema/user.schema.ts index 3cf2148..b4a52ab 100644 --- a/src/db/schema/user.schema.ts +++ b/src/db/schema/user.schema.ts @@ -1,50 +1,50 @@ import { type InferSelectModel, relations } from 'drizzle-orm'; import { - boolean, - date, - pgEnum, - pgTable, - text, - timestamp, + boolean, + date, + pgEnum, + pgTable, + text, + timestamp, } from 'drizzle-orm/pg-core'; import { getNow } from '../../utils/drizzle-schema-util'; import { userIdentity } from './auth.schema'; import { teamMember } from './team-member.schema'; export const userEducationEnum = pgEnum('user_education_enum', [ - 's1', - 's2', - 'sma', + 's1', + 's2', + 'sma', ]); export const user = pgTable('user', { - id: text('id') - .primaryKey() - .references(() => userIdentity.id), - email: text('email').notNull().unique(), // Add unique constraint - fullName: text('full_name'), - birthDate: date('birth_date'), - education: userEducationEnum('education'), - entrySource: text('entry_source'), // Ini semacam 'Where did you hear from us?', - instance: text('instance'), - phoneNumber: text('phone_number'), - idLine: text('id_line'), - idDiscord: text('id_discord'), - idInstagram: text('id_instagram'), - consent: boolean('consent').notNull().default(false), - isRegistrationComplete: boolean('is_registration_complete') - .notNull() - .default(false), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').$onUpdate(getNow), + id: text('id') + .primaryKey() + .references(() => userIdentity.id), + email: text('email').notNull().unique(), // Add unique constraint + fullName: text('full_name'), + birthDate: date('birth_date'), + education: userEducationEnum('education'), + entrySource: text('entry_source'), // Ini semacam 'Where did you hear from us?', + instance: text('instance'), + phoneNumber: text('phone_number'), + idLine: text('id_line'), + idDiscord: text('id_discord'), + idInstagram: text('id_instagram'), + consent: boolean('consent').notNull().default(false), + isRegistrationComplete: boolean('is_registration_complete') + .notNull() + .default(false), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').$onUpdate(getNow), }); export const userRelations = relations(user, ({ one, many }) => ({ - userIdentity: one(userIdentity, { - fields: [user.id], - references: [userIdentity.id], - }), - teamMember: many(teamMember), + userIdentity: one(userIdentity, { + fields: [user.id], + references: [userIdentity.id], + }), + teamMember: many(teamMember), })); export type User = InferSelectModel; diff --git a/src/index.ts b/src/index.ts index 155ef30..a89d949 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,24 @@ import { OpenAPIHono } from '@hono/zod-openapi'; import { apiReference } from '@scalar/hono-api-reference'; import { serve } from 'bun'; -import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { env } from './configs/env.config'; import { apiRouter } from './controllers/api.controller'; const app = new OpenAPIHono({ - defaultHook: (result, c) => { - if (!result.success) { - return c.json({ errors: result.error.flatten() }, 400); - } - }, + defaultHook: (result, c) => { + if (!result.success) { + return c.json({ errors: result.error.flatten() }, 400); + } + }, }); app.use( - '/api/*', - cors({ - credentials: true, - origin: env.ALLOWED_ORIGINS, - }), + '/api/*', + cors({ + credentials: true, + origin: env.ALLOWED_ORIGINS, + }), ); app.get('/', (c) => c.json({ message: 'Server runs successfully' })); @@ -27,34 +26,34 @@ app.get('/', (c) => c.json({ message: 'Server runs successfully' })); app.route('/api', apiRouter); app.doc('/openapi.json', { - openapi: '3.1.0', - info: { - version: '1.0', - title: 'Arkavidia API', - }, - tags: [ - { name: 'auth', description: 'Authentication API' }, - { name: 'media', description: 'Media API' }, - { name: 'team', description: 'Team API' }, - { name: 'team-member', description: 'Team Member API' }, - { name: 'admin', description: 'Admin API' }, - { name: 'user', description: 'User API' }, - ], + openapi: '3.1.0', + info: { + version: '1.0', + title: 'Arkavidia API', + }, + tags: [ + { name: 'auth', description: 'Authentication API' }, + { name: 'media', description: 'Media API' }, + { name: 'team', description: 'Team API' }, + { name: 'team-member', description: 'Team Member API' }, + { name: 'admin', description: 'Admin API' }, + { name: 'user', description: 'User API' }, + ], }); app.get( - '/docs', - apiReference({ - theme: 'purple', - spec: { - url: '/openapi.json', - }, - }), + '/docs', + apiReference({ + theme: 'purple', + spec: { + url: '/openapi.json', + }, + }), ); console.log(`Server is running on port ${env.PORT}`); serve({ - fetch: app.fetch, - port: env.PORT, + fetch: app.fetch, + port: env.PORT, }); diff --git a/src/lib/nodemailer.ts b/src/lib/nodemailer.ts index 03e76a9..888a68c 100644 --- a/src/lib/nodemailer.ts +++ b/src/lib/nodemailer.ts @@ -4,26 +4,26 @@ import { env } from '~/configs/env.config'; const MAIL_FROM = `Arkavidia <${env.SMTP_USER}>`; const transporter = nodemailer.createTransport({ - host: env.SMTP_HOST, - port: env.SMTP_PORT, - secure: env.SMTP_SECURE, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASSWORD, - }, + host: env.SMTP_HOST, + port: env.SMTP_PORT, + secure: env.SMTP_SECURE, + auth: { + user: env.SMTP_USER, + pass: env.SMTP_PASSWORD, + }, }); export const sendVerificationEmail = async ( - targetEmail: string, - verificationToken: string, - userId: string, + targetEmail: string, + verificationToken: string, + userId: string, ) => { - const info = await transporter.sendMail({ - from: MAIL_FROM, - to: targetEmail, - subject: 'Verify your account!', - text: `http://api.arkavidia.com/api/verify?user=${userId}&token=${verificationToken}`, // TODO: Change this to beautiful HTML - }); + const info = await transporter.sendMail({ + from: MAIL_FROM, + to: targetEmail, + subject: 'Verify your account!', + text: `http://api.arkavidia.com/api/verify?user=${userId}&token=${verificationToken}`, // TODO: Change this to beautiful HTML + }); - console.log('Message sent: %s', info.messageId); + console.log('Message sent: %s', info.messageId); }; diff --git a/src/lib/s3.ts b/src/lib/s3.ts index 0ebfc8b..06fcb1c 100644 --- a/src/lib/s3.ts +++ b/src/lib/s3.ts @@ -2,17 +2,17 @@ import * as Minio from 'minio'; import { env } from '~/configs/env.config'; const client = new Minio.Client({ - endPoint: env.S3_ENDPOINT, - // port: 9000, - useSSL: true, - accessKey: env.S3_ACCESS_KEY_ID, - secretKey: env.S3_SECRET_ACCESS_KEY, + endPoint: env.S3_ENDPOINT, + // port: 9000, + useSSL: true, + accessKey: env.S3_ACCESS_KEY_ID, + secretKey: env.S3_SECRET_ACCESS_KEY, }); export const createPutObjectPresignedUrl = async ( - key: string, - bucketName: string, - expiresIn: number, + key: string, + bucketName: string, + expiresIn: number, ) => { - return await client.presignedPutObject(bucketName, key, expiresIn); + return await client.presignedPutObject(bucketName, key, expiresIn); }; diff --git a/src/middlewares/role-access.middleware.ts b/src/middlewares/role-access.middleware.ts index 5543eee..5968aa7 100644 --- a/src/middlewares/role-access.middleware.ts +++ b/src/middlewares/role-access.middleware.ts @@ -6,19 +6,19 @@ import { findUserIdentityById } from '~/repositories/auth.repository'; import type { JWTPayloadSchema } from '~/types/auth.type'; const factory = createFactory<{ - Variables: { - user: z.infer; - }; + Variables: { + user: z.infer; + }; }>(); export const roleMiddleware = (requestedRole: UserIdentityRolesEnum) => { - return factory.createMiddleware(async (c, next) => { - const role = (await findUserIdentityById(db, c.var.user.id))?.role; + return factory.createMiddleware(async (c, next) => { + const role = (await findUserIdentityById(db, c.var.user.id))?.role; - if (role !== requestedRole) { - return c.json({ message: 'Unauthorized' }, 403); - } + if (role !== requestedRole) { + return c.json({ message: 'Unauthorized' }, 403); + } - await next(); - }); + await next(); + }); }; diff --git a/src/repositories/auth.repository.ts b/src/repositories/auth.repository.ts index ff92bc7..98ae6e6 100644 --- a/src/repositories/auth.repository.ts +++ b/src/repositories/auth.repository.ts @@ -6,47 +6,47 @@ import { type UserIdentityInsert, userIdentity } from '~/db/schema/auth.schema'; import type { UserIdentityUpdateSchema } from '~/types/auth.type'; export const createUserIdentity = async ( - db: Database, - user: UserIdentityInsert, + db: Database, + user: UserIdentityInsert, ) => { - // Also automatically creates user profile with triggers - return await db.insert(userIdentity).values(user).returning().then(firstSure); + // Also automatically creates user profile with triggers + return await db.insert(userIdentity).values(user).returning().then(firstSure); }; export const findUserIdentityById = async (db: Database, userId: string) => { - return await db - .select() - .from(userIdentity) - .where(eq(userIdentity.id, userId)) - .then(first); + return await db + .select() + .from(userIdentity) + .where(eq(userIdentity.id, userId)) + .then(first); }; export const findUserIdentityByEmail = async (db: Database, email: string) => { - return await db - .select() - .from(userIdentity) - .where(eq(userIdentity.email, email)) - .then(first); + return await db + .select() + .from(userIdentity) + .where(eq(userIdentity.email, email)) + .then(first); }; export const updateUserIdentity = async ( - db: Database, - userId: string, - user: z.infer, + db: Database, + userId: string, + user: z.infer, ) => { - return await db - .update(userIdentity) - .set(user) - .where(eq(userIdentity.id, userId)) - .returning() - .then(first); + return await db + .update(userIdentity) + .set(user) + .where(eq(userIdentity.id, userId)) + .returning() + .then(first); }; export const updateUserVerification = async (db: Database, userId: string) => { - return await db - .update(userIdentity) - .set({ isVerified: true }) - .where(eq(userIdentity.id, userId)) - .returning() - .then(first); + return await db + .update(userIdentity) + .set({ isVerified: true }) + .where(eq(userIdentity.id, userId)) + .returning() + .then(first); }; diff --git a/src/repositories/competition.repository.ts b/src/repositories/competition.repository.ts index a6aff0d..670155c 100644 --- a/src/repositories/competition.repository.ts +++ b/src/repositories/competition.repository.ts @@ -6,57 +6,57 @@ import type { z } from 'zod'; import { first } from '~/db/helper'; export const getCompetitionParticipantNumber = async ( - db: Database, - competitionId: string, + db: Database, + competitionId: string, ) => { - const result = await db.query.team.findMany({ - where: eq(team.competitionId, competitionId), - }); - return { participantCount: result.length }; + const result = await db.query.team.findMany({ + where: eq(team.competitionId, competitionId), + }); + return { participantCount: result.length }; }; export const getCompetitionById = async ( - db: Database, - competitionId: string, + db: Database, + competitionId: string, ) => { - const result = await db.query.competition.findFirst({ - where: eq(competition.id, competitionId), - }); + const result = await db.query.competition.findFirst({ + where: eq(competition.id, competitionId), + }); - return { maxParticipants: result?.maxParticipants }; + return { maxParticipants: result?.maxParticipants }; }; export const getCompetition = async (db: Database, competitionId: string) => { - const result = await db.query.competition.findFirst({ - where: eq(competition.id, competitionId), - }); - return result; + const result = await db.query.competition.findFirst({ + where: eq(competition.id, competitionId), + }); + return result; }; export const getAnnouncementsByCompetitionId = async ( - db: Database, - competitionId: string, + db: Database, + competitionId: string, ) => { - const result = await db.query.competitionAnnouncement.findMany({ - where: eq(competitionAnnouncement.competitionId, competitionId), - }); - return result; + const result = await db.query.competitionAnnouncement.findMany({ + where: eq(competitionAnnouncement.competitionId, competitionId), + }); + return result; }; export const postAnnouncement = async ( - db: Database, - competitionId: string, - authorId: string, - body: z.infer, + db: Database, + competitionId: string, + authorId: string, + body: z.infer, ) => { - return await db - .insert(competitionAnnouncement) - .values({ - competitionId: competitionId, - authorId: authorId, - title: body.title, - description: body.description, - }) - .returning() - .then(first); + return await db + .insert(competitionAnnouncement) + .values({ + competitionId: competitionId, + authorId: authorId, + title: body.title, + description: body.description, + }) + .returning() + .then(first); }; diff --git a/src/repositories/media.repository.ts b/src/repositories/media.repository.ts index 074f5e6..1e00311 100644 --- a/src/repositories/media.repository.ts +++ b/src/repositories/media.repository.ts @@ -2,23 +2,24 @@ import type { Database } from '~/db/drizzle'; import { media } from '~/db/schema'; const parseUrl = (url: string, creatorId: string) => ({ - creatorId, - name: url.split('/').at(-1) as string, - bucket: url.split('/').at(-2) as string, - type: url.split('/').at(-1) as string, - url, + creatorId, + name: url.split('/').at(-1) as string, + bucket: url.split('/').at(-2) as string, + type: url.split('/').at(-1) as string, + url, }); export const insertMediaFromUrl = async ( - db: Database, - creatorId: string, - url: string | string[], + db: Database, + creatorId: string, + url: string | string[], ) => { - const values = - typeof url === 'string' - ? [parseUrl(url, creatorId)] - : url.map((el) => parseUrl(el, creatorId)); - return await db.insert(media).values(values).returning(); + const values = + typeof url === 'string' + ? [parseUrl(url, creatorId)] + : url.map((el) => parseUrl(el, creatorId)); + return await db.insert(media).values(values).returning(); }; +/* eslint-disable */ export const deleteMedia = async (db: Database, id: string) => {}; diff --git a/src/repositories/team-member.repository.ts b/src/repositories/team-member.repository.ts index 58c2c9c..ef79317 100644 --- a/src/repositories/team-member.repository.ts +++ b/src/repositories/team-member.repository.ts @@ -4,127 +4,127 @@ import type { Database } from '~/db/drizzle'; import { first } from '~/db/helper'; import { teamMember } from '~/db/schema'; import type { - PostTeamMemberDocumentBodySchema, - PostTeamMemberVerificationBodySchema, + PostTeamMemberDocumentBodySchema, + PostTeamMemberVerificationBodySchema, } from '~/types/team-member.type'; import { getCompetitionById } from './competition.repository'; import { insertMediaFromUrl } from './media.repository'; import { getTeamById } from './team.repository'; export interface TeamMemberRelationOption { - user?: boolean; - nisn?: boolean; - kartu?: boolean; - poster?: boolean; - twibbon?: boolean; + user?: boolean; + nisn?: boolean; + kartu?: boolean; + poster?: boolean; + twibbon?: boolean; } export const getTeamMemberById = async ( - db: Database, - teamId: string, - userId: string, - options?: TeamMemberRelationOption, + db: Database, + teamId: string, + userId: string, + options?: TeamMemberRelationOption, ) => { - return await db.query.teamMember.findFirst({ - where: and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId)), - with: { - user: options?.user ? true : undefined, - nisn: options?.nisn ? true : undefined, - kartu: options?.kartu ? true : undefined, - poster: options?.poster ? true : undefined, - twibbon: options?.twibbon ? true : undefined, - }, - }); + return await db.query.teamMember.findFirst({ + where: and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId)), + with: { + user: options?.user ? true : undefined, + nisn: options?.nisn ? true : undefined, + kartu: options?.kartu ? true : undefined, + poster: options?.poster ? true : undefined, + twibbon: options?.twibbon ? true : undefined, + }, + }); }; export const updateTeamMemberDocument = async ( - db: Database, - teamId: string, - userId: string, - data: z.infer, + db: Database, + teamId: string, + userId: string, + data: z.infer, ) => { - // create media - const insert = { - nisnMediaId: data.nisnMediaId - ? (await insertMediaFromUrl(db, userId, data.nisnMediaId))[0].id - : undefined, - kartuMediaId: data.kartuMediaId - ? (await insertMediaFromUrl(db, userId, data.kartuMediaId))[0].id - : undefined, - twibbonMediaId: data.twibbonMediaId - ? (await insertMediaFromUrl(db, userId, data.twibbonMediaId))[0].id - : undefined, - posterMediaId: data.posterMediaId - ? (await insertMediaFromUrl(db, userId, data.posterMediaId))[0].id - : undefined, - }; + // create media + const insert = { + nisnMediaId: data.nisnMediaId + ? (await insertMediaFromUrl(db, userId, data.nisnMediaId))[0].id + : undefined, + kartuMediaId: data.kartuMediaId + ? (await insertMediaFromUrl(db, userId, data.kartuMediaId))[0].id + : undefined, + twibbonMediaId: data.twibbonMediaId + ? (await insertMediaFromUrl(db, userId, data.twibbonMediaId))[0].id + : undefined, + posterMediaId: data.posterMediaId + ? (await insertMediaFromUrl(db, userId, data.posterMediaId))[0].id + : undefined, + }; - return await db - .update(teamMember) - .set(insert) - .where(and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId))) - .returning() - .then(first); + return await db + .update(teamMember) + .set(insert) + .where(and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId))) + .returning() + .then(first); }; export const getTeamMemberCount = async (db: Database, teamId: string) => { - const result = await db.query.teamMember.findMany({ - where: eq(teamMember.teamId, teamId), - columns: { - teamId: true, - }, - }); + const result = await db.query.teamMember.findMany({ + where: eq(teamMember.teamId, teamId), + columns: { + teamId: true, + }, + }); - return { teamMemberCount: result.length }; + return { teamMemberCount: result.length }; }; export const insertUserToTeam = async ( - db: Database, - teamId: string, - userId: string, + db: Database, + teamId: string, + userId: string, ) => { - return await db.transaction(async (tx) => { - const team = await getTeamById(db, teamId); - if (!team) { - throw new Error("Such team doesn't exist"); - } + return await db.transaction(async (tx) => { + const team = await getTeamById(db, teamId); + if (!team) { + throw new Error("Such team doesn't exist"); + } - const { teamMemberCount } = await getTeamMemberCount(db, teamId); - const { maxParticipants } = await getCompetitionById( - db, - team.competitionId, - ); + const { teamMemberCount } = await getTeamMemberCount(db, teamId); + const { maxParticipants } = await getCompetitionById( + db, + team.competitionId, + ); - if (!maxParticipants) { - throw new Error('There is no such competition'); - } - if (maxParticipants <= teamMemberCount) { - throw new Error('The team is already full'); - } + if (!maxParticipants) { + throw new Error('There is no such competition'); + } + if (maxParticipants <= teamMemberCount) { + throw new Error('The team is already full'); + } - const [insertedMember] = await tx - .insert(teamMember) - .values({ - teamId, - userId, - role: 'leader', - }) - .returning(); + const [insertedMember] = await tx + .insert(teamMember) + .values({ + teamId, + userId, + role: 'leader', + }) + .returning(); - return insertedMember; - }); + return insertedMember; + }); }; export const updateTeamMemberVerification = async ( - db: Database, - teamId: string, - userId: string, - data: z.infer, + db: Database, + teamId: string, + userId: string, + data: z.infer, ) => { - return await db - .update(teamMember) - .set(data) - .where(and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId))) - .returning() - .then(first); + return await db + .update(teamMember) + .set(data) + .where(and(eq(teamMember.teamId, teamId), eq(teamMember.userId, userId))) + .returning() + .then(first); }; diff --git a/src/repositories/team.repository.ts b/src/repositories/team.repository.ts index 9a6449a..26a6c3c 100644 --- a/src/repositories/team.repository.ts +++ b/src/repositories/team.repository.ts @@ -6,7 +6,6 @@ import { team, teamMember } from '~/db/schema'; import type { PostTeamDocumentBodySchema, PostTeamVerificationBodySchema, - TeamMemberIdSchema, putChangeTeamNameBodySchema, } from '~/types/team.type'; import { diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 4e6e058..a85e164 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -6,22 +6,22 @@ import { user } from '~/db/schema/user.schema'; import type { UserUpdateSchema } from '~/types/user.type'; export const findUserByEmail = async (db: Database, email: string) => { - return db.select().from(user).where(eq(user.email, email)).then(first); + return db.select().from(user).where(eq(user.email, email)).then(first); }; export const findUserById = async (db: Database, id: string) => { - return await db.select().from(user).where(eq(user.id, id)).then(first); + return await db.select().from(user).where(eq(user.id, id)).then(first); }; export const updateUser = async ( - db: Database, - userId: string, - userData: z.infer, + db: Database, + userId: string, + userData: z.infer, ) => { - return await db - .update(user) - .set(userData) - .where(eq(user.id, userId)) - .returning() - .then(first); + return await db + .update(user) + .set(userData) + .where(eq(user.id, userId)) + .returning() + .then(first); }; diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts index 10d003c..ddf215f 100644 --- a/src/routes/auth.route.ts +++ b/src/routes/auth.route.ts @@ -1,188 +1,187 @@ -import { createRoute, z } from '@hono/zod-openapi'; +import { createRoute } from '@hono/zod-openapi'; import { - AccessRefreshTokenSchema, - AccessTokenSchema, - BasicLoginBodySchema, - BasicRegisterBodySchema, - BasicVerifyAccountQuerySchema, - GoogleCallbackQuerySchema, - RefreshTokenQuerySchema, + AccessRefreshTokenSchema, + BasicLoginBodySchema, + BasicRegisterBodySchema, + BasicVerifyAccountQuerySchema, + GoogleCallbackQuerySchema, + RefreshTokenQuerySchema, } from '~/types/auth.type'; import { UserSchema } from '~/types/user.type'; import { createErrorResponse } from '../utils/error-response-factory'; /** BASIC AUTHENTICATION ROUTES (Email & Password) */ export const basicRegisterRoute = createRoute({ - operationId: 'basicRegister', - tags: ['auth'], - method: 'post', - path: '/auth/basic/register', - request: { - body: { - content: { - 'application/json': { - schema: BasicRegisterBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 204: { - description: 'Registration succesful. Verification token sent to email.', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'basicRegister', + tags: ['auth'], + method: 'post', + path: '/auth/basic/register', + request: { + body: { + content: { + 'application/json': { + schema: BasicRegisterBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 204: { + description: 'Registration succesful. Verification token sent to email.', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const basicVerifyAccountRoute = createRoute({ - operationId: 'basicVerifyAccount', - tags: ['auth'], - method: 'post', - path: '/auth/verify', - request: { - query: BasicVerifyAccountQuerySchema, - }, - responses: { - 200: { - description: 'Verification sucessful, automatic login', - content: { - 'application/json': { - schema: AccessRefreshTokenSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'basicVerifyAccount', + tags: ['auth'], + method: 'post', + path: '/auth/verify', + request: { + query: BasicVerifyAccountQuerySchema, + }, + responses: { + 200: { + description: 'Verification sucessful, automatic login', + content: { + 'application/json': { + schema: AccessRefreshTokenSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const basicLoginRoute = createRoute({ - operationId: 'basicLogin', - tags: ['auth'], - method: 'post', - path: '/auth/basic/login', - request: { - body: { - content: { - 'application/json': { - schema: BasicLoginBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - description: 'Login succesful', - content: { - 'application/json': { - schema: AccessRefreshTokenSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'basicLogin', + tags: ['auth'], + method: 'post', + path: '/auth/basic/login', + request: { + body: { + content: { + 'application/json': { + schema: BasicLoginBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + description: 'Login succesful', + content: { + 'application/json': { + schema: AccessRefreshTokenSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); /** GOOGLE AUTHENTICATION ROUTES */ export const googleAuthRoute = createRoute({ - operationId: 'googleAuth', - tags: ['auth'], - method: 'get', - path: '/auth/google', - responses: { - 302: { - description: 'Redirect to Google login', - headers: { - location: { - description: 'URL to Google consent screen', - schema: { - type: 'string', - }, - }, - }, - }, - }, + operationId: 'googleAuth', + tags: ['auth'], + method: 'get', + path: '/auth/google', + responses: { + 302: { + description: 'Redirect to Google login', + headers: { + location: { + description: 'URL to Google consent screen', + schema: { + type: 'string', + }, + }, + }, + }, + }, }); export const googleAuthCallbackRoute = createRoute({ - operationId: 'googleAuthCallback', - tags: ['auth'], - method: 'get', - path: '/auth/google/callback', - request: { - query: GoogleCallbackQuerySchema, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: AccessRefreshTokenSchema, - }, - }, - description: 'Login succesful', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'googleAuthCallback', + tags: ['auth'], + method: 'get', + path: '/auth/google/callback', + request: { + query: GoogleCallbackQuerySchema, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: AccessRefreshTokenSchema, + }, + }, + description: 'Login succesful', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); /** BOTH AUTH */ export const logoutRoute = createRoute({ - operationId: 'logout', - tags: ['auth'], - method: 'post', - path: '/auth/logout', - responses: { - 204: { - description: 'Logout sucessful', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 401: createErrorResponse('UNION', 'Unauthorized'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'logout', + tags: ['auth'], + method: 'post', + path: '/auth/logout', + responses: { + 204: { + description: 'Logout sucessful', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 401: createErrorResponse('UNION', 'Unauthorized'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const selfRoute = createRoute({ - operationId: 'self', - tags: ['auth'], - method: 'get', - path: '/auth/self', - responses: { - 200: { - description: 'Get self', - content: { - 'application/json': { - schema: UserSchema, - }, - }, - }, - 401: createErrorResponse('GENERIC', 'Unauthorized'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'self', + tags: ['auth'], + method: 'get', + path: '/auth/self', + responses: { + 200: { + description: 'Get self', + content: { + 'application/json': { + schema: UserSchema, + }, + }, + }, + 401: createErrorResponse('GENERIC', 'Unauthorized'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const refreshRoute = createRoute({ - operationId: 'refresh', - tags: ['auth'], - method: 'get', - path: '/auth/refresh', - request: { - query: RefreshTokenQuerySchema, - }, - responses: { - 200: { - description: 'Refresh access token,', - content: { - 'application/json': { - schema: AccessRefreshTokenSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'refresh', + tags: ['auth'], + method: 'get', + path: '/auth/refresh', + request: { + query: RefreshTokenQuerySchema, + }, + responses: { + 200: { + description: 'Refresh access token,', + content: { + 'application/json': { + schema: AccessRefreshTokenSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/competition.route.ts b/src/routes/competition.route.ts index 38000fe..f023753 100644 --- a/src/routes/competition.route.ts +++ b/src/routes/competition.route.ts @@ -1,60 +1,60 @@ import { createRoute } from '@hono/zod-openapi'; import { - AnnouncementSchema, - PostCompAnnouncementBodySchema, + AnnouncementSchema, + PostCompAnnouncementBodySchema, } from '~/types/competition.type'; import { AllAnnouncementSchema } from '~/types/competition.type'; import { CompetitionIdParam } from '~/types/competition.type'; import { createErrorResponse } from '~/utils/error-response-factory'; export const getAdminCompAnnouncementRoute = createRoute({ - operationId: 'getAdminCompAnnouncement', - tags: ['admin', 'competition'], - method: 'get', - path: '/admin/{competitionId}/announcement', - request: { - params: CompetitionIdParam, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: AllAnnouncementSchema, - }, - }, - description: 'Succesfully fetched all announcements', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getAdminCompAnnouncement', + tags: ['admin', 'competition'], + method: 'get', + path: '/admin/{competitionId}/announcement', + request: { + params: CompetitionIdParam, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: AllAnnouncementSchema, + }, + }, + description: 'Succesfully fetched all announcements', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postAdminCompAnnouncementRoute = createRoute({ - operationId: 'postAdminCompAnnouncement', - tags: ['admin', 'competition'], - method: 'post', - path: '/api/admin/{competitionId}/announcement', - request: { - params: CompetitionIdParam, - body: { - content: { - 'application/json': { - schema: PostCompAnnouncementBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: AnnouncementSchema, - }, - }, - description: 'Succesfully posted announcement', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postAdminCompAnnouncement', + tags: ['admin', 'competition'], + method: 'post', + path: '/api/admin/{competitionId}/announcement', + request: { + params: CompetitionIdParam, + body: { + content: { + 'application/json': { + schema: PostCompAnnouncementBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: AnnouncementSchema, + }, + }, + description: 'Succesfully posted announcement', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/health.route.ts b/src/routes/health.route.ts index 8793ef3..232e680 100644 --- a/src/routes/health.route.ts +++ b/src/routes/health.route.ts @@ -2,22 +2,22 @@ import { createRoute, z } from '@hono/zod-openapi'; import { createErrorResponse } from '../utils/error-response-factory'; export const getHealthStatusRoute = createRoute({ - operationId: 'getHealthStatus', - tags: ['health'], - method: 'get', - path: '/health', - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - message: z.string().default('API is running sucesfully!'), - }), - }, - }, - description: 'Check if server is healthy', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getHealthStatus', + tags: ['health'], + method: 'get', + path: '/health', + responses: { + 200: { + content: { + 'application/json': { + schema: z.object({ + message: z.string().default('API is running sucesfully!'), + }), + }, + }, + description: 'Check if server is healthy', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/media.route.ts b/src/routes/media.route.ts index 7620149..7ac6af2 100644 --- a/src/routes/media.route.ts +++ b/src/routes/media.route.ts @@ -1,28 +1,28 @@ import { createRoute } from '@hono/zod-openapi'; import { - GetPresignedLinkQuerySchema, - PresignedUrlSchema, + GetPresignedLinkQuerySchema, + PresignedUrlSchema, } from '~/types/media.type'; import { createErrorResponse } from '../utils/error-response-factory'; export const getPresignedLink = createRoute({ - operationId: 'getPresignedLink', - tags: ['media'], - method: 'get', - path: '/media/upload', - request: { - query: GetPresignedLinkQuerySchema, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: PresignedUrlSchema, - }, - }, - description: 'Get presign URL to upload file', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getPresignedLink', + tags: ['media'], + method: 'get', + path: '/media/upload', + request: { + query: GetPresignedLinkQuerySchema, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: PresignedUrlSchema, + }, + }, + description: 'Get presign URL to upload file', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/team-member.route.ts b/src/routes/team-member.route.ts index d32d93a..baac3b8 100644 --- a/src/routes/team-member.route.ts +++ b/src/routes/team-member.route.ts @@ -1,86 +1,85 @@ import { createRoute } from '@hono/zod-openapi'; import { - CompetitionAndTeamAndUserIdParam, - PostTeamMemberDocumentBodySchema, - PostTeamMemberVerificationBodySchema, - TeamAndUserIdParam, - TeamMemberSchema, + CompetitionAndTeamAndUserIdParam, + PostTeamMemberDocumentBodySchema, + PostTeamMemberVerificationBodySchema, + TeamMemberSchema, } from '~/types/team-member.type'; import { TeamIdParam } from '~/types/team.type'; import { createErrorResponse } from '~/utils/error-response-factory'; export const getTeamMemberRoute = createRoute({ - operationId: 'getTeamMember', - tags: ['team-member'], - method: 'get', - path: '/team/{teamId}/member', - request: { - params: TeamIdParam, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamMemberSchema, - }, - }, - description: 'Succesfully fetched tean member', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getTeamMember', + tags: ['team-member'], + method: 'get', + path: '/team/{teamId}/member', + request: { + params: TeamIdParam, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamMemberSchema, + }, + }, + description: 'Succesfully fetched tean member', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postTeamMemberDocumentRoute = createRoute({ - operationId: 'postTeamMemberDocument', - tags: ['team-member'], - method: 'post', - path: '/team/{teamId}/upload', - request: { - params: TeamIdParam, - body: { - content: { - 'application/json': { - schema: PostTeamMemberDocumentBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamMemberSchema, - }, - }, - description: 'Succesfully updated document upload', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postTeamMemberDocument', + tags: ['team-member'], + method: 'post', + path: '/team/{teamId}/upload', + request: { + params: TeamIdParam, + body: { + content: { + 'application/json': { + schema: PostTeamMemberDocumentBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamMemberSchema, + }, + }, + description: 'Succesfully updated document upload', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postTeamMemberVerificationRoute = createRoute({ - operationId: 'postTeamMemberVerification', - tags: ['team-member', 'admin'], - method: 'post', - path: '/admin/{competitionId}/team/{teamId}/{userId}', - request: { - params: CompetitionAndTeamAndUserIdParam, - body: { - content: { - 'application/json': { - schema: PostTeamMemberVerificationBodySchema, - }, - }, - }, - }, - responses: { - 200: { - description: 'Succesfully updated document verification', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postTeamMemberVerification', + tags: ['team-member', 'admin'], + method: 'post', + path: '/admin/{competitionId}/team/{teamId}/{userId}', + request: { + params: CompetitionAndTeamAndUserIdParam, + body: { + content: { + 'application/json': { + schema: PostTeamMemberVerificationBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Succesfully updated document verification', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/team.route.ts b/src/routes/team.route.ts index 96c56b0..f17862e 100644 --- a/src/routes/team.route.ts +++ b/src/routes/team.route.ts @@ -1,202 +1,202 @@ import { createRoute } from '@hono/zod-openapi'; import { TeamMemberSchema } from '~/types/team-member.type'; import { - CompetitionAndTeamIdParam, - PostTeamBodySchema, - PostTeamDocumentBodySchema, - PostTeamVerificationBodySchema, - TeamIdParam, - TeamMemberIdSchema, - TeamSchema, - putChangeTeamNameBodySchema, + CompetitionAndTeamIdParam, + PostTeamBodySchema, + PostTeamDocumentBodySchema, + PostTeamVerificationBodySchema, + TeamIdParam, + TeamMemberIdSchema, + TeamSchema, + putChangeTeamNameBodySchema, } from '~/types/team.type'; import { createErrorResponse } from '~/utils/error-response-factory'; export const joinTeamByCodeRoute = createRoute({ - operationId: 'joinTeamByCode', - tags: ['team'], - method: 'get', - path: '/team/join', - responses: {}, + operationId: 'joinTeamByCode', + tags: ['team'], + method: 'get', + path: '/team/join', + responses: {}, }); export const getTeamsRoute = createRoute({ - operationId: 'getTeams', - tags: ['team'], - method: 'get', - path: '/team', - responses: {}, + operationId: 'getTeams', + tags: ['team'], + method: 'get', + path: '/team', + responses: {}, }); export const getTeamByIdRoute = createRoute({ - operationId: 'getTeamById', - tags: ['team'], - method: 'get', - path: '/team/{teamId}', - responses: {}, + operationId: 'getTeamById', + tags: ['team'], + method: 'get', + path: '/team/{teamId}', + responses: {}, }); export const postCreateTeamRoute = createRoute({ - operationId: 'postCreateTeam', - tags: ['team'], - method: 'post', - path: '/team', - request: { - body: { - content: { - 'application/json': { - schema: PostTeamBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamSchema, - }, - }, - description: 'Successfully created a team', - }, - 400: createErrorResponse('UNION', 'Bad Request Error'), - 500: createErrorResponse('GENERIC', 'Internal Server Error'), - }, + operationId: 'postCreateTeam', + tags: ['team'], + method: 'post', + path: '/team', + request: { + body: { + content: { + 'application/json': { + schema: PostTeamBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamSchema, + }, + }, + description: 'Successfully created a team', + }, + 400: createErrorResponse('UNION', 'Bad Request Error'), + 500: createErrorResponse('GENERIC', 'Internal Server Error'), + }, }); export const postQuitTeamRoute = createRoute({ - operationId: 'postQuitTeam', - tags: ['team'], - method: 'post', - path: '/team/{teamId}/quit', - request: { - params: TeamIdParam, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamSchema, - }, - }, - description: 'Succesfully quit team', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postQuitTeam', + tags: ['team'], + method: 'post', + path: '/team/{teamId}/quit', + request: { + params: TeamIdParam, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamSchema, + }, + }, + description: 'Succesfully quit team', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postTeamDocumentRoute = createRoute({ - operationId: 'postTeamDocument', - tags: ['team'], - method: 'put', // change method to put: method (post) and path intersect with other feature (team member document submit) - path: '/team/{teamId}/upload', - request: { - params: TeamIdParam, - body: { - content: { - 'application/json': { - schema: PostTeamDocumentBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamSchema, - }, - }, - description: 'Succesfully updated team document upload', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postTeamDocument', + tags: ['team'], + method: 'put', // change method to put: method (post) and path intersect with other feature (team member document submit) + path: '/team/{teamId}/upload', + request: { + params: TeamIdParam, + body: { + content: { + 'application/json': { + schema: PostTeamDocumentBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamSchema, + }, + }, + description: 'Succesfully updated team document upload', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const putChangeTeamNameRoute = createRoute({ - operationId: 'putChangeTeamName', - tags: ['team'], - method: 'put', - path: '/team/{teamId}', - request: { - params: TeamIdParam, - body: { - content: { - 'application/json': { - schema: putChangeTeamNameBodySchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamSchema, - }, - }, - description: 'Succesfully updated team name', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'putChangeTeamName', + tags: ['team'], + method: 'put', + path: '/team/{teamId}', + request: { + params: TeamIdParam, + body: { + content: { + 'application/json': { + schema: putChangeTeamNameBodySchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamSchema, + }, + }, + description: 'Succesfully updated team name', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const deleteTeamMemberRoute = createRoute({ - operationId: 'deleteTeamMember', - tags: ['team'], - method: 'delete', - path: '/team/{teamId}', - request: { - params: TeamIdParam, - body: { - content: { - 'application/json': { - schema: TeamMemberIdSchema, - }, - }, - required: true, - }, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: TeamMemberSchema, - }, - }, - description: 'Succesfully deleted team member', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'deleteTeamMember', + tags: ['team'], + method: 'delete', + path: '/team/{teamId}', + request: { + params: TeamIdParam, + body: { + content: { + 'application/json': { + schema: TeamMemberIdSchema, + }, + }, + required: true, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: TeamMemberSchema, + }, + }, + description: 'Succesfully deleted team member', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postTeamVerificationRoute = createRoute({ - operationId: 'postTeamVerification', - tags: ['team', 'admin'], - method: 'post', - path: '/admin/{competitionId}/team/{teamId}', - request: { - params: CompetitionAndTeamIdParam, - body: { - content: { - 'application/json': { - schema: PostTeamVerificationBodySchema, - }, - }, - }, - }, - responses: { - 200: { - description: 'Succesfully updated team verification', - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'postTeamVerification', + tags: ['team', 'admin'], + method: 'post', + path: '/admin/{competitionId}/team/{teamId}', + request: { + params: CompetitionAndTeamIdParam, + body: { + content: { + 'application/json': { + schema: PostTeamVerificationBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Succesfully updated team verification', + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts index da9f1dd..323889b 100644 --- a/src/routes/user.route.ts +++ b/src/routes/user.route.ts @@ -1,54 +1,50 @@ import { createRoute } from '@hono/zod-openapi'; -import { - UpdateUserBodyRoute, - UserSchema, - UserUpdateSchema, -} from '~/types/user.type'; +import { UpdateUserBodyRoute, UserSchema } from '~/types/user.type'; import { createErrorResponse } from '~/utils/error-response-factory'; export const getUserRoute = createRoute({ - operationId: 'getUser', - tags: ['user'], - method: 'get', - path: '/user', - responses: { - 200: { - description: 'Fetched currently logged in user.', - content: { - 'application/json': { - schema: UserSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getUser', + tags: ['user'], + method: 'get', + path: '/user', + responses: { + 200: { + description: 'Fetched currently logged in user.', + content: { + 'application/json': { + schema: UserSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const updateUserRoute = createRoute({ - operationId: 'updateUser', - tags: ['user'], - method: 'put', - path: '/user', - request: { - body: { - content: { - 'application/json': { - schema: UpdateUserBodyRoute, - }, - }, - }, - }, - responses: { - 200: { - description: 'Updates currenly logged in user profile.', - content: { - 'application/json': { - schema: UserSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'updateUser', + tags: ['user'], + method: 'put', + path: '/user', + request: { + body: { + content: { + 'application/json': { + schema: UpdateUserBodyRoute, + }, + }, + }, + }, + responses: { + 200: { + description: 'Updates currenly logged in user profile.', + content: { + 'application/json': { + schema: UserSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); diff --git a/src/types/auth.type.ts b/src/types/auth.type.ts index e923ca7..09c5315 100644 --- a/src/types/auth.type.ts +++ b/src/types/auth.type.ts @@ -6,89 +6,89 @@ import { UserSchema } from './user.type'; export const UserIdentitySchema = createSelectSchema(userIdentity); export const UserIdentityUpdateSchema = - createInsertSchema(userIdentity).partial(); + createInsertSchema(userIdentity).partial(); export const JWTPayloadSchema = UserSchema.merge( - UserIdentitySchema.pick({ provider: true }), + UserIdentitySchema.pick({ provider: true }), ).openapi('JWTPayload'); export const BasicLoginBodySchema = z.object({ - email: z.string().email(), - password: z.string(), + email: z.string().email(), + password: z.string(), }); export const BasicRegisterBodySchema = z - .object({ - email: z.string().email(), - password: z.string().min(8, 'Password must have minimum length of 8'), - confirmPassword: z.string(), - }) - .refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match", - path: ['confirm'], // path of error - }); + .object({ + email: z.string().email(), + password: z.string().min(8, 'Password must have minimum length of 8'), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirm'], // path of error + }); export const BasicVerifyAccountQuerySchema = z.object({ - user: z.string().openapi({ - param: { - in: 'query', - required: true, - }, - }), - token: z.string().openapi({ - param: { - in: 'query', - required: true, - }, - }), + user: z.string().openapi({ + param: { + in: 'query', + required: true, + }, + }), + token: z.string().openapi({ + param: { + in: 'query', + required: true, + }, + }), }); export const GoogleCallbackQuerySchema = z.object({ - code: z.string().openapi({ - param: { - in: 'query', - example: '4/0AY0e-g7Qj6y2v7zR7iJ2b9b4V6K7zrZ9X0q4Q', - }, - }), + code: z.string().openapi({ + param: { + in: 'query', + example: '4/0AY0e-g7Qj6y2v7zR7iJ2b9b4V6K7zrZ9X0q4Q', + }, + }), }); export const AccessRefreshTokenSchema = z - .object({ - accessToken: z.string(), - refreshToken: z.string(), - }) - .openapi('AccessAndRefreshToken'); + .object({ + accessToken: z.string(), + refreshToken: z.string(), + }) + .openapi('AccessAndRefreshToken'); export const AccessTokenSchema = AccessRefreshTokenSchema.pick({ - accessToken: true, + accessToken: true, }).openapi('AccessToken'); export const RefreshTokenQuerySchema = z.object({ - token: z.string().openapi({ - param: { - in: 'query', - required: true, - }, - }), + token: z.string().openapi({ + param: { + in: 'query', + required: true, + }, + }), }); export const GoogleTokenDataSchema = z.object({ - access_token: z.string(), - expires_in: z.number(), - refresh_token: z.string(), - scope: z.string(), - token_type: z.string(), - id_token: z.string(), + access_token: z.string(), + expires_in: z.number(), + refresh_token: z.string(), + scope: z.string(), + token_type: z.string(), + id_token: z.string(), }); export const GoogleUserSchema = z.object({ - id: z.string(), - name: z.string(), - given_name: z.string().optional(), - family_name: z.string().optional(), - picture: z.string().optional(), - email: z.string(), - email_verified: z.string().optional(), - locale: z.string().optional(), - hd: z.string().optional(), + id: z.string(), + name: z.string(), + given_name: z.string().optional(), + family_name: z.string().optional(), + picture: z.string().optional(), + email: z.string(), + email_verified: z.string().optional(), + locale: z.string().optional(), + hd: z.string().optional(), }); diff --git a/src/types/competition.type.ts b/src/types/competition.type.ts index 433e634..abfd4db 100644 --- a/src/types/competition.type.ts +++ b/src/types/competition.type.ts @@ -1,23 +1,23 @@ -import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; +import { createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { competitionAnnouncement } from '~/db/schema'; export const AnnouncementSchema = createSelectSchema(competitionAnnouncement, { - createdAt: z.union([z.string(), z.date()]), + createdAt: z.union([z.string(), z.date()]), }).openapi('Announcement'); export const AllAnnouncementSchema = z.array(AnnouncementSchema); export const CompetitionIdParam = z.object({ - competitionId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), + competitionId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), }); export const PostCompAnnouncementBodySchema = z.object({ - title: z.string().min(1), - description: z.string().min(1), + title: z.string().min(1), + description: z.string().min(1), }); diff --git a/src/types/media.type.ts b/src/types/media.type.ts index cc6d2b8..1d2c8a9 100644 --- a/src/types/media.type.ts +++ b/src/types/media.type.ts @@ -5,29 +5,29 @@ import { media, mediaBucketEnum } from '~/db/schema/media.schema'; export const MediaSchema = createSelectSchema(media).openapi('Media'); export const GetPresignedLinkQuerySchema = z.object({ - filename: z.string().openapi({ - description: 'name of file with extension', - example: 'cat.png', - param: { - in: 'query', - required: true, - }, - }), - bucket: z.enum(mediaBucketEnum.enumValues).openapi({ - example: 'competition-registration', - param: { - in: 'query', - required: true, - }, - }), + filename: z.string().openapi({ + description: 'name of file with extension', + example: 'cat.png', + param: { + in: 'query', + required: true, + }, + }), + bucket: z.enum(mediaBucketEnum.enumValues).openapi({ + example: 'competition-registration', + param: { + in: 'query', + required: true, + }, + }), }); export const PresignedUrlSchema = z - .object({ - presignedUrl: z.string().url(), - mediaUrl: z.string().url(), - expiresIn: z.number().openapi({ - example: 3600, - }), - }) - .openapi('PresignedURL'); + .object({ + presignedUrl: z.string().url(), + mediaUrl: z.string().url(), + expiresIn: z.number().openapi({ + example: 3600, + }), + }) + .openapi('PresignedURL'); diff --git a/src/types/responses.type.ts b/src/types/responses.type.ts index 3070c22..8461e7b 100644 --- a/src/types/responses.type.ts +++ b/src/types/responses.type.ts @@ -1,18 +1,18 @@ import { type createRoute, z } from '@hono/zod-openapi'; export type ResponseItem = Parameters< - typeof createRoute + typeof createRoute >[0]['responses'][string]; export const ValidationErrorSchema = z - .object({ - formErrors: z.string().array(), - fieldErrors: z.record(z.string().array()), - }) - .openapi('ValidationError'); + .object({ + formErrors: z.string().array(), + fieldErrors: z.record(z.string().array()), + }) + .openapi('ValidationError'); export const GenericErrorShema = z - .object({ - error: z.string(), - }) - .openapi('GenericError'); + .object({ + error: z.string(), + }) + .openapi('GenericError'); diff --git a/src/types/team-member.type.ts b/src/types/team-member.type.ts index bb3046e..1d6e6dd 100644 --- a/src/types/team-member.type.ts +++ b/src/types/team-member.type.ts @@ -4,62 +4,62 @@ import { teamMember } from '~/db/schema/team-member.schema'; import { MediaSchema } from './media.type'; export const TeamAndUserIdParam = z.object({ - teamId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), - userId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), + teamId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), + userId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), }); export const TeamMemberSchema = createSelectSchema(teamMember) - .merge( - z.object({ - nisn: MediaSchema, - kartu: MediaSchema, - poster: MediaSchema, - twibbon: MediaSchema, - }), - ) - .openapi('TeamMember'); + .merge( + z.object({ + nisn: MediaSchema, + kartu: MediaSchema, + poster: MediaSchema, + twibbon: MediaSchema, + }), + ) + .openapi('TeamMember'); export const PostTeamMemberDocumentBodySchema = createInsertSchema( - teamMember, + teamMember, ).pick({ - nisnMediaId: true, - kartuMediaId: true, - posterMediaId: true, - twibbonMediaId: true, + nisnMediaId: true, + kartuMediaId: true, + posterMediaId: true, + twibbonMediaId: true, }); export const CompetitionAndTeamAndUserIdParam = z.object({ - competitionId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), - teamId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), - userId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), + competitionId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), + teamId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), + userId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), }); export const PostTeamMemberVerificationBodySchema = z.object({ - isVerified: z.boolean(), - verificationError: z.string().optional(), + isVerified: z.boolean(), + verificationError: z.string().optional(), }); diff --git a/src/types/team.type.ts b/src/types/team.type.ts index 49b6535..314c649 100644 --- a/src/types/team.type.ts +++ b/src/types/team.type.ts @@ -3,11 +3,11 @@ import { z } from 'zod'; import { team } from '~/db/schema'; export const PostTeamDocumentBodySchema = createInsertSchema(team).pick({ - paymentProofMediaId: true, + paymentProofMediaId: true, }); export const TeamSchema = createSelectSchema(team, { - createdAt: z.union([z.string(), z.date()]), + createdAt: z.union([z.string(), z.date()]), }).openapi('Team'); export const TeamIdParam = z.object({ teamId: z.string() }); @@ -15,30 +15,30 @@ export const TeamIdParam = z.object({ teamId: z.string() }); export const TeamMemberIdSchema = z.object({ userId: z.string() }); export const putChangeTeamNameBodySchema = z.object({ - name: z.string().min(1), + name: z.string().min(1), }); export const CompetitionAndTeamIdParam = z.object({ - competitionId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), - teamId: z.string().openapi({ - param: { - in: 'path', - required: true, - }, - }), + competitionId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), + teamId: z.string().openapi({ + param: { + in: 'path', + required: true, + }, + }), }); export const PostTeamVerificationBodySchema = z.object({ - isVerified: z.boolean(), - verificationError: z.string().optional(), + isVerified: z.boolean(), + verificationError: z.string().optional(), }); export const PostTeamBodySchema = createInsertSchema(team).pick({ - competitionId: true, - name: true, + competitionId: true, + name: true, }); diff --git a/src/types/user.type.ts b/src/types/user.type.ts index 064e405..6ea05a5 100644 --- a/src/types/user.type.ts +++ b/src/types/user.type.ts @@ -3,16 +3,16 @@ import { z } from 'zod'; import { user } from '~/db/schema'; export const UserSchema = createSelectSchema(user, { - createdAt: z.union([z.string(), z.date()]), - updatedAt: z.union([z.string(), z.date()]), + createdAt: z.union([z.string(), z.date()]), + updatedAt: z.union([z.string(), z.date()]), }).openapi('User'); export const UserUpdateSchema = createInsertSchema(user).partial(); export const UpdateUserBodyRoute = UserUpdateSchema.omit({ - id: true, - email: true, - createdAt: true, - updatedAt: true, - isRegistrationComplete: true, + id: true, + email: true, + createdAt: true, + updatedAt: true, + isRegistrationComplete: true, }); diff --git a/src/utils/drizzle-schema-util.ts b/src/utils/drizzle-schema-util.ts index 35f4cd0..942beb1 100644 --- a/src/utils/drizzle-schema-util.ts +++ b/src/utils/drizzle-schema-util.ts @@ -1,7 +1,7 @@ import { init } from '@paralleldrive/cuid2'; export const createId = init({ - length: 8, + length: 8, }); export const getNow = () => new Date(); diff --git a/src/utils/error-response-factory.ts b/src/utils/error-response-factory.ts index b2da183..2fb37b5 100644 --- a/src/utils/error-response-factory.ts +++ b/src/utils/error-response-factory.ts @@ -1,27 +1,27 @@ import type { ResponseConfig } from '@asteasolutions/zod-to-openapi/dist/openapi-registry.js'; import { z } from 'zod'; import { - GenericErrorShema, - ValidationErrorSchema, + GenericErrorShema, + ValidationErrorSchema, } from '../types/responses.type'; const typeToSchema = (error: 'GENERIC' | 'VALIDATION' | 'UNION') => { - if (error === 'GENERIC') return GenericErrorShema; - if (error === 'VALIDATION') return ValidationErrorSchema; - if (error === 'UNION') - return z.union([GenericErrorShema, ValidationErrorSchema]); + if (error === 'GENERIC') return GenericErrorShema; + if (error === 'VALIDATION') return ValidationErrorSchema; + if (error === 'UNION') + return z.union([GenericErrorShema, ValidationErrorSchema]); }; export const createErrorResponse = ( - error: 'GENERIC' | 'VALIDATION' | 'UNION', - description: string, + error: 'GENERIC' | 'VALIDATION' | 'UNION', + description: string, ) => { - return { - description, - content: { - 'application/json': { - schema: typeToSchema(error), - }, - }, - } as ResponseConfig; + return { + description, + content: { + 'application/json': { + schema: typeToSchema(error), + }, + }, + } as ResponseConfig; }; diff --git a/src/utils/router-factory.ts b/src/utils/router-factory.ts index 5b10f13..10d29f1 100644 --- a/src/utils/router-factory.ts +++ b/src/utils/router-factory.ts @@ -3,42 +3,41 @@ import { jwt } from 'hono/jwt'; import { env } from '~/configs/env.config'; import { JWTPayloadSchema } from '~/types/auth.type'; -// biome-ignore lint/suspicious/noExplicitAny: const defaultHook: Hook = (result, c) => { - if (!result.success) { - return c.json({ errors: result.error.flatten() }, 400); - } + if (!result.success) { + return c.json({ errors: result.error.flatten() }, 400); + } }; export function createRouter() { - return new OpenAPIHono({ defaultHook }); + return new OpenAPIHono({ defaultHook }); } export function createAuthRouter() { - const authRouter = new OpenAPIHono<{ - Variables: { - user: z.infer; - }; - }>({ defaultHook }); + const authRouter = new OpenAPIHono<{ + Variables: { + user: z.infer; + }; + }>({ defaultHook }); - // JWT Hono Middleware - try { - authRouter.use( - jwt({ - secret: env.ACCESS_TOKEN_SECRET, - cookie: 'khongguan', - }), - ); - } catch (e) { - console.log(e); - } + // JWT Hono Middleware + try { + authRouter.use( + jwt({ + secret: env.ACCESS_TOKEN_SECRET, + cookie: 'khongguan', + }), + ); + } catch (e) { + console.log(e); + } - // Set user middleware - authRouter.use(async (c, next) => { - const payload = JWTPayloadSchema.parse(c.var.jwtPayload); - c.set('user', payload); - await next(); - }); + // Set user middleware + authRouter.use(async (c, next) => { + const payload = JWTPayloadSchema.parse(c.var.jwtPayload); + c.set('user', payload); + await next(); + }); - return authRouter; + return authRouter; } From abbcab6d31f8aa3afb36b0bf3b65273e5577d457 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Mon, 16 Dec 2024 14:43:54 +0700 Subject: [PATCH 04/13] chore: change eslint config --- .eslintrc.js | 1 - src/utils/router-factory.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 894563e..48b409f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,6 @@ module.exports = { }, rules: { 'no-console': 'off', - '@typescript-eslint/no-explicit-any': 'off', 'prettier/prettier': 'error', }, }; \ No newline at end of file diff --git a/src/utils/router-factory.ts b/src/utils/router-factory.ts index 10d29f1..c730ac3 100644 --- a/src/utils/router-factory.ts +++ b/src/utils/router-factory.ts @@ -3,6 +3,7 @@ import { jwt } from 'hono/jwt'; import { env } from '~/configs/env.config'; import { JWTPayloadSchema } from '~/types/auth.type'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const defaultHook: Hook = (result, c) => { if (!result.success) { return c.json({ errors: result.error.flatten() }, 400); From 6152000452cb7f44e9863e2153865aec5628a0d5 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Mon, 16 Dec 2024 14:46:15 +0700 Subject: [PATCH 05/13] style: format drizzle.config --- drizzle.config.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/drizzle.config.ts b/drizzle.config.ts index e1fb958..2808107 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,11 +1,11 @@ import { defineConfig } from 'drizzle-kit'; export default defineConfig({ - dialect: 'postgresql', - schema: './src/db/schema', - out: './drizzle', - dbCredentials: { - // biome-ignore lint/style/noNonNullAssertion: - url: process.env.DATABASE_URL!, - }, + dialect: 'postgresql', + schema: './src/db/schema', + out: './drizzle', + dbCredentials: { + // biome-ignore lint/style/noNonNullAssertion: + url: process.env.DATABASE_URL!, + }, }); From b9715b58ee88ff79db0abe9c6ea46644fdffdcaa Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:16:39 +0700 Subject: [PATCH 06/13] chore: init workflows --- .github/workflows/codecheck.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/codecheck.yml diff --git a/.github/workflows/codecheck.yml b/.github/workflows/codecheck.yml new file mode 100644 index 0000000..270a53c --- /dev/null +++ b/.github/workflows/codecheck.yml @@ -0,0 +1,30 @@ +name: codecheck + +on: + pull_request: + branches: + - develop + types: [opened, synchronize] + +jobs: + code-check: + name: Run eslint and typescript check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Setup bun + uses: bun-action/setup@v3 + with: + version: 0.9.0 + run_install: true + + - name: Run code check + run: bun check \ No newline at end of file From 94067f9f013bce8ec963fc96b7ab6e367c8a0f97 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:22:02 +0700 Subject: [PATCH 07/13] fix: fix codecheck action --- .github/workflows/codecheck.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codecheck.yml b/.github/workflows/codecheck.yml index 270a53c..1b83ca3 100644 --- a/.github/workflows/codecheck.yml +++ b/.github/workflows/codecheck.yml @@ -13,18 +13,15 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - - name: Install Node.js - uses: actions/setup-node@v4 + - name: Setup Bun + uses: bun-action/setup@v1 with: - node-version: 18 + bun-version: "latest" # Specify the version (e.g., "0.7.0") or use "latest" - - name: Setup bun - uses: bun-action/setup@v3 - with: - version: 0.9.0 - run_install: true + - name: Install dependencies + run: bun install - name: Run code check run: bun check \ No newline at end of file From 22132caf65ef9923596b6dade73380fa91b1ec4d Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:26:48 +0700 Subject: [PATCH 08/13] fix: change bun uses --- .github/workflows/codecheck.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codecheck.yml b/.github/workflows/codecheck.yml index 1b83ca3..e324438 100644 --- a/.github/workflows/codecheck.yml +++ b/.github/workflows/codecheck.yml @@ -16,12 +16,10 @@ jobs: uses: actions/checkout@v3 - name: Setup Bun - uses: bun-action/setup@v1 - with: - bun-version: "latest" # Specify the version (e.g., "0.7.0") or use "latest" + uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install - name: Run code check - run: bun check \ No newline at end of file + run: bun check From 7219bf6036e33304ac79396d368d3080c5b3b88d Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:29:24 +0700 Subject: [PATCH 09/13] refactor: format code --- src/controllers/competition.controller.ts | 44 ++++++------ src/controllers/team.controller.ts | 50 +++++++------- src/repositories/competition.repository.ts | 56 ++++++++-------- src/repositories/team.repository.ts | 16 ++--- src/routes/competition.route.ts | 48 ++++++------- src/routes/team.route.ts | 78 +++++++++++----------- src/types/competition.type.ts | 58 ++++++++-------- src/types/team.type.ts | 8 +-- 8 files changed, 180 insertions(+), 178 deletions(-) diff --git a/src/controllers/competition.controller.ts b/src/controllers/competition.controller.ts index 6b0d8ac..166fb9d 100644 --- a/src/controllers/competition.controller.ts +++ b/src/controllers/competition.controller.ts @@ -2,42 +2,42 @@ import { db } from '~/db/drizzle'; import { createAuthRouter } from '~/utils/router-factory'; import { roleMiddleware } from '~/middlewares/role-access.middleware'; import { - getAnnouncementsByCompetitionId, - getCompetition, - getCompetitionParticipant, - postAnnouncement, + getAnnouncementsByCompetitionId, + getCompetition, + getCompetitionParticipant, + postAnnouncement, } from '~/repositories/competition.repository'; import { - getAdminCompAnnouncementRoute, - getCompetitionParticipantRoute, - postAdminCompAnnouncementRoute, + getAdminCompAnnouncementRoute, + getCompetitionParticipantRoute, + postAdminCompAnnouncementRoute, } from '~/routes/competition.route'; export const competitionProtectedRouter = createAuthRouter(); competitionProtectedRouter.get( - getCompetitionParticipantRoute.getRoutingPath(), - roleMiddleware('admin'), + getCompetitionParticipantRoute.getRoutingPath(), + roleMiddleware('admin'), ); competitionProtectedRouter.openapi( - getCompetitionParticipantRoute, - async (c) => { - const { page, limit } = c.req.valid('query'); - const { competitionId } = c.req.valid('param'); + getCompetitionParticipantRoute, + async (c) => { + const { page, limit } = c.req.valid('query'); + const { competitionId } = c.req.valid('param'); - const competitionParticipant = await getCompetitionParticipant( - db, - competitionId, - { page: Number(page), limit: Number(limit) }, - ); - return c.json(competitionParticipant, 200); - }, + const competitionParticipant = await getCompetitionParticipant( + db, + competitionId, + { page: Number(page), limit: Number(limit) }, + ); + return c.json(competitionParticipant, 200); + }, ); competitionProtectedRouter.get( - getAdminCompAnnouncementRoute.getRoutingPath(), - roleMiddleware('admin'), + getAdminCompAnnouncementRoute.getRoutingPath(), + roleMiddleware('admin'), ); competitionProtectedRouter.openapi(getAdminCompAnnouncementRoute, async (c) => { const { competitionId } = c.req.valid('param'); diff --git a/src/controllers/team.controller.ts b/src/controllers/team.controller.ts index d99bee0..685ab48 100644 --- a/src/controllers/team.controller.ts +++ b/src/controllers/team.controller.ts @@ -1,23 +1,25 @@ import { db } from '~/db/drizzle'; import { roleMiddleware } from '~/middlewares/role-access.middleware'; import { - changeTeamName, - createTeam, - deleteTeamMember, - getTeamById, - getTeamsByCompetitionId, - insertUserToTeam, - updateTeamDocument, - updateTeamVerification, + changeTeamName, + createTeam, + deleteTeam, + deleteTeamMember, + getTeamById, + getTeamsByCompetitionId, + insertUserToTeam, + updateTeamDocument, + updateTeamVerification, } from '~/repositories/team.repository'; import { - deleteTeamMemberRoute, - getTeamCompetitionRoute, - getTeamDetailRoute, - postCreateTeamRoute, - postTeamDocumentRoute, - postTeamVerificationRoute, - putChangeTeamNameRoute, + deleteTeamMemberRoute, + getTeamCompetitionRoute, + getTeamDetailRoute, + postCreateTeamRoute, + postQuitTeamRoute, + postTeamDocumentRoute, + postTeamVerificationRoute, + putChangeTeamNameRoute, } from '~/routes/team.route'; import { createAuthRouter } from '~/utils/router-factory'; @@ -174,15 +176,15 @@ teamProtectedRouter.openapi(postTeamDocumentRoute, async (c) => { }); teamProtectedRouter.openapi(getTeamCompetitionRoute, async (c) => { - const { competitionId } = c.req.valid('param'); - const teams = await getTeamsByCompetitionId(db, competitionId); - if(!teams) return c.json({ error: "Competition doesn't exist!" }, 400); - return c.json(teams, 200); + const { competitionId } = c.req.valid('param'); + const teams = await getTeamsByCompetitionId(db, competitionId); + if (!teams) return c.json({ error: "Competition doesn't exist!" }, 400); + return c.json(teams, 200); }); teamProtectedRouter.openapi(getTeamDetailRoute, async (c) => { - const { competitionId, teamId } = c.req.valid('param'); - const team = await getTeamById(db, teamId, { teamMember: true }); - if (!team) return c.json({ error: "Team doesn't exist!" }, 400); - return c.json(team, 200); -}); \ No newline at end of file + const { teamId } = c.req.valid('param'); + const team = await getTeamById(db, teamId, { teamMember: true }); + if (!team) return c.json({ error: "Team doesn't exist!" }, 400); + return c.json(team, 200); +}); diff --git a/src/repositories/competition.repository.ts b/src/repositories/competition.repository.ts index 339c6d6..fd2de43 100644 --- a/src/repositories/competition.repository.ts +++ b/src/repositories/competition.repository.ts @@ -16,39 +16,39 @@ export const getCompetitionParticipantNumber = async ( }; export const getCompetitionParticipant = async ( - db: Database, - competitionId: string, - options: { page: number; limit: number }, + db: Database, + competitionId: string, + options: { page: number; limit: number }, ) => { - const { page, limit } = options; - const offset = (page - 1) * limit; + const { page, limit } = options; + const offset = (page - 1) * limit; - const result = await db.query.team.findMany({ - where: eq(team.competitionId, competitionId), - limit, - offset, - }); + const result = await db.query.team.findMany({ + where: eq(team.competitionId, competitionId), + limit, + offset, + }); - const totalItems = ( - await db.query.team.findMany({ - where: eq(team.competitionId, competitionId), - }) - ).length; + const totalItems = ( + await db.query.team.findMany({ + where: eq(team.competitionId, competitionId), + }) + ).length; - const totalPages = Math.ceil(totalItems / limit); - const next = page < totalPages ? `?page=${page + 1}&limit=${limit}` : null; - const prev = page > 1 ? `?page=${page - 1}&limit=${limit}` : null; + const totalPages = Math.ceil(totalItems / limit); + const next = page < totalPages ? `?page=${page + 1}&limit=${limit}` : null; + const prev = page > 1 ? `?page=${page - 1}&limit=${limit}` : null; - return { - pagination: { - currentPage: page, - totalItems, - totalPages, - next, - prev, - }, - result, - }; + return { + pagination: { + currentPage: page, + totalItems, + totalPages, + next, + prev, + }, + result, + }; }; export const getCompetitionById = async ( diff --git a/src/repositories/team.repository.ts b/src/repositories/team.repository.ts index a940f02..137ae20 100644 --- a/src/repositories/team.repository.ts +++ b/src/repositories/team.repository.ts @@ -192,12 +192,12 @@ export const updateTeamVerification = async ( export const getTeamsByCompetitionId = async ( db: Database, - competitionId: string + competitionId: string, ) => { - return await db.query.team.findMany({ - where: eq(team.competitionId, competitionId), - with: { - teamMembers: true, - } - }); -} \ No newline at end of file + return await db.query.team.findMany({ + where: eq(team.competitionId, competitionId), + with: { + teamMembers: true, + }, + }); +}; diff --git a/src/routes/competition.route.ts b/src/routes/competition.route.ts index 3556903..36bb1ed 100644 --- a/src/routes/competition.route.ts +++ b/src/routes/competition.route.ts @@ -1,9 +1,9 @@ import { createRoute } from '@hono/zod-openapi'; import { - AnnouncementSchema, - CompetitionParticipantSchema, - GetCompetitionTimeQuerySchema, - PostCompAnnouncementBodySchema, + AnnouncementSchema, + CompetitionParticipantSchema, + GetCompetitionTimeQuerySchema, + PostCompAnnouncementBodySchema, } from '~/types/competition.type'; import { AllAnnouncementSchema } from '~/types/competition.type'; import { CompetitionIdParam } from '~/types/competition.type'; @@ -32,26 +32,26 @@ export const getAdminCompAnnouncementRoute = createRoute({ }); export const getCompetitionParticipantRoute = createRoute({ - operationId: 'getCompetitionParticipant', - tags: ['team', 'admin', 'competition'], - method: 'get', - path: '/admin/{competitionId}/team', - request: { - params: CompetitionIdParam, - query: GetCompetitionTimeQuerySchema, - }, - responses: { - 200: { - description: "Fetched competition's participant.", - content: { - 'application/json': { - schema: CompetitionParticipantSchema, - }, - }, - }, - 400: createErrorResponse('UNION', 'Bad request error'), - 500: createErrorResponse('GENERIC', 'Internal server error'), - }, + operationId: 'getCompetitionParticipant', + tags: ['team', 'admin', 'competition'], + method: 'get', + path: '/admin/{competitionId}/team', + request: { + params: CompetitionIdParam, + query: GetCompetitionTimeQuerySchema, + }, + responses: { + 200: { + description: "Fetched competition's participant.", + content: { + 'application/json': { + schema: CompetitionParticipantSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, }); export const postAdminCompAnnouncementRoute = createRoute({ diff --git a/src/routes/team.route.ts b/src/routes/team.route.ts index cf73376..3d3fa9d 100644 --- a/src/routes/team.route.ts +++ b/src/routes/team.route.ts @@ -1,17 +1,17 @@ import { createRoute } from '@hono/zod-openapi'; import { TeamMemberSchema } from '~/types/team-member.type'; import { - CompetitionAndTeamIdParam, - CompetitionIdParam, - PostTeamBodySchema, - PostTeamDocumentBodySchema, - PostTeamVerificationBodySchema, - TeamCompetitionDetailSchema, - TeamCompetitionSchema, - TeamIdParam, - TeamMemberIdSchema, - TeamSchema, - putChangeTeamNameBodySchema, + CompetitionAndTeamIdParam, + CompetitionIdParam, + PostTeamBodySchema, + PostTeamDocumentBodySchema, + PostTeamVerificationBodySchema, + TeamCompetitionDetailSchema, + TeamCompetitionSchema, + TeamIdParam, + TeamMemberIdSchema, + TeamSchema, + putChangeTeamNameBodySchema, } from '~/types/team.type'; import { createErrorResponse } from '~/utils/error-response-factory'; @@ -205,45 +205,45 @@ export const postTeamVerificationRoute = createRoute({ }); export const getTeamCompetitionRoute = createRoute({ - operationId: "getTeamCompetition", - tags: ["team", "admin"], - method: "get", - path: "/admin/{competitionId}/team", + operationId: 'getTeamCompetition', + tags: ['team', 'admin'], + method: 'get', + path: '/admin/{competitionId}/team', request: { params: CompetitionIdParam, }, responses: { 200: { - description: "Successfully get team competition", + description: 'Successfully get team competition', content: { - "application/json": { + 'application/json': { schema: TeamCompetitionSchema, }, }, }, - 400: createErrorResponse("UNION", "Bad request error"), - 500: createErrorResponse("GENERIC", "Internal server error"), + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), }, }); export const getTeamDetailRoute = createRoute({ - operationId: "getTeamDetail", - tags: ["team", "admin"], - method: "get", - path: "/admin/{competitionId}/team/{teamId}", - request: { - params: CompetitionAndTeamIdParam, - }, - responses: { - 200: { - description: "Successfully get team detail", - content: { - "application/json": { - schema: TeamCompetitionDetailSchema, - }, - }, - }, - 400: createErrorResponse("UNION", "Bad request error"), - 500: createErrorResponse("GENERIC", "Internal server error"), - }, -}) \ No newline at end of file + operationId: 'getTeamDetail', + tags: ['team', 'admin'], + method: 'get', + path: '/admin/{competitionId}/team/{teamId}', + request: { + params: CompetitionAndTeamIdParam, + }, + responses: { + 200: { + description: 'Successfully get team detail', + content: { + 'application/json': { + schema: TeamCompetitionDetailSchema, + }, + }, + }, + 400: createErrorResponse('UNION', 'Bad request error'), + 500: createErrorResponse('GENERIC', 'Internal server error'), + }, +}); diff --git a/src/types/competition.type.ts b/src/types/competition.type.ts index bd215aa..2c1d9b1 100644 --- a/src/types/competition.type.ts +++ b/src/types/competition.type.ts @@ -24,35 +24,35 @@ export const PostCompAnnouncementBodySchema = z.object({ }); export const CompetitionParticipantSchema = z - .object({ - pagination: z.object({ - currentPage: z.number(), - totalItems: z.number(), - totalPages: z.number(), - next: z.string().url().nullable(), - prev: z.string().url().nullable(), - }), - result: z.array(TeamSchema), - }) - .openapi('CompetitionParticipant'); + .object({ + pagination: z.object({ + currentPage: z.number(), + totalItems: z.number(), + totalPages: z.number(), + next: z.string().url().nullable(), + prev: z.string().url().nullable(), + }), + result: z.array(TeamSchema), + }) + .openapi('CompetitionParticipant'); export const GetCompetitionTimeQuerySchema = z.object({ - page: z - .string() - .default('1') - .openapi({ - param: { - in: 'query', - required: false, - }, - }), - limit: z - .string() - .default('10') - .openapi({ - param: { - in: 'query', - required: false, - }, - }), + page: z + .string() + .default('1') + .openapi({ + param: { + in: 'query', + required: false, + }, + }), + limit: z + .string() + .default('10') + .openapi({ + param: { + in: 'query', + required: false, + }, + }), }); diff --git a/src/types/team.type.ts b/src/types/team.type.ts index f7b75d5..9d083f2 100644 --- a/src/types/team.type.ts +++ b/src/types/team.type.ts @@ -47,7 +47,7 @@ export const PostTeamBodySchema = createInsertSchema(team).pick({ export const CompetitionIdParam = z.object({ competitionId: z.string().openapi({ param: { - in: "path", + in: 'path', required: true, }, }), @@ -56,9 +56,9 @@ export const CompetitionIdParam = z.object({ export const TeamCompetitionSchema = z.array( TeamSchema.extend({ members: z.array(TeamMemberSchema), - }) + }), ); export const TeamCompetitionDetailSchema = TeamSchema.extend({ - members: z.array(TeamMemberSchema), -}); \ No newline at end of file + members: z.array(TeamMemberSchema), +}); From e8775845cd9c50dc38483b5e8f6dc600f3b8a8c0 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:33:07 +0700 Subject: [PATCH 10/13] chore: init husky --- .husky/pre-commit | 2 ++ bun.lockb | Bin 185077 -> 185407 bytes package.json | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..dd04663 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +bun i +bun check diff --git a/bun.lockb b/bun.lockb index fd9d4d82761c4143f8fe6defd83d3dfeaf71f894..c02146b25b5ae6204c60823e8ac1f3fef541c5dc 100755 GIT binary patch delta 29777 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 Qptg6@LJIHo(SFVSKcBhRvwzOK> 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 KJM~hpP5nPO1Nn*o diff --git a/package.json b/package.json index 4a24646..796a702 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "db:migrate": "drizzle-kit migrate && bun run src/db/migrate-trigger.ts", "db:studio": "drizzle-kit studio", "check": "bun run typecheck && bun run codecheck", - "codecheck": "eslint src --ext .ts", - "typecheck": "tsc --noEmit", - "format": "eslint src --ext .ts --fix" + "codecheck": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "format": "eslint src --ext .ts --fix", + "prepare": "husky" }, "dependencies": { "@hono/zod-openapi": "^0.18.3", @@ -20,6 +21,7 @@ "drizzle-orm": "^0.36.0", "drizzle-zod": "^0.5.1", "hono": "^4.6.12", + "husky": "^9.1.7", "minio": "^8.0.2", "nodemailer": "^6.9.16", "postgres": "^3.4.5", @@ -34,12 +36,12 @@ "drizzle-dbml-generator": "^0.10.0", "drizzle-kit": "^0.27.1", "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-promise": "^6.0.0", - "prettier": "^3.2.5", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-promise": "^6.0.0", + "prettier": "^3.2.5", "tsx": "^4.19.2", "typescript": "^5.7.2", "typescript-eslint": "^8.18.0" From 6c992f94ba1556f15c4f05a1283a089d4fee9405 Mon Sep 17 00:00:00 2001 From: Valentino Triadi <13522164@std.stei.itb.ac.id> Date: Thu, 19 Dec 2024 16:40:53 +0700 Subject: [PATCH 11/13] fix: fix husky pre commit script --- .husky/pre-commit | 1 + bun.lockb | Bin 185407 -> 185407 bytes 2 files changed, 1 insertion(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index dd04663..72001b5 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ bun i bun check +docker build -t arkav-be-9.0 . \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index c02146b25b5ae6204c60823e8ac1f3fef541c5dc..c6f5e20794a96a4c696d0f643f15eb1000839ecf 100755 GIT binary patch delta 214 zcmdnLf_wi8?g@Gd5?uOh-)0-Bb2pg0UwHcA`{;8&A}5DCA3GFuV5Q9AkoApzTm2Y? zia0Y$i?b^kfMBwqy#Drt97YY60V6*IF&Z##7dXu*E62_lXQ^kXXTII!B_sE10AW)^ A*8l(j delta 208 zcmdnLf_wi8?g@Gd;@=-s6(0A!!u5^i|BdZ}>ryn8IhbU(AAEGW`c9e1A)!XUt$vI` sMUyAU3QU%e*Vvwr!>GYBVC07&Mgzv}0;d^e<=B}Rz+k({OGfV305>K=TmS$7 From 07b9c2c760edab013cd992aff82049070fabfedd Mon Sep 17 00:00:00 2001 From: Fawwaz Abrial Saffa <18221067@std.stei.itb.ac.id> Date: Sat, 21 Dec 2024 16:34:38 +0700 Subject: [PATCH 12/13] chore: add import organizer --- .eslintrc.js | 4 +- .husky/pre-commit | 3 +- .prettierrc | 8 +- README.md | 5 ++ bun.lockb | Bin 185407 -> 193555 bytes package.json | 98 +++++++++++---------- src/controllers/api.controller.ts | 3 +- src/controllers/auth.controller.ts | 34 +++---- src/controllers/competition.controller.ts | 2 +- src/db/dbml.ts | 3 +- src/db/drizzle.ts | 1 + src/db/migrate-trigger.ts | 2 +- src/db/schema/auth.schema.ts | 1 + src/db/schema/competition.schema.ts | 3 +- src/db/schema/media.schema.ts | 1 + src/db/schema/team-member.schema.ts | 1 + src/db/schema/team.schema.ts | 1 + src/db/schema/user.schema.ts | 1 + src/index.ts | 1 + src/repositories/competition.repository.ts | 7 +- src/repositories/team-member.repository.ts | 1 + src/repositories/team.repository.ts | 1 + src/routes/auth.route.ts | 1 + src/routes/health.route.ts | 1 + src/routes/media.route.ts | 1 + src/types/auth.type.ts | 1 + src/types/competition.type.ts | 1 + src/types/team-member.type.ts | 1 + src/types/team.type.ts | 1 + src/utils/error-response-factory.ts | 1 + tsconfig.json | 32 +++---- 31 files changed, 128 insertions(+), 93 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 48b409f..945a9b9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', - ], + ], ignorePatterns: ['.eslintrc.js', 'node_modules/', 'drizzle/'], overrides: [ { @@ -29,4 +29,4 @@ module.exports = { 'no-console': 'off', 'prettier/prettier': 'error', }, -}; \ No newline at end of file +}; diff --git a/.husky/pre-commit b/.husky/pre-commit index 72001b5..8194b86 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,2 @@ bun i -bun check -docker build -t arkav-be-9.0 . \ No newline at end of file +bun check:write \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index af218d8..3201eb8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,9 @@ "semi": true, "tabWidth": 2, "trailingComma": "all", - "endOfLine": "auto" -} \ No newline at end of file + "endOfLine": "auto", + "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] +} diff --git a/README.md b/README.md index d9ce9df..0f83802 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Arkavidia 9.0 Backend ## Welcome! + Please refer ke [Guidebook IT](https://docs.google.com/document/d/1e0YsDlhmFLVOkYFQUWyq5eJnh_5tSmRwkGSmqW-j36I/edit?tab=t.3segyhsw1vo3#heading=h.tq38eiq9xfrt) untuk petunjuk cara kontribusi ke repository ini. ## Tech Stack + - Bun as Javascript runtime - Hono as API framework - Drizzle as Object Relational Mapper @@ -12,12 +14,15 @@ Please refer ke [Guidebook IT](https://docs.google.com/document/d/1e0YsDlhmFLVOk - Minio as object storage ## How To Run + To install dependencies: + ```sh bun install ``` To run: + ```sh bun run dev ``` diff --git a/bun.lockb b/bun.lockb index c6f5e20794a96a4c696d0f643f15eb1000839ecf..a32664c535fb410c72685dbbe34abd79b6c4846e 100755 GIT binary patch delta 39983 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 eLGNVI#Fp}Y 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 diff --git a/package.json b/package.json index 796a702..b1cdb12 100644 --- a/package.json +++ b/package.json @@ -1,49 +1,53 @@ { - "name": "backend", - "scripts": { - "dev": "bun run --hot src/index.ts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate && bun run src/db/migrate-trigger.ts", - "db:studio": "drizzle-kit studio", - "check": "bun run typecheck && bun run codecheck", - "codecheck": "eslint src --ext .ts", - "typecheck": "tsc --noEmit", - "format": "eslint src --ext .ts --fix", - "prepare": "husky" - }, - "dependencies": { - "@hono/zod-openapi": "^0.18.3", - "@paralleldrive/cuid2": "^2.2.2", - "@scalar/hono-api-reference": "^0.5.162", - "@types/nodemailer": "^6.4.17", - "argon2": "^0.41.1", - "dotenv": "^16.4.5", - "drizzle-orm": "^0.36.0", - "drizzle-zod": "^0.5.1", - "hono": "^4.6.12", - "husky": "^9.1.7", - "minio": "^8.0.2", - "nodemailer": "^6.9.16", - "postgres": "^3.4.5", - "zod": "^3.23.8" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@softwaretechnik/dbml-renderer": "^1.0.30", - "@types/bun": "latest", - "@types/pg": "^8.11.10", - "@typescript-eslint/eslint-plugin": "^6.4.0", - "drizzle-dbml-generator": "^0.10.0", - "drizzle-kit": "^0.27.1", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-promise": "^6.0.0", - "prettier": "^3.2.5", - "tsx": "^4.19.2", - "typescript": "^5.7.2", - "typescript-eslint": "^8.18.0" - } + "name": "backend", + "scripts": { + "dev": "bun run --hot src/index.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate && bun run src/db/migrate-trigger.ts", + "db:studio": "drizzle-kit studio", + "check": "bun run typecheck && bun run codecheck", + "check:write": "bun run format && bun run pretty", + "codecheck": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "format": "eslint src --ext .ts --fix", + "pretty": "prettier . --write", + "prepare": "husky" + }, + "dependencies": { + "@hono/zod-openapi": "^0.18.3", + "@paralleldrive/cuid2": "^2.2.2", + "@scalar/hono-api-reference": "^0.5.162", + "@trivago/prettier-plugin-sort-imports": "^5.2.0", + "@types/nodemailer": "^6.4.17", + "argon2": "^0.41.1", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.36.0", + "drizzle-zod": "^0.5.1", + "hono": "^4.6.12", + "husky": "^9.1.7", + "install": "^0.13.0", + "minio": "^8.0.2", + "nodemailer": "^6.9.16", + "postgres": "^3.4.5", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@softwaretechnik/dbml-renderer": "^1.0.30", + "@types/bun": "latest", + "@types/pg": "^8.11.10", + "@typescript-eslint/eslint-plugin": "^6.4.0", + "drizzle-dbml-generator": "^0.10.0", + "drizzle-kit": "^0.27.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-promise": "^6.0.0", + "prettier": "^3.2.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" + } } diff --git a/src/controllers/api.controller.ts b/src/controllers/api.controller.ts index c8dfec7..6c63ba1 100644 --- a/src/controllers/api.controller.ts +++ b/src/controllers/api.controller.ts @@ -1,11 +1,12 @@ import { OpenAPIHono } from '@hono/zod-openapi'; + import { authProtectedRouter, authRouter } from './auth.controller'; +import { competitionProtectedRouter } from './competition.controller'; import { healthRouter } from './health.controller'; import { mediaRouter } from './media.controller'; import { teamMemberProtectedRouter } from './team-member.controller'; import { teamProtectedRouter } from './team.controller'; import { userProtectedRouter } from './user.controller'; -import { competitionProtectedRouter } from './competition.controller'; const unprotectedApiRouter = new OpenAPIHono(); unprotectedApiRouter.route('/', healthRouter); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 604d2f9..7c14317 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,4 +1,5 @@ import * as argon2 from 'argon2'; +import type { Context } from 'hono'; import { deleteCookie, setCookie } from 'hono/cookie'; import * as jwt from 'hono/jwt'; import { env } from '~/configs/env.config'; @@ -7,31 +8,31 @@ import type { UserIdentity } from '~/db/schema/auth.schema'; import type { User } from '~/db/schema/user.schema'; import { sendVerificationEmail } from '~/lib/nodemailer'; import { - createUserIdentity, - findUserIdentityByEmail, - findUserIdentityById, - updateUserIdentity, - updateUserVerification, -} from '~/repositories/auth.repository'; -import { + updateUser, findUserByEmail, findUserById, - updateUser, } from '~/repositories/user.repository'; import { - basicLoginRoute, - basicRegisterRoute, basicVerifyAccountRoute, + basicRegisterRoute, + basicLoginRoute, + refreshRoute, googleAuthCallbackRoute, - googleAuthRoute, logoutRoute, - refreshRoute, + googleAuthRoute, selfRoute, } from '~/routes/auth.route'; +import { + createUserIdentity, + findUserIdentityByEmail, + findUserIdentityById, + updateUserIdentity, + updateUserVerification, +} from '~/repositories/auth.repository'; import { GoogleTokenDataSchema, GoogleUserSchema } from '~/types/auth.type'; import { UserSchema } from '~/types/user.type'; + import { createAuthRouter, createRouter } from '../utils/router-factory'; -import type { Context } from 'hono'; const VERIFICATION_TOKEN_EXPIRATION_TIME = 360000; // TTL 1 hour @@ -39,9 +40,12 @@ export const authRouter = createRouter(); export const authProtectedRouter = createAuthRouter(); const generateAccessToken = async (user: User, userIdentity: UserIdentity) => { + + + + const payload = { - ...user, - provider: userIdentity.provider, + ...user, provider: userIdentity.provider, exp: Math.floor(Date.now() / 1000) + env.ACCESS_TOKEN_EXPIRATION, }; const token = await jwt.sign(payload, env.ACCESS_TOKEN_SECRET); diff --git a/src/controllers/competition.controller.ts b/src/controllers/competition.controller.ts index 166fb9d..441422a 100644 --- a/src/controllers/competition.controller.ts +++ b/src/controllers/competition.controller.ts @@ -1,5 +1,4 @@ import { db } from '~/db/drizzle'; -import { createAuthRouter } from '~/utils/router-factory'; import { roleMiddleware } from '~/middlewares/role-access.middleware'; import { getAnnouncementsByCompetitionId, @@ -12,6 +11,7 @@ import { getCompetitionParticipantRoute, postAdminCompAnnouncementRoute, } from '~/routes/competition.route'; +import { createAuthRouter } from '~/utils/router-factory'; export const competitionProtectedRouter = createAuthRouter(); diff --git a/src/db/dbml.ts b/src/db/dbml.ts index 9a0ad0c..80333f7 100644 --- a/src/db/dbml.ts +++ b/src/db/dbml.ts @@ -1,7 +1,6 @@ /** Ini buat bikin database schema diagram */ - -import * as schema from '~/db/schema'; import { pgGenerate } from 'drizzle-dbml-generator'; +import * as schema from '~/db/schema'; const out = './docs/schema.dbml'; const relational = true; diff --git a/src/db/drizzle.ts b/src/db/drizzle.ts index cdaff39..c6939a3 100644 --- a/src/db/drizzle.ts +++ b/src/db/drizzle.ts @@ -1,6 +1,7 @@ import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import * as schema from '~/db/schema'; + import { env } from '../configs/env.config'; const client = postgres(env.DATABASE_URL); diff --git a/src/db/migrate-trigger.ts b/src/db/migrate-trigger.ts index a3c5d63..92908ba 100644 --- a/src/db/migrate-trigger.ts +++ b/src/db/migrate-trigger.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; -import postgres from 'postgres'; import path from 'node:path'; +import postgres from 'postgres'; if (!process.env.DATABASE_URL) { throw new Error('DATABASE_URL is required'); diff --git a/src/db/schema/auth.schema.ts b/src/db/schema/auth.schema.ts index ec39949..88f8431 100644 --- a/src/db/schema/auth.schema.ts +++ b/src/db/schema/auth.schema.ts @@ -4,6 +4,7 @@ import { relations, } from 'drizzle-orm'; import { boolean, pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + import { createId, getNow } from '../../utils/drizzle-schema-util'; import { user } from './user.schema'; diff --git a/src/db/schema/competition.schema.ts b/src/db/schema/competition.schema.ts index 7436dd7..bf943d6 100644 --- a/src/db/schema/competition.schema.ts +++ b/src/db/schema/competition.schema.ts @@ -8,10 +8,11 @@ import { text, timestamp, } from 'drizzle-orm/pg-core'; + import { createId, getNow } from '../../utils/drizzle-schema-util'; +import { media } from './media.schema'; import { team } from './team.schema'; import { user } from './user.schema'; -import { media } from './media.schema'; /** Main Compeitition Table */ export const competition = pgTable('competition', { diff --git a/src/db/schema/media.schema.ts b/src/db/schema/media.schema.ts index 4d23ce9..40d6488 100644 --- a/src/db/schema/media.schema.ts +++ b/src/db/schema/media.schema.ts @@ -1,4 +1,5 @@ import { pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + import { createId } from '../../utils/drizzle-schema-util'; import { user } from './user.schema'; diff --git a/src/db/schema/team-member.schema.ts b/src/db/schema/team-member.schema.ts index 5e971d2..ab97ea1 100644 --- a/src/db/schema/team-member.schema.ts +++ b/src/db/schema/team-member.schema.ts @@ -1,5 +1,6 @@ import { relations } from 'drizzle-orm'; import { boolean, pgEnum, pgTable, text } from 'drizzle-orm/pg-core'; + import { media } from './media.schema'; import { team } from './team.schema'; import { user } from './user.schema'; diff --git a/src/db/schema/team.schema.ts b/src/db/schema/team.schema.ts index ecaad86..7d76731 100644 --- a/src/db/schema/team.schema.ts +++ b/src/db/schema/team.schema.ts @@ -1,5 +1,6 @@ import { relations } from 'drizzle-orm'; import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + import { createId, getNow } from '../../utils/drizzle-schema-util'; import { competition, competitionSubmission } from './competition.schema'; import { media } from './media.schema'; diff --git a/src/db/schema/user.schema.ts b/src/db/schema/user.schema.ts index b4a52ab..f2dc414 100644 --- a/src/db/schema/user.schema.ts +++ b/src/db/schema/user.schema.ts @@ -7,6 +7,7 @@ import { text, timestamp, } from 'drizzle-orm/pg-core'; + import { getNow } from '../../utils/drizzle-schema-util'; import { userIdentity } from './auth.schema'; import { teamMember } from './team-member.schema'; diff --git a/src/index.ts b/src/index.ts index a89d949..3b5fc8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { OpenAPIHono } from '@hono/zod-openapi'; import { apiReference } from '@scalar/hono-api-reference'; import { serve } from 'bun'; import { cors } from 'hono/cors'; + import { env } from './configs/env.config'; import { apiRouter } from './controllers/api.controller'; diff --git a/src/repositories/competition.repository.ts b/src/repositories/competition.repository.ts index fd2de43..3ac77a5 100644 --- a/src/repositories/competition.repository.ts +++ b/src/repositories/competition.repository.ts @@ -1,9 +1,10 @@ import { eq } from 'drizzle-orm'; -import type { Database } from '../db/drizzle'; -import { competition, competitionAnnouncement, team } from '../db/schema'; -import type { PostCompAnnouncementBodySchema } from '~/types/competition.type'; import type { z } from 'zod'; import { first } from '~/db/helper'; +import type { PostCompAnnouncementBodySchema } from '~/types/competition.type'; + +import type { Database } from '../db/drizzle'; +import { competition, competitionAnnouncement, team } from '../db/schema'; export const getCompetitionParticipantNumber = async ( db: Database, diff --git a/src/repositories/team-member.repository.ts b/src/repositories/team-member.repository.ts index ef79317..208dd6e 100644 --- a/src/repositories/team-member.repository.ts +++ b/src/repositories/team-member.repository.ts @@ -7,6 +7,7 @@ import type { PostTeamMemberDocumentBodySchema, PostTeamMemberVerificationBodySchema, } from '~/types/team-member.type'; + import { getCompetitionById } from './competition.repository'; import { insertMediaFromUrl } from './media.repository'; import { getTeamById } from './team.repository'; diff --git a/src/repositories/team.repository.ts b/src/repositories/team.repository.ts index 137ae20..014ad72 100644 --- a/src/repositories/team.repository.ts +++ b/src/repositories/team.repository.ts @@ -8,6 +8,7 @@ import type { PostTeamVerificationBodySchema, putChangeTeamNameBodySchema, } from '~/types/team.type'; + import { getCompetitionById, getCompetitionParticipantNumber, diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts index ddf215f..198c140 100644 --- a/src/routes/auth.route.ts +++ b/src/routes/auth.route.ts @@ -8,6 +8,7 @@ import { RefreshTokenQuerySchema, } from '~/types/auth.type'; import { UserSchema } from '~/types/user.type'; + import { createErrorResponse } from '../utils/error-response-factory'; /** BASIC AUTHENTICATION ROUTES (Email & Password) */ diff --git a/src/routes/health.route.ts b/src/routes/health.route.ts index 232e680..423ca07 100644 --- a/src/routes/health.route.ts +++ b/src/routes/health.route.ts @@ -1,4 +1,5 @@ import { createRoute, z } from '@hono/zod-openapi'; + import { createErrorResponse } from '../utils/error-response-factory'; export const getHealthStatusRoute = createRoute({ diff --git a/src/routes/media.route.ts b/src/routes/media.route.ts index 7ac6af2..a87b6de 100644 --- a/src/routes/media.route.ts +++ b/src/routes/media.route.ts @@ -3,6 +3,7 @@ import { GetPresignedLinkQuerySchema, PresignedUrlSchema, } from '~/types/media.type'; + import { createErrorResponse } from '../utils/error-response-factory'; export const getPresignedLink = createRoute({ diff --git a/src/types/auth.type.ts b/src/types/auth.type.ts index 09c5315..5281c51 100644 --- a/src/types/auth.type.ts +++ b/src/types/auth.type.ts @@ -1,6 +1,7 @@ import { z } from '@hono/zod-openapi'; import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { userIdentity } from '~/db/schema/auth.schema'; + import { UserSchema } from './user.type'; export const UserIdentitySchema = createSelectSchema(userIdentity); diff --git a/src/types/competition.type.ts b/src/types/competition.type.ts index 2c1d9b1..25d142a 100644 --- a/src/types/competition.type.ts +++ b/src/types/competition.type.ts @@ -1,6 +1,7 @@ import { createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { competitionAnnouncement } from '~/db/schema'; + import { TeamSchema } from './team.type'; export const AnnouncementSchema = createSelectSchema(competitionAnnouncement, { diff --git a/src/types/team-member.type.ts b/src/types/team-member.type.ts index 1d6e6dd..505dd3a 100644 --- a/src/types/team-member.type.ts +++ b/src/types/team-member.type.ts @@ -1,6 +1,7 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { teamMember } from '~/db/schema/team-member.schema'; + import { MediaSchema } from './media.type'; export const TeamAndUserIdParam = z.object({ diff --git a/src/types/team.type.ts b/src/types/team.type.ts index 9d083f2..0b99186 100644 --- a/src/types/team.type.ts +++ b/src/types/team.type.ts @@ -1,6 +1,7 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { team } from '~/db/schema'; + import { TeamMemberSchema } from './team-member.type'; export const PostTeamDocumentBodySchema = createInsertSchema(team).pick({ diff --git a/src/utils/error-response-factory.ts b/src/utils/error-response-factory.ts index 2fb37b5..44e6b66 100644 --- a/src/utils/error-response-factory.ts +++ b/src/utils/error-response-factory.ts @@ -1,5 +1,6 @@ import type { ResponseConfig } from '@asteasolutions/zod-to-openapi/dist/openapi-registry.js'; import { z } from 'zod'; + import { GenericErrorShema, ValidationErrorSchema, diff --git a/tsconfig.json b/tsconfig.json index 6bd872b..16c97fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "types": ["bun"], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "noImplicitAny": true, - "paths": { - "~/*": ["./src/*"] - }, - "baseUrl": ".", - "skipLibCheck": true, - "experimentalDecorators": true - } + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "types": ["bun"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "noImplicitAny": true, + "paths": { + "~/*": ["./src/*"] + }, + "baseUrl": ".", + "skipLibCheck": true, + "experimentalDecorators": true + } } From 8769d52cd5deee0a31c88c3c2083e488336bbb73 Mon Sep 17 00:00:00 2001 From: Fawwaz Abrial Saffa <18221067@std.stei.itb.ac.id> Date: Sat, 21 Dec 2024 16:35:05 +0700 Subject: [PATCH 13/13] style: format & style --- bun.lockb | Bin 193555 -> 193555 bytes package.json | 4 ++-- src/controllers/auth.controller.ts | 31 +++++++++++++---------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/bun.lockb b/bun.lockb index a32664c535fb410c72685dbbe34abd79b6c4846e..6c04d371dfa98ce5d50969578f8d0976a8b88b5a 100755 GIT binary patch delta 39 vcmbRIf_w4{?uIRlfq&T<<4pC8^bDp){AE1J!5C+tXK1WvuwC;X { - - - - const payload = { - ...user, provider: userIdentity.provider, + ...user, + provider: userIdentity.provider, exp: Math.floor(Date.now() / 1000) + env.ACCESS_TOKEN_EXPIRATION, }; const token = await jwt.sign(payload, env.ACCESS_TOKEN_SECRET);