diff --git a/app/api/orgs/[orgId]/users/[userId]/route.ts b/app/api/orgs/[orgId]/users/[userId]/route.ts new file mode 100644 index 0000000..72c106d --- /dev/null +++ b/app/api/orgs/[orgId]/users/[userId]/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import * as userService from "./service"; +import { IdSchema } from "@/app/api/types"; +import { updateUserRoleSchema } from "@/lib/validations"; +import { z } from "zod"; + +export async function DELETE( + _req: NextRequest, + { params }: { params: { orgId: string; userId: string } }, +) { + try { + const orgId = IdSchema.parse(params.orgId); + const userId = IdSchema.parse(params.userId); + + const deleted = await userService.removeUserFromOrg(orgId, userId); + return NextResponse.json(deleted); + } catch (error) { + if (error instanceof Error) { + return NextResponse.json({ message: error.message }, { status: 404 }); + } + return NextResponse.json( + { message: "Failed to remove user" }, + { status: 500 }, + ); + } +} + +export async function PATCH( + _req: NextRequest, + { params }: { params: { orgId: string; userId: string } }, +) { + try { + const orgId = IdSchema.parse(params.orgId); + const userId = IdSchema.parse(params.userId); + const { role } = updateUserRoleSchema.parse(await _req.json()); + + const updated = await userService.updateUserRole(orgId, userId, role); + return NextResponse.json(updated); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ errors: error.errors }, { status: 400 }); + } + if (error instanceof Error) { + return NextResponse.json({ message: error.message }, { status: 404 }); + } + return NextResponse.json( + { message: "Failed to update role" }, + { status: 500 }, + ); + } +} diff --git a/app/api/orgs/[orgId]/users/[userId]/service.ts b/app/api/orgs/[orgId]/users/[userId]/service.ts new file mode 100644 index 0000000..a3af2ad --- /dev/null +++ b/app/api/orgs/[orgId]/users/[userId]/service.ts @@ -0,0 +1,32 @@ +import { db } from "@/db/drizzle"; +import { memberships } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; + +export async function removeUserFromOrg(orgId: number, userId: number) { + const [deleted] = await db + .delete(memberships) + .where(and(eq(memberships.orgId, orgId), eq(memberships.userId, userId))) + .returning(); + + if (!deleted) { + throw new Error("Membership not found"); + } + return deleted; +} + +export async function updateUserRole( + orgId: number, + userId: number, + role: string, +) { + const [updated] = await db + .update(memberships) + .set({ role }) + .where(and(eq(memberships.orgId, orgId), eq(memberships.userId, userId))) + .returning(); + + if (!updated) { + throw new Error("Membership not found"); + } + return updated; +} diff --git a/app/api/orgs/[orgId]/users/route.ts b/app/api/orgs/[orgId]/users/route.ts new file mode 100644 index 0000000..d9e8250 --- /dev/null +++ b/app/api/orgs/[orgId]/users/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import * as userService from "./service"; +import { IdSchema } from "@/app/api/types"; +import { inviteUserSchema } from "@/lib/validations"; +import { z } from "zod"; + +export async function GET( + _req: NextRequest, + { params }: { params: { orgId: string } }, +) { + try { + const orgId = IdSchema.parse(params.orgId); + const users = await userService.getOrgUsers(orgId); + return NextResponse.json(users); + } catch (error) { + return NextResponse.json( + { message: "Failed to fetch users" }, + { status: 500 }, + ); + } +} + +export async function POST( + request: NextRequest, + { params }: { params: { orgId: string } }, +) { + try { + const orgId = IdSchema.parse(params.orgId); + const data = inviteUserSchema.parse(await request.json()); + + const membership = await userService.inviteUser(orgId, data); + return NextResponse.json(membership, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ errors: error.errors }, { status: 400 }); + } + if (error instanceof Error) { + return NextResponse.json({ message: error.message }, { status: 400 }); + } + return NextResponse.json( + { message: "Failed to invite user" }, + { status: 500 }, + ); + } +} diff --git a/app/api/orgs/[orgId]/users/service.ts b/app/api/orgs/[orgId]/users/service.ts new file mode 100644 index 0000000..b79de46 --- /dev/null +++ b/app/api/orgs/[orgId]/users/service.ts @@ -0,0 +1,64 @@ +import { db } from "@/db/drizzle"; +import { users, memberships } from "@/db/schema"; +import { inviteUserSchema } from "@/lib/validations"; +import { eq, and } from "drizzle-orm"; +import { z } from "zod"; + +export async function getOrgUsers(orgId: number) { + return await db + .select({ + id: users.id, + name: users.name, + nameId: users.nameId, + avatar: users.avatar, + about: users.about, + role: memberships.role, + joinedAt: memberships.joinedAt, + }) + .from(users) + .innerJoin(memberships, eq(memberships.userId, users.id)) + .where(eq(memberships.orgId, orgId)) + .orderBy(memberships.joinedAt); +} + +export async function inviteUser( + orgId: number, + data: z.infer, +) { + return await db.transaction(async (tx) => { + const user = await tx + .select({ + id: users.id, + }) + .from(users) + .where(eq(users.email, data.email)) + .limit(1); + + if (user.length === 0) { + throw new Error("User not found"); + } + + const existingMembership = await tx + .select() + .from(memberships) + .where( + and(eq(memberships.userId, user[0].id), eq(memberships.orgId, orgId)), + ) + .limit(1); + + if (existingMembership.length > 0) { + throw new Error("User is already a member"); + } + + const [membership] = await tx + .insert(memberships) + .values({ + userId: user[0].id, + orgId, + role: data.role, + }) + .returning(); + + return membership; + }); +}