From e48bf7cc36741e617dd6422bbd0f2638ce51eddf Mon Sep 17 00:00:00 2001 From: Sorawit Date: Wed, 11 Sep 2024 11:34:07 +0700 Subject: [PATCH] feat: auth routes --- bun.lockb | Bin 4214 -> 29981 bytes package.json | 10 ++- .../20240909031529_init/migration.sql | 17 +++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 30 ++++++++ src/controllers/auth.controller.ts | 67 ++++++++++++++++++ src/controllers/user.controller.ts | 3 + src/dto/auth.dto.ts | 33 +++++++++ src/index.ts | 44 +++++++++++- src/services/auth.service.ts | 58 +++++++++++++++ src/utils/db.ts | 18 +++++ src/utils/error.ts | 5 ++ src/utils/jwtParams.ts | 9 +++ 13 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20240909031529_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/controllers/auth.controller.ts create mode 100644 src/controllers/user.controller.ts create mode 100644 src/dto/auth.dto.ts create mode 100644 src/services/auth.service.ts create mode 100644 src/utils/db.ts create mode 100644 src/utils/error.ts create mode 100644 src/utils/jwtParams.ts diff --git a/bun.lockb b/bun.lockb index 6de7842eb2a97d37eaa27af20fb1e543f25c0ec3..43381239a9a9b19eb8b29dca528ed44cbce75a29 100644 GIT binary patch literal 29981 zcmeHw2|QI@_y46^Pidq(gW)BYlbN!0!P>2V7t9*}$$q6p zhbtd?eR>{!c+{a4U85g%iWEX42-8Ffqx}zQus9z*ReV7uG7LsA--jpg;4m1@0@RSc1Wjg>)38Js|B1X=g~4#CfiJpn%6=FkIS*%BMiu2kITYycY1d42B`( zr6KR&=o82X>8>(GbU1BA<;NhEf_f*egRcwl^bm+7vxM@FP(Ddqf3uyao!nmx2Ffe+ z(bmP59-IYSmJ`oM!1eIs`Y;$I_2GH=aGXIdcP|HQcNJ984R9rpBHaQ=J3zVv{=d4Zp6TMF9)!Trx$9S!QePJ_;CFg3?T=U1i>1Vh&T_PCy&n&2m(3o z?mU5zf%%2*JO@(lu>h3IXD}RHz5G1^S_Jk37Qtpf15-2t%I)SQp!70KTs{a=?C1QR zB0kBGVt-~risitb!i_xpcA4;UTgx`k-_zoWW#{#Zk4T#KVWIomwG-`}ts?9C&Yd!*sI(?`sof;+ zwpFpZo}2b{8YKD9c6wbMe{Mlt+l0H%hnkh2Tl&21 zoY6xIbhm3&Oi4=psCTsNZ-?E2oR#vo)NGY(<*O_**wg&>FOlnHn;`eMs^y>^XZ;pP zmiq+GSXHra;G79kH##rd7PH2&?2EfZ7GJ^q<{mGN9QQiMt)+Ak#KA5`y~GH8N5QtuUDgY9g%QMY_}(~v(39SZghvpiN=-_2gTr)lX=#J7&fk|#BU4$76G z{CW@bgjK7z&iqjz!haN8So{S4WBVWT1Rn|)qy7zi!N1ABJt!URhjzmVYgMZPk$)-R z%>a)$#G?&c6A1n!0OpDLBMxb_CJ=l-SeLZ{j~KXBwmJ~}RKS~x@p6DxXe9-LUnk~| zfJ@W>wnp(D!u`hS8i2A~1piS9`GuaTk}(oPb9 znt%s2P_~C{N~;3FYXe>t@R�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 { + // Register an user + const user = await authService.register(body) + if (!user) { + throw new BadRequestError("Failed to register user") + } + + // Add JWT token to user + auth.set({ + value: await jwt.sign(jwtParams(user)), + httpOnly: true, + maxAge: 7 * 86400, + path: '/', + }) + + return user + }, { + body: RegisterBody + }) + + // POST /auth/login + .post("/login", async ({ jwt, cookie: { auth }, body }) => { + const user = await authService.login(body) + + if (!user) { + throw new BadRequestError("Invalid credentials") + } + + // Add JWT token to user + auth.set({ + value: await jwt.sign(jwtParams(user)), + httpOnly: true, + maxAge: 7 * 86400, + path: '/', + }) + + return user + }, { + body: LoginBody + }) + + // POST /auth/logout + .post("/logout", async ({ cookie: { auth }}) => { + auth.remove() + + return { + message: "Logged out" + } + }) \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..0c5556a --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,3 @@ +import Elysia from "elysia"; + +export const workspaceController = new Elysia({ prefix: '/users' }) \ No newline at end of file diff --git a/src/dto/auth.dto.ts b/src/dto/auth.dto.ts new file mode 100644 index 0000000..2a6fce0 --- /dev/null +++ b/src/dto/auth.dto.ts @@ -0,0 +1,33 @@ +import { Static, t } from "elysia"; + +export const RegisterBody = t.Object({ + email: t.String({ + format: "email", + minLength: 1, + error: "Email is required", + }), + password: t.String({ + minLength: 8, + error: "Password must be at least 8 characters", + }), + displayName: t.String({ + minLength: 1, + error: "Display name is required", + }), +}) + +export type RegisterInput = Static + +export const LoginBody = t.Object({ + email: t.String({ + format: "email", + minLength: 1, + error: "Email is required", + }), + password: t.String({ + minLength: 8, + error: "Password must be at least 8 characters", + }), +}) + +export type LoginInput = Static \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9c1f7a1..28999f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,48 @@ import { Elysia } from "elysia"; +import cors from "@elysiajs/cors"; -const app = new Elysia().get("/", () => "Hello Elysia").listen(3000); +import { authController } from "./controllers/auth.controller"; +import { BadRequestError } from "./utils/error"; +import jwt from "@elysiajs/jwt"; + +const PORT = 3000; + +const app = new Elysia() + .use(cors()) + .error({ + "BAD_REQUEST": BadRequestError + }) + .onError(({ code, error, set }) => { + if (code === "NOT_FOUND") { + set.status = 404; + return { + error: "Not Found 🦊", + }; + } + if (code === "VALIDATION") { + set.status = 400; + return { + error: "Bad Request 🦊", + message: error.message, + }; + } + if (code === "BAD_REQUEST") { + set.status = 400; + return { + error: "Bad Request 🦊", + message: error.message, + }; + } + if (code === "INTERNAL_SERVER_ERROR") { + set.status = 500; + return { + error: "Internal Server Error 🦊", + }; + } + }) + .get("/", () => "Welcome to User Management Microservice") + .use(authController) + .listen(PORT); console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..4e90016 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,58 @@ +import { LoginInput, RegisterInput } from "../dto/auth.dto"; +import bcrypt from "bcrypt"; +import { db } from "../utils/db"; +import { ValidationError } from "elysia"; +import { BadRequestError } from "../utils/error"; + +export default class AuthService { + + async register(data: RegisterInput) { + // Check if email is used + const existedUser = await db.user.findUnique({ + where: { + email: data.email + } + }) + + if (existedUser) { + throw new BadRequestError("Email has been already used"); + } + + // Hash password + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(data.password, salt); + + // Create user + const user = await db.user.create({ + data: { + ...data, + password: hashedPassword + } + }) + + return user; + } + + + async login(data: LoginInput) { + // Find user by email + const user = await db.user.findUnique({ + where: { + email: data.email + } + }) + + if (!user) { + throw new BadRequestError("Invalid credentials"); + } + + // Compare password + const isValidPassword = await bcrypt.compare(data.password, user.password); + if (!isValidPassword) { + throw new BadRequestError("Invalid credentials"); + } + + return user; + } + +} \ No newline at end of file diff --git a/src/utils/db.ts b/src/utils/db.ts new file mode 100644 index 0000000..8a33a38 --- /dev/null +++ b/src/utils/db.ts @@ -0,0 +1,18 @@ +import { PrismaClient } from "@prisma/client" + +declare global { + // eslint-disable-next-line no-var + var cachedPrisma: PrismaClient +} + +let prisma: PrismaClient +if (process.env.NODE_ENV === "production") { + prisma = new PrismaClient() +} else { + if (!global.cachedPrisma) { + global.cachedPrisma = new PrismaClient() + } + prisma = global.cachedPrisma +} + +export const db = prisma \ No newline at end of file diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..f3c1255 --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,5 @@ +export class BadRequestError extends Error { + constructor(public message: string) { + super(message) + } +} \ No newline at end of file diff --git a/src/utils/jwtParams.ts b/src/utils/jwtParams.ts new file mode 100644 index 0000000..dc49cb4 --- /dev/null +++ b/src/utils/jwtParams.ts @@ -0,0 +1,9 @@ +import { User } from "@prisma/client"; + +export function jwtParams(user: User) { + return { + id: user.id, + email: user.email, + role: user.role + } +} \ No newline at end of file