Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions apps/web/app/api/google/threads/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import he from "he";
import { NextResponse } from "next/server";
import { parseMessages } from "@/utils/mail";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { getGmailAccessToken, getGmailClient } from "@/utils/gmail/client";
import {
getGmailAccessToken,
getGmailClient,
getGmailClientForEmailAccount,
} from "@/utils/gmail/client";
import { getPlan } from "@/utils/redis/plan";
import { INBOX_LABEL_ID, getGmailLabels } from "@/utils/label";
import { ThreadWithPayloadMessages } from "@/utils/types";
Expand All @@ -13,6 +17,7 @@ import { getThreadsBatch, parseGmailApiResponse } from "@/utils/gmail/thread";
import { withError } from "@/utils/middleware";
import { createGmailLabel } from "./archive/controller";
import { getEmailSummary } from "@/utils/langbase";
import { getWorkspaceEmailAccountForUser } from "@/utils/workspace";

export const dynamic = "force-dynamic";

Expand All @@ -26,22 +31,40 @@ const threadsQuery = z.object({
isTeam: z.coerce.boolean().nullish(),
isCalendar: z.coerce.boolean().nullish(),
isSent: z.coerce.boolean().nullish(),
workspaceId: z.string().nullish(),
emailAccountId: z.string().nullish(),
});
export type ThreadsQuery = z.infer<typeof threadsQuery>;
export type ThreadsResponse = Awaited<ReturnType<typeof getThreads>>;

async function getThreads(query: ThreadsQuery) {
const session = await auth();
const email = session?.user.email;
const emailDomain = email?.split("@")[1];

if (!email) throw new Error("Not authenticated");

const gmail = getGmailClient(session);
const token = await getGmailAccessToken(session);
const accessToken = token?.token;
let gmail = getGmailClient(session);
let token = await getGmailAccessToken(session);
let accessToken = token?.token;
let mailboxEmail = email;

if (query.workspaceId) {
const { emailAccount } = await getWorkspaceEmailAccountForUser({
userId: session.user.id,
workspaceId: query.workspaceId,
emailAccountId: query.emailAccountId,
});

if (!emailAccount) return { threads: [] };

const scopedGmail = await getGmailClientForEmailAccount(emailAccount);
gmail = scopedGmail.gmail;
accessToken = scopedGmail.accessToken;
mailboxEmail = emailAccount.email;
}

if (!accessToken) throw new Error("Missing access token");
const emailDomain = mailboxEmail.split("@")[1];

const gmailLabels = await getGmailLabels(gmail);

Expand Down Expand Up @@ -117,7 +140,7 @@ async function getThreads(query: ThreadsQuery) {
messages: messagesAsGMailMessage,
snippet: he.decode(thread.snippet || ""),
plan: plan ? { ...plan, databaseRule: rule } : undefined,
category: await getCategory({ email, threadId: id }),
category: await getCategory({ email: mailboxEmail, threadId: id }),
};
}) || [],
);
Expand All @@ -134,6 +157,8 @@ export const GET = withError(async (request: Request) => {
const isTeam = searchParams.get("isTeam");
const isCalendar = searchParams.get("isCalendar");
const isSent = searchParams.get("isSent");
const workspaceId = searchParams.get("workspaceId");
const emailAccountId = searchParams.get("emailAccountId");
const query = threadsQuery.parse({
limit,
fromEmail,
Expand All @@ -142,6 +167,8 @@ export const GET = withError(async (request: Request) => {
isTeam,
isCalendar,
isSent,
workspaceId,
emailAccountId,
});

const threads = await getThreads(query);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import { removeWorkspaceEmailAccount } from "@/utils/workspace";

export const DELETE = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId || !params.emailAccountId)
throw new Error("Missing workspace email account");

return NextResponse.json(
await removeWorkspaceEmailAccount({
userId: session.user.id,
workspaceId: params.workspaceId,
emailAccountId: params.emailAccountId,
}),
);
});
43 changes: 43 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,43 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import {
addCurrentUserEmailAccount,
getWorkspaceMembership,
} from "@/utils/workspace";

export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const membership = await getWorkspaceMembership({
userId: session.user.id,
workspaceId: params.workspaceId,
});

if (!membership) throw new Error("Workspace not found");

return NextResponse.json({
emailAccounts: membership.organization.emailAccounts.map((account) => ({
id: account.id,
name: account.name,
email: account.email,
})),
});
});

export const POST = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const emailAccount = await addCurrentUserEmailAccount({
userId: session.user.id,
workspaceId: params.workspaceId,
email: session.user.email,
name: session.user.name,
});

return NextResponse.json({ emailAccount });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MembershipRole } from "@prisma/client";
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import {
removeWorkspaceMember,
updateWorkspaceMemberRole,
} from "@/utils/workspace";

const updateMemberBody = z.object({
role: z.nativeEnum(MembershipRole),
});

export const PATCH = withError(async (request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId || !params.memberId)
throw new Error("Missing workspace member");

const body = updateMemberBody.parse(await request.json());
const member = await updateWorkspaceMemberRole({
userId: session.user.id,
workspaceId: params.workspaceId,
memberId: params.memberId,
role: body.role,
});

return NextResponse.json({ member });
});

export const DELETE = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId || !params.memberId)
throw new Error("Missing workspace member");

return NextResponse.json(
await removeWorkspaceMember({
userId: session.user.id,
workspaceId: params.workspaceId,
memberId: params.memberId,
}),
);
});
70 changes: 70 additions & 0 deletions apps/web/app/api/workspaces/[workspaceId]/members/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { MembershipRole } from "@prisma/client";
import { sendWorkspaceInviteEmail } from "@inboxzero/resend";
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { env } from "@/env.mjs";
import { withError } from "@/utils/middleware";
import prisma from "@/utils/prisma";
import {
getWorkspaceMembership,
inviteWorkspaceMember,
} from "@/utils/workspace";

const inviteMemberBody = z.object({
email: z.string().trim().email(),
name: z.string().trim().max(80).optional(),
role: z.nativeEnum(MembershipRole).default(MembershipRole.USER),
});

export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const membership = await getWorkspaceMembership({
userId: session.user.id,
workspaceId: params.workspaceId,
});

if (!membership) throw new Error("Workspace not found");

return NextResponse.json({
members: membership.organization.membership,
});
});

export const POST = withError(async (request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const body = inviteMemberBody.parse(await request.json());
const workspace = await prisma.organization.findUniqueOrThrow({
where: { id: params.workspaceId },
select: { id: true, name: true },
});

const invite = await inviteWorkspaceMember({
workspaceId: workspace.id,
inviterUserId: session.user.id,
inviterEmail: session.user.email,
invitedEmail: body.email,
invitedName: body.name,
role: body.role,
});

await sendWorkspaceInviteEmail({
to: body.email,
emailProps: {
baseUrl: env.NEXT_PUBLIC_BASE_URL,
invitedByName: session.user.name || session.user.email,
workspaceName: workspace.name,
inviteUrl: invite.invitedEmail
? `${env.NEXT_PUBLIC_BASE_URL}/api/workspaces/invitations/${invite.id}/accept`
: `${env.NEXT_PUBLIC_BASE_URL}/mail`,
},
});

return NextResponse.json({ member: invite });
});
43 changes: 43 additions & 0 deletions apps/web/app/api/workspaces/[workspaceId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import {
getWorkspaceMembership,
serializeWorkspace,
updateWorkspaceForUser,
} from "@/utils/workspace";

const updateWorkspaceBody = z.object({
name: z.string().trim().min(1).max(80).optional(),
image: z.string().url().nullable().optional(),
});

export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const membership = await getWorkspaceMembership({
userId: session.user.id,
workspaceId: params.workspaceId,
});

return NextResponse.json({ workspace: serializeWorkspace(membership) });
});

export const PATCH = withError(async (request, { params }) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");
if (!params.workspaceId) throw new Error("Missing workspace id");

const body = updateWorkspaceBody.parse(await request.json());
const membership = await updateWorkspaceForUser({
userId: session.user.id,
workspaceId: params.workspaceId,
name: body.name,
image: body.image,
});

return NextResponse.json({ workspace: serializeWorkspace(membership) });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { env } from "@/env.mjs";
import { withError } from "@/utils/middleware";
import { acceptWorkspaceInvitation } from "@/utils/workspace";

export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) {
return NextResponse.redirect(`${env.NEXT_PUBLIC_BASE_URL}/login`);
}
if (!params.invitationId) throw new Error("Missing invitation id");

await acceptWorkspaceInvitation({
invitationId: params.invitationId,
userId: session.user.id,
email: session.user.email,
name: session.user.name,
});

return NextResponse.redirect(`${env.NEXT_PUBLIC_BASE_URL}/mail`);
});
55 changes: 55 additions & 0 deletions apps/web/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { MembershipRole } from "@prisma/client";
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import {
createWorkspaceForUser,
ensurePersonalWorkspaceForUser,
getWorkspacesForUser,
serializeWorkspace,
} from "@/utils/workspace";

const createWorkspaceBody = z.object({
name: z.string().trim().min(1).max(80),
image: z.string().url().nullish(),
});

async function getWorkspaces() {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");

await ensurePersonalWorkspaceForUser({
userId: session.user.id,
email: session.user.email,
name: session.user.name,
image: session.user.image,
});

return { workspaces: await getWorkspacesForUser(session.user.id) };
}

export const GET = withError(async () => {
return NextResponse.json(await getWorkspaces());
});

export const POST = withError(async (request: Request) => {
const session = await auth();
if (!session?.user.email) throw new Error("Not authenticated");

const body = createWorkspaceBody.parse(await request.json());
const membership = await createWorkspaceForUser({
userId: session.user.id,
email: session.user.email,
userName: session.user.name,
name: body.name,
image: body.image || null,
});

return NextResponse.json({
workspace: {
...serializeWorkspace(membership),
role: MembershipRole.OWNER,
},
});
});
Loading