Skip to content

Commit 286d264

Browse files
committed
calemdar
1 parent b331f9c commit 286d264

File tree

15 files changed

+1552
-442
lines changed

15 files changed

+1552
-442
lines changed

prisma/schema.prisma

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ model User {
126126
semesters Int?
127127
tags String?
128128
excludeFromExport Boolean @default(false)
129+
// Microsoft integration (tokens never exposed to client)
130+
msAccessToken String?
131+
msRefreshToken String?
132+
msTokenExpiry DateTime?
129133
130134
// NextAuth relations
131135
accounts Account[]
@@ -262,15 +266,20 @@ model WorkPlanCompletion {
262266
// ─── Attendance ────────────────────────────────────────────────────────────────
263267

264268
model Meeting {
265-
id String @id @default(cuid())
266-
title String
267-
description String?
268-
startTime DateTime
269-
duration Int // minutes
270-
checkInToken String @unique @default(cuid())
271-
createdBy String
272-
projectId String?
273-
createdAt DateTime @default(now())
269+
id String @id @default(cuid())
270+
title String
271+
description String?
272+
notes String?
273+
notesAllowAttendees Boolean @default(false)
274+
startTime DateTime
275+
duration Int // minutes
276+
checkInToken String @unique @default(cuid())
277+
createdBy String
278+
projectId String?
279+
teamsChannelId String?
280+
teamsJoinUrl String?
281+
outlookEventId String?
282+
createdAt DateTime @default(now())
274283
275284
creator User @relation("MeetingCreator", fields: [createdBy], references: [id])
276285
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Microsoft OAuth callback — exchanges the auth code for tokens and stores them.
3+
*/
4+
import { auth } from "~/server/auth";
5+
import { db } from "~/server/db";
6+
import { env } from "~/env.js";
7+
8+
const SCOPES =
9+
"offline_access Calendars.ReadWrite OnlineMeetings.ReadWrite ChannelMessage.Send Channel.ReadBasic.All User.Read";
10+
11+
export async function GET(request: Request) {
12+
const url = new URL(request.url);
13+
const code = url.searchParams.get("code");
14+
const state = url.searchParams.get("state");
15+
const error = url.searchParams.get("error");
16+
const profileUrl = `${url.origin}/dashboard/profile/edit`;
17+
18+
if (error) {
19+
return Response.redirect(`${profileUrl}?ms_error=${encodeURIComponent(error)}`);
20+
}
21+
if (!code || !state) {
22+
return Response.redirect(`${profileUrl}?ms_error=missing_params`);
23+
}
24+
25+
// Verify the authenticated user matches the state (userId)
26+
const session = await auth();
27+
if (!session?.user?.id || session.user.id !== state) {
28+
return Response.redirect(`${profileUrl}?ms_error=session_mismatch`);
29+
}
30+
31+
if (!env.MICROSOFT_CLIENT_ID || !env.MICROSOFT_CLIENT_SECRET || !env.MICROSOFT_TENANT_ID) {
32+
return Response.redirect(`${profileUrl}?ms_error=not_configured`);
33+
}
34+
35+
const params = new URLSearchParams({
36+
grant_type: "authorization_code",
37+
client_id: env.MICROSOFT_CLIENT_ID,
38+
client_secret: env.MICROSOFT_CLIENT_SECRET,
39+
code,
40+
redirect_uri: `${url.origin}/api/auth/microsoft/callback`,
41+
scope: SCOPES,
42+
});
43+
44+
const tokenRes = await fetch(
45+
`https://login.microsoftonline.com/${env.MICROSOFT_TENANT_ID}/oauth2/v2.0/token`,
46+
{
47+
method: "POST",
48+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
49+
body: params.toString(),
50+
},
51+
);
52+
53+
if (!tokenRes.ok) {
54+
return Response.redirect(`${profileUrl}?ms_error=token_exchange_failed`);
55+
}
56+
57+
const tokens = (await tokenRes.json()) as {
58+
access_token: string;
59+
refresh_token: string;
60+
expires_in: number;
61+
};
62+
63+
await db.user.update({
64+
where: { id: session.user.id },
65+
data: {
66+
msAccessToken: tokens.access_token,
67+
msRefreshToken: tokens.refresh_token,
68+
msTokenExpiry: new Date(Date.now() + tokens.expires_in * 1000),
69+
},
70+
});
71+
72+
return Response.redirect(`${profileUrl}?ms_connected=1`);
73+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Disconnects the user's Microsoft account by clearing stored tokens.
3+
*/
4+
import { auth } from "~/server/auth";
5+
import { db } from "~/server/db";
6+
7+
export async function DELETE(request: Request) {
8+
const session = await auth();
9+
if (!session?.user?.id) {
10+
return Response.json({ error: "Unauthorized" }, { status: 401 });
11+
}
12+
13+
await db.user.update({
14+
where: { id: session.user.id },
15+
data: { msAccessToken: null, msRefreshToken: null, msTokenExpiry: null },
16+
});
17+
18+
return Response.json({ ok: true });
19+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Initiates Microsoft OAuth flow.
3+
* User must be signed in; their session ID is used as the state parameter.
4+
*/
5+
import { auth } from "~/server/auth";
6+
import { env } from "~/env.js";
7+
8+
const SCOPES =
9+
"offline_access Calendars.ReadWrite OnlineMeetings.ReadWrite ChannelMessage.Send Channel.ReadBasic.All User.Read";
10+
11+
export async function GET(request: Request) {
12+
const session = await auth();
13+
if (!session?.user?.id) {
14+
return Response.redirect(new URL("/", request.url));
15+
}
16+
17+
if (!env.MICROSOFT_CLIENT_ID || !env.MICROSOFT_TENANT_ID) {
18+
return Response.redirect(
19+
new URL("/dashboard/profile/edit?ms_error=not_configured", request.url),
20+
);
21+
}
22+
23+
const origin = new URL(request.url).origin;
24+
const redirectUri = `${origin}/api/auth/microsoft/callback`;
25+
26+
const params = new URLSearchParams({
27+
client_id: env.MICROSOFT_CLIENT_ID,
28+
response_type: "code",
29+
redirect_uri: redirectUri,
30+
scope: SCOPES,
31+
response_mode: "query",
32+
state: session.user.id,
33+
});
34+
35+
return Response.redirect(
36+
`https://login.microsoftonline.com/${env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?${params.toString()}`,
37+
);
38+
}

0 commit comments

Comments
 (0)