EmberClone/apps/api/src/routes/users.ts

166 lines
4.1 KiB
TypeScript

import { FastifyInstance } from "fastify"
import { db } from "../db"
import { users } from "../db/schema"
import { eq } from "drizzle-orm"
import { z } from "zod"
import argon2 from "argon2"
import { emailService } from "../services/emailService"
const UserUpdateSchema = z.object({
name: z.string().min(1).optional(),
role: z.string().optional()
})
const UserCreateSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
role: z.string(),
password: z.string().min(8)
})
const PasswordChangeSchema = z.object({
oldPassword: z.string(),
newPassword: z.string().min(8)
})
export default async function userRoutes(fastify: FastifyInstance) {
fastify.addHook("preHandler", async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
return reply.code(401).send({ message: "Unauthorized" })
}
})
const isAdmin = (request: any) => {
return (request.user as { sub: string, role: string })?.role === "admin"
}
fastify.get("/me", async (request, reply) => {
const userId = (request.user as { sub: string } | undefined)?.sub
if (!userId) {
return reply.code(401).send({ message: "User ID not found in token" })
}
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1)
if (!user) {
return reply.code(404).send({ message: "User not found" })
}
return user
})
fastify.patch("/me", async (request, reply) => {
const userId = (request.user as { sub: string } | undefined)?.sub
if (!userId) {
return reply.code(401).send({ message: "User ID not found in token" })
}
const body = UserUpdateSchema.parse(request.body)
const [user] = await db
.update(users)
.set(body)
.where(eq(users.id, userId))
.returning()
if (!user) {
return reply.code(404).send({ message: "User not found" })
}
return user
})
fastify.post("/me/password", async (request, reply) => {
const userId = (request.user as { sub: string } | undefined)?.sub
if (!userId) {
return reply.code(401).send({ message: "User ID not found in token" })
}
const body = PasswordChangeSchema.parse(request.body)
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1)
if (!user) {
return reply.code(404).send({ message: "User not found" })
}
const isPasswordCorrect = await argon2.verify(user.passwordHash, body.oldPassword)
if (!isPasswordCorrect) {
return reply.code(401).send({ message: "Incorrect old password" })
}
const newPasswordHash = await argon2.hash(body.newPassword)
await db
.update(users)
.set({ passwordHash: newPasswordHash })
.where(eq(users.id, userId))
return reply.send({ message: "Password updated successfully" })
})
fastify.get("/", async (request, reply) => {
if (!isAdmin(request)) {
return reply.code(403).send({ message: "Forbidden: Admin role required" })
}
return await db.select().from(users)
})
fastify.post("/", async (request, reply) => {
if (!isAdmin(request)) {
return reply.code(403).send({ message: "Forbidden: Admin role required" })
}
const body = UserCreateSchema.parse(request.body)
const passwordHash = await argon2.hash(body.password)
const [user] = await db
.insert(users)
.values({
email: body.email,
name: body.name,
role: body.role,
passwordHash
})
.returning()
if (user) {
await emailService.sendWelcome(user)
}
return reply.code(201).send(user)
})
fastify.patch("/:id", async (request, reply) => {
if (!isAdmin(request)) {
return reply.code(403).send({ message: "Forbidden: Admin role required" })
}
const { id } = request.params as { id: string }
const body = UserUpdateSchema.parse(request.body)
const [user] = await db
.update(users)
.set(body)
.where(eq(users.id, id))
.returning()
if (!user) {
return reply.code(404).send({ message: "User not found" })
}
return user
})
}