diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..945a9b9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,32 @@ +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', + 'prettier/prettier': 'error', + }, +}; diff --git a/.github/workflows/codecheck.yml b/.github/workflows/codecheck.yml new file mode 100644 index 0000000..e324438 --- /dev/null +++ b/.github/workflows/codecheck.yml @@ -0,0 +1,25 @@ +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@v3 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install + + - name: Run code check + run: bun check diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..8194b86 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +bun i +bun check:write \ No newline at end of file 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..3201eb8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "singleQuote": true, + "arrowParens": "always", + "semi": true, + "tabWidth": 2, + "trailingComma": "all", + "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/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/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index c8e8e18..6c04d37 Binary files a/bun.lockb and b/bun.lockb differ 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!, + }, }); diff --git a/package.json b/package.json index 22f7d55..fb0547d 100644 --- a/package.json +++ b/package.json @@ -1,39 +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": "bunx biome check src && bunx check:type", - "check:write": "bunx biome check --write", - "check:lint": "biome lint src", - "check:format": "biome format src", - "check:type": "tsc --noEmit" - }, - "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", - "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", - "drizzle-dbml-generator": "^0.10.0", - "drizzle-kit": "^0.27.1", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } + "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/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/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 e62f7a1..ddca3bc 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, + 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'; + import { createAuthRouter, createRouter } from '../utils/router-factory'; -import type { Context } from 'hono'; const VERIFICATION_TOKEN_EXPIRATION_TIME = 360000; // TTL 1 hour @@ -39,290 +40,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 05aa779..441422a 100644 --- a/src/controllers/competition.controller.ts +++ b/src/controllers/competition.controller.ts @@ -1,81 +1,81 @@ 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'; +import { createAuthRouter } from '~/utils/router-factory'; 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'); + 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/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/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/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/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..92908ba 100644 --- a/src/db/migrate-trigger.ts +++ b/src/db/migrate-trigger.ts @@ -1,46 +1,46 @@ 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'); + 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..88f8431 100644 --- a/src/db/schema/auth.schema.ts +++ b/src/db/schema/auth.schema.ts @@ -1,52 +1,53 @@ 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..bf943d6 100644 --- a/src/db/schema/competition.schema.ts +++ b/src/db/schema/competition.schema.ts @@ -1,126 +1,126 @@ -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 { 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', { - 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..40d6488 100644 --- a/src/db/schema/media.schema.ts +++ b/src/db/schema/media.schema.ts @@ -1,35 +1,22 @@ -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..ab97ea1 100644 --- a/src/db/schema/team-member.schema.ts +++ b/src/db/schema/team-member.schema.ts @@ -1,65 +1,64 @@ 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..7d76731 100644 --- a/src/db/schema/team.schema.ts +++ b/src/db/schema/team.schema.ts @@ -1,38 +1,39 @@ 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'; 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..f2dc414 100644 --- a/src/db/schema/user.schema.ts +++ b/src/db/schema/user.schema.ts @@ -1,50 +1,51 @@ 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..3b5fc8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,25 @@ 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 +27,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 0a9f072..3ac77a5 100644 --- a/src/repositories/competition.repository.ts +++ b/src/repositories/competition.repository.ts @@ -1,98 +1,99 @@ 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, - 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 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 ( - 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..208dd6e 100644 --- a/src/repositories/team-member.repository.ts +++ b/src/repositories/team-member.repository.ts @@ -4,127 +4,128 @@ 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 435a9ac..014ad72 100644 --- a/src/repositories/team.repository.ts +++ b/src/repositories/team.repository.ts @@ -6,9 +6,9 @@ import { team, teamMember } from '~/db/schema'; import type { PostTeamDocumentBodySchema, PostTeamVerificationBodySchema, - TeamMemberIdSchema, putChangeTeamNameBodySchema, } from '~/types/team.type'; + import { getCompetitionById, getCompetitionParticipantNumber, @@ -193,12 +193,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/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..198c140 100644 --- a/src/routes/auth.route.ts +++ b/src/routes/auth.route.ts @@ -1,188 +1,188 @@ -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 eaab15e..36bb1ed 100644 --- a/src/routes/competition.route.ts +++ b/src/routes/competition.route.ts @@ -1,85 +1,85 @@ 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'; 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 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({ - 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..423ca07 100644 --- a/src/routes/health.route.ts +++ b/src/routes/health.route.ts @@ -1,23 +1,24 @@ 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..a87b6de 100644 --- a/src/routes/media.route.ts +++ b/src/routes/media.route.ts @@ -1,28 +1,29 @@ 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 cdd0f79..3d3fa9d 100644 --- a/src/routes/team.route.ts +++ b/src/routes/team.route.ts @@ -1,249 +1,249 @@ 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'; 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'), + }, }); 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/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..5281c51 100644 --- a/src/types/auth.type.ts +++ b/src/types/auth.type.ts @@ -1,94 +1,95 @@ 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); 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 f5dd616..25d142a 100644 --- a/src/types/competition.type.ts +++ b/src/types/competition.type.ts @@ -1,58 +1,59 @@ -import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; +import { createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { competitionAnnouncement } from '~/db/schema'; + import { TeamSchema } from './team.type'; 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), }); 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/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..505dd3a 100644 --- a/src/types/team-member.type.ts +++ b/src/types/team-member.type.ts @@ -1,65 +1,66 @@ 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({ - 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 2f81248..0b99186 100644 --- a/src/types/team.type.ts +++ b/src/types/team.type.ts @@ -1,14 +1,15 @@ 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({ - 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() }); @@ -16,38 +17,38 @@ 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, }); export const CompetitionIdParam = z.object({ competitionId: z.string().openapi({ param: { - in: "path", + in: 'path', required: true, }, }), @@ -56,9 +57,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), +}); 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..44e6b66 100644 --- a/src/utils/error-response-factory.ts +++ b/src/utils/error-response-factory.ts @@ -1,27 +1,28 @@ 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..c730ac3 100644 --- a/src/utils/router-factory.ts +++ b/src/utils/router-factory.ts @@ -3,42 +3,42 @@ import { jwt } from 'hono/jwt'; import { env } from '~/configs/env.config'; import { JWTPayloadSchema } from '~/types/auth.type'; -// biome-ignore lint/suspicious/noExplicitAny: +// 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); - } + 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; } 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 + } }