diff --git a/.changeset/cyan-steaks-flow.md b/.changeset/cyan-steaks-flow.md new file mode 100644 index 000000000..8a1b73acb --- /dev/null +++ b/.changeset/cyan-steaks-flow.md @@ -0,0 +1,5 @@ +--- +"frontend": patch +--- + +Collect access tokens in preparation for notification editing diff --git a/packages/frontend/app/routes/api/auth.discord.callback.ts b/packages/frontend/app/routes/api/auth.discord.callback.ts index f7d8fa644..5309b0918 100644 --- a/packages/frontend/app/routes/api/auth.discord.callback.ts +++ b/packages/frontend/app/routes/api/auth.discord.callback.ts @@ -65,12 +65,17 @@ export const APIRoute = createAPIFileRoute("/api/auth/discord/callback")({ const guildMember = await api.users.getGuildMember(env.VITE_GUILD_ID); + const { accessToken, refreshToken, accessTokenExpiresAt } = tokens + const [authedUser] = await db .insert(userTable) .values({ id: discordUser.id, username: guildMember.nick || discordUser.username, roles: guildMember.roles, + accessToken, + refreshToken, + accessTokenExpiresAt, }) .onConflictDoUpdate({ target: userTable.id, @@ -78,6 +83,9 @@ export const APIRoute = createAPIFileRoute("/api/auth/discord/callback")({ username: guildMember.nick || discordUser.username, roles: guildMember.roles, updatedAt: new Date().toISOString(), + accessToken, + refreshToken, + accessTokenExpiresAt, }, }) .returning(); diff --git a/packages/frontend/app/server/drizzle/schema.ts b/packages/frontend/app/server/drizzle/schema.ts index 808e549f4..5a8828bc7 100644 --- a/packages/frontend/app/server/drizzle/schema.ts +++ b/packages/frontend/app/server/drizzle/schema.ts @@ -42,6 +42,13 @@ export const userTable = pgTable("User", { .notNull(), id: text("id").primaryKey().notNull(), roles: text("roles").array(), + accessToken: text("accessToken"), + refreshToken: text("refreshToken"), + accessTokenExpiresAt: timestamp("accessTokenExpiresAt", { + precision: 3, + mode: "date", + withTimezone: true, + }), defaultStatus: rsvpStatus("defaultStatus"), additionalOutreachHours: integer("additionalOutreachHours") .default(0) diff --git a/packages/frontend/app/server/schema/UserSchema.ts b/packages/frontend/app/server/schema/UserSchema.ts index 63755d82f..59eea3489 100644 --- a/packages/frontend/app/server/schema/UserSchema.ts +++ b/packages/frontend/app/server/schema/UserSchema.ts @@ -4,6 +4,10 @@ import { userTable } from "../drizzle/schema"; export const UserSchema = createSelectSchema(userTable, { roles: z.array(z.string()).nullable(), -}); +}).omit({ + accessToken: true, + refreshToken: true, + accessTokenExpiresAt: true, +}) export default UserSchema; diff --git a/packages/frontend/drizzle.config.ts b/packages/frontend/drizzle.config.ts index 3ca1da296..91df2c360 100644 --- a/packages/frontend/drizzle.config.ts +++ b/packages/frontend/drizzle.config.ts @@ -2,7 +2,7 @@ import type { Config } from "drizzle-kit"; export default { dialect: "postgresql", - schema: "./src/api/drizzle/schema.ts", + schema: "./app/server/drizzle/schema.ts", out: "./drizzle", dbCredentials: { host: "localhost", diff --git a/packages/frontend/drizzle/0014_new_edwin_jarvis.sql b/packages/frontend/drizzle/0014_new_edwin_jarvis.sql new file mode 100644 index 000000000..d5c7125c2 --- /dev/null +++ b/packages/frontend/drizzle/0014_new_edwin_jarvis.sql @@ -0,0 +1,3 @@ +ALTER TABLE "User" ADD COLUMN "accessToken" text;--> statement-breakpoint +ALTER TABLE "User" ADD COLUMN "refreshToken" text;--> statement-breakpoint +ALTER TABLE "User" ADD COLUMN "expiresAt" timestamp(3) with time zone; \ No newline at end of file diff --git a/packages/frontend/drizzle/0015_chilly_silver_samurai.sql b/packages/frontend/drizzle/0015_chilly_silver_samurai.sql new file mode 100644 index 000000000..e09c4f00b --- /dev/null +++ b/packages/frontend/drizzle/0015_chilly_silver_samurai.sql @@ -0,0 +1 @@ +ALTER TABLE "User" RENAME COLUMN "expiresAt" TO "accessTokenExpiresAt"; \ No newline at end of file diff --git a/packages/frontend/drizzle/meta/0014_snapshot.json b/packages/frontend/drizzle/meta/0014_snapshot.json new file mode 100644 index 000000000..4b6f19667 --- /dev/null +++ b/packages/frontend/drizzle/meta/0014_snapshot.json @@ -0,0 +1,430 @@ +{ + "id": "655a12a8-0de4-4e0e-82f2-346b1dfdb9e3", + "prevId": "e48bb7dd-6794-460b-a35f-3ae76eca803d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Event": { + "name": "Event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startDate": { + "name": "startDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "allDay": { + "name": "allDay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "EventTypes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'Regular'" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isSyncedEvent": { + "name": "isSyncedEvent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isPosted": { + "name": "isPosted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Event_secret_key": { + "name": "Event_secret_key", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RSVP": { + "name": "RSVP", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delay": { + "name": "delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "checkinTime": { + "name": "checkinTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "checkoutTime": { + "name": "checkoutTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "RSVP_eventId_userId_key": { + "name": "RSVP_eventId_userId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RSVP_eventId_Event_id_fk": { + "name": "RSVP_eventId_Event_id_fk", + "tableFrom": "RSVP", + "tableTo": "Event", + "columnsFrom": [ + "eventId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RSVP_userId_User_id_fk": { + "name": "RSVP_userId_User_id_fk", + "tableFrom": "RSVP", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Scancode": { + "name": "Scancode", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Scancode_userId_User_id_fk": { + "name": "Scancode_userId_User_id_fk", + "tableFrom": "Scancode", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_User_id_fk": { + "name": "session_user_id_User_id_fk", + "tableFrom": "session", + "tableTo": "User", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "roles": { + "name": "roles", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "defaultStatus": { + "name": "defaultStatus", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "additionalOutreachHours": { + "name": "additionalOutreachHours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.EventTypes": { + "name": "EventTypes", + "schema": "public", + "values": [ + "Social", + "Regular", + "Outreach", + "Mentor" + ] + }, + "public.RSVPStatus": { + "name": "RSVPStatus", + "schema": "public", + "values": [ + "LATE", + "MAYBE", + "NO", + "YES", + "ATTENDED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/frontend/drizzle/meta/0015_snapshot.json b/packages/frontend/drizzle/meta/0015_snapshot.json new file mode 100644 index 000000000..b2a095bee --- /dev/null +++ b/packages/frontend/drizzle/meta/0015_snapshot.json @@ -0,0 +1,430 @@ +{ + "id": "0303810a-04d5-4576-862e-aeb2ad4a6901", + "prevId": "655a12a8-0de4-4e0e-82f2-346b1dfdb9e3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Event": { + "name": "Event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startDate": { + "name": "startDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "allDay": { + "name": "allDay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "EventTypes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'Regular'" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isSyncedEvent": { + "name": "isSyncedEvent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isPosted": { + "name": "isPosted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Event_secret_key": { + "name": "Event_secret_key", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RSVP": { + "name": "RSVP", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delay": { + "name": "delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "checkinTime": { + "name": "checkinTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "checkoutTime": { + "name": "checkoutTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "RSVP_eventId_userId_key": { + "name": "RSVP_eventId_userId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RSVP_eventId_Event_id_fk": { + "name": "RSVP_eventId_Event_id_fk", + "tableFrom": "RSVP", + "tableTo": "Event", + "columnsFrom": [ + "eventId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RSVP_userId_User_id_fk": { + "name": "RSVP_userId_User_id_fk", + "tableFrom": "RSVP", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Scancode": { + "name": "Scancode", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Scancode_userId_User_id_fk": { + "name": "Scancode_userId_User_id_fk", + "tableFrom": "Scancode", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_User_id_fk": { + "name": "session_user_id_User_id_fk", + "tableFrom": "session", + "tableTo": "User", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "roles": { + "name": "roles", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "defaultStatus": { + "name": "defaultStatus", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "additionalOutreachHours": { + "name": "additionalOutreachHours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.EventTypes": { + "name": "EventTypes", + "schema": "public", + "values": [ + "Social", + "Regular", + "Outreach", + "Mentor" + ] + }, + "public.RSVPStatus": { + "name": "RSVPStatus", + "schema": "public", + "values": [ + "LATE", + "MAYBE", + "NO", + "YES", + "ATTENDED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/frontend/drizzle/meta/_journal.json b/packages/frontend/drizzle/meta/_journal.json index 35db2ad55..17db88398 100644 --- a/packages/frontend/drizzle/meta/_journal.json +++ b/packages/frontend/drizzle/meta/_journal.json @@ -99,6 +99,20 @@ "when": 1731461316225, "tag": "0013_moaning_stardust", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1736558901376, + "tag": "0014_new_edwin_jarvis", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1736559100263, + "tag": "0015_chilly_silver_samurai", + "breakpoints": true } ] } \ No newline at end of file