Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import prisma from "@/utils/prisma";
import { NextResponse } from "next/server";
import { z } from "zod";

const updateEmailAccountSchema = z.object({
name: z.string().trim().min(1).max(80).optional(),
email: z
.string()
.trim()
.email()
.max(255)
.transform((email) => email.toLowerCase())
.optional(),
});

async function canManageWorkspace(workspaceId: string, userId: string) {
const membership = await prisma.membership.findFirst({
where: {
organizationId: workspaceId,
userId,
},
});

return membership?.role === "OWNER" || membership?.role === "ADMIN";
}

async function getEmailAccount(workspaceId: string, emailAccountId: string) {
return prisma.emailAccount.findFirst({
where: {
id: emailAccountId,
organizationId: workspaceId,
},
select: {
id: true,
email: true,
},
});
}

export async function PATCH(
request: Request,
{ params }: { params: { workspaceId: string; emailAccountId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!(await canManageWorkspace(params.workspaceId, session.user.id))) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const targetEmailAccount = await getEmailAccount(
params.workspaceId,
params.emailAccountId,
);
if (!targetEmailAccount) {
return NextResponse.json(
{ error: "Email account not found" },
{ status: 404 },
);
}

const parsed = updateEmailAccountSchema.safeParse(await request.json());
if (!parsed.success || (!parsed.data.name && !parsed.data.email)) {
return NextResponse.json(
{ error: "Invalid email account payload" },
{ status: 400 },
);
}

if (parsed.data.email && parsed.data.email !== targetEmailAccount.email) {
const existingEmailAccount = await prisma.emailAccount.findFirst({
where: {
organizationId: params.workspaceId,
email: parsed.data.email,
NOT: { id: targetEmailAccount.id },
},
});
if (existingEmailAccount) {
return NextResponse.json(
{ error: "Email account already exists in this workspace" },
{ status: 409 },
);
}
}

const emailAccount = await prisma.emailAccount.update({
where: { id: targetEmailAccount.id },
data: parsed.data,
select: {
id: true,
name: true,
email: true,
},
});

return NextResponse.json({ emailAccount });
}

export async function DELETE(
_request: Request,
{ params }: { params: { workspaceId: string; emailAccountId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!(await canManageWorkspace(params.workspaceId, session.user.id))) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const targetEmailAccount = await getEmailAccount(
params.workspaceId,
params.emailAccountId,
);
if (!targetEmailAccount) {
return NextResponse.json(
{ error: "Email account not found" },
{ status: 404 },
);
}

await prisma.emailAccount.delete({
where: { id: targetEmailAccount.id },
});

return NextResponse.json({ success: true });
}
120 changes: 120 additions & 0 deletions apps/web/app/api/workspaces/[workspaceId]/email-accounts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import prisma from "@/utils/prisma";
import { NextResponse } from "next/server";
import { z } from "zod";

const emailAccountSchema = z.object({
name: z.string().trim().min(1).max(80),
email: z
.string()
.trim()
.email()
.max(255)
.transform((email) => email.toLowerCase()),
});

async function getWorkspaceMembership(workspaceId: string, userId: string) {
return prisma.membership.findFirst({
where: {
organizationId: workspaceId,
userId,
},
});
}

async function canManageWorkspace(workspaceId: string, userId: string) {
const membership = await getWorkspaceMembership(workspaceId, userId);
return membership?.role === "OWNER" || membership?.role === "ADMIN";
}

export async function GET(
_request: Request,
{ params }: { params: { workspaceId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

const membership = await getWorkspaceMembership(
params.workspaceId,
session.user.id,
);
if (!membership) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const emailAccounts = await prisma.emailAccount.findMany({
where: { organizationId: params.workspaceId },
select: {
id: true,
name: true,
email: true,
},
orderBy: { createdAt: "asc" },
});

return NextResponse.json({ emailAccounts });
}

export async function POST(
request: Request,
{ params }: { params: { workspaceId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!(await canManageWorkspace(params.workspaceId, session.user.id))) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const parsed = emailAccountSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid email account payload" },
{ status: 400 },
);
}

const accountCount = await prisma.emailAccount.count({
where: { organizationId: params.workspaceId },
});
if (accountCount >= 10) {
return NextResponse.json(
{ error: "A workspace can have up to 10 email accounts" },
{ status: 400 },
);
}

const existingEmailAccount = await prisma.emailAccount.findFirst({
where: {
organizationId: params.workspaceId,
email: parsed.data.email,
},
});
if (existingEmailAccount) {
return NextResponse.json(
{ error: "Email account already exists in this workspace" },
{ status: 409 },
);
}

const emailAccount = await prisma.emailAccount.create({
data: {
organizationId: params.workspaceId,
name: parsed.data.name,
email: parsed.data.email,
},
select: {
id: true,
name: true,
email: true,
},
});

return NextResponse.json({ emailAccount }, { status: 201 });
}
125 changes: 125 additions & 0 deletions apps/web/app/api/workspaces/[workspaceId]/members/[memberId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import prisma from "@/utils/prisma";
import { NextResponse } from "next/server";
import { z } from "zod";

const updateMemberSchema = z.object({
role: z.enum(["ADMIN", "USER"]),
});

async function getOwnerMembership(workspaceId: string, userId: string) {
return prisma.membership.findFirst({
where: {
organizationId: workspaceId,
userId,
role: "OWNER",
},
});
}

async function getTargetMembership(workspaceId: string, memberId: string) {
return prisma.membership.findFirst({
where: {
id: memberId,
organizationId: workspaceId,
},
select: {
id: true,
role: true,
userId: true,
},
});
}

export async function PATCH(
request: Request,
{ params }: { params: { workspaceId: string; memberId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!(await getOwnerMembership(params.workspaceId, session.user.id))) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const targetMembership = await getTargetMembership(
params.workspaceId,
params.memberId,
);
if (!targetMembership) {
return NextResponse.json({ error: "Member not found" }, { status: 404 });
}
if (targetMembership.role === "OWNER") {
return NextResponse.json(
{ error: "Owner role cannot be changed" },
{ status: 400 },
);
}

const parsed = updateMemberSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid member payload" },
{ status: 400 },
);
}

const member = await prisma.membership.update({
where: { id: targetMembership.id },
data: { role: parsed.data.role },
select: {
id: true,
role: true,
invitedName: true,
invitedEmail: true,
user: {
select: {
id: true,
name: true,
email: true,
image: true,
},
},
},
});

return NextResponse.json({ member });
}

export async function DELETE(
_request: Request,
{ params }: { params: { workspaceId: string; memberId: string } },
) {
const session = await auth();

if (!session?.user?.id) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!(await getOwnerMembership(params.workspaceId, session.user.id))) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}

const targetMembership = await getTargetMembership(
params.workspaceId,
params.memberId,
);
if (!targetMembership) {
return NextResponse.json({ error: "Member not found" }, { status: 404 });
}
if (targetMembership.role === "OWNER") {
return NextResponse.json(
{ error: "Owner membership cannot be removed" },
{ status: 400 },
);
}

await prisma.membership.delete({
where: { id: targetMembership.id },
});

return NextResponse.json({ success: true });
}
Loading