From 084a2f761882b495c198bef136eea8db409821fe Mon Sep 17 00:00:00 2001 From: NotNite Date: Tue, 25 Apr 2023 19:28:07 -0400 Subject: [PATCH] refactor database to split ticket/user/oauth --- .../migration.sql | 45 ++++++++++++ prisma/schema.prisma | 22 ++++-- src/app/api/register/route.ts | 24 +++--- src/app/me/page.tsx | 11 +++ src/app/oauth/discord/login/route.ts | 8 +- src/app/oauth/discord/oauth.ts | 2 - src/app/oauth/discord/redirect/route.ts | 73 +++++++++++++++---- src/auth.ts | 43 +++++++++++ 8 files changed, 185 insertions(+), 43 deletions(-) create mode 100644 prisma/migrations/20230425231459_redo_ticket_model/migration.sql create mode 100644 src/app/me/page.tsx create mode 100644 src/auth.ts diff --git a/prisma/migrations/20230425231459_redo_ticket_model/migration.sql b/prisma/migrations/20230425231459_redo_ticket_model/migration.sql new file mode 100644 index 0000000..129a4c6 --- /dev/null +++ b/prisma/migrations/20230425231459_redo_ticket_model/migration.sql @@ -0,0 +1,45 @@ +/* + Warnings: + + - You are about to drop the column `username` on the `AuthTicket` table. All the data in the column will be lost. + - You are about to drop the column `authTicketId` on the `DiscordAuth` table. All the data in the column will be lost. + - You are about to drop the column `refreshAt` on the `DiscordAuth` table. All the data in the column will be lost. + - Added the required column `expiresAt` to the `AuthTicket` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `AuthTicket` table without a default value. This is not possible if the table is not empty. + - Added the required column `expiresAt` to the `DiscordAuth` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `DiscordAuth` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_AuthTicket" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "ticket" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "userId" INTEGER NOT NULL, + CONSTRAINT "AuthTicket_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_AuthTicket" ("id", "ticket") SELECT "id", "ticket" FROM "AuthTicket"; +DROP TABLE "AuthTicket"; +ALTER TABLE "new_AuthTicket" RENAME TO "AuthTicket"; +CREATE UNIQUE INDEX "AuthTicket_userId_key" ON "AuthTicket"("userId"); +CREATE TABLE "new_DiscordAuth" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" INTEGER NOT NULL, + "accessToken" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + CONSTRAINT "DiscordAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_DiscordAuth" ("accessToken", "id", "refreshToken") SELECT "accessToken", "id", "refreshToken" FROM "DiscordAuth"; +DROP TABLE "DiscordAuth"; +ALTER TABLE "new_DiscordAuth" RENAME TO "DiscordAuth"; +CREATE UNIQUE INDEX "DiscordAuth_userId_key" ON "DiscordAuth"("userId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5069163..39d297e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,19 +8,29 @@ datasource db { } model AuthTicket { - id Int @id @default(autoincrement()) - username String? - ticket String + id Int @id @default(autoincrement()) + ticket String + expiresAt DateTime + + user User @relation(references: [id], fields: [userId]) + userId Int @unique +} + +model User { + id Int @id @default(autoincrement()) + username String? + authTicket AuthTicket? + discordAuth DiscordAuth? } model DiscordAuth { id String @id - authTicket AuthTicket @relation(fields: [authTicketId], references: [id]) - authTicketId Int @unique + user User @relation(fields: [userId], references: [id]) + userId Int @unique accessToken String refreshToken String - refreshAt DateTime + expiresAt DateTime } diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 321ab53..bc69d52 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -1,6 +1,6 @@ -import { NextApiRequest } from "next"; import * as ldap from "@/ldap"; import prisma from "@/prisma"; +import { getUserFromRequest } from "@/auth"; type RequestBody = { username: string; @@ -11,20 +11,14 @@ type RequestBody = { }; export async function POST(request: Request) { - const authorization = request.headers - .get("authorization") - ?.replace("Bearer ", ""); - - if (authorization == null) return new Response(null, { status: 401 }); - - const user = await prisma.authTicket.findFirst({ - where: { - ticket: authorization - } - }); - + const user = await getUserFromRequest(request); if (user == null) return new Response(null, { status: 401 }); + if (user.username !== null) { + // user already has an account, don't re-register + return new Response(null, { status: 403 }); + } + const { username, displayName, email, password, avatarBase64 } = (await request.json()) as RequestBody; @@ -89,12 +83,12 @@ export async function POST(request: Request) { await ldap.createUser(username, displayName, email, avatarBuf); await ldap.setPassword(username, password); - await prisma.authTicket.update({ + await prisma.user.update({ where: { id: user.id }, data: { - username: username + username } }); diff --git a/src/app/me/page.tsx b/src/app/me/page.tsx new file mode 100644 index 0000000..ec6879b --- /dev/null +++ b/src/app/me/page.tsx @@ -0,0 +1,11 @@ +import { getUserFromPage } from "@/auth"; + +export default async function Page() { + const user = await getUserFromPage(); + + if (!user) { + return

Not logged in

; + } + + return

{user.username}

; +} diff --git a/src/app/oauth/discord/login/route.ts b/src/app/oauth/discord/login/route.ts index 1ad7215..4d4c897 100644 --- a/src/app/oauth/discord/login/route.ts +++ b/src/app/oauth/discord/login/route.ts @@ -1,15 +1,15 @@ import { discordRedirectUri } from "../oauth"; +import { v4 } from "uuid"; export async function GET(request: Request) { let url = `https://discord.com/oauth2/authorize`; - - let randomAssString = Math.random().toString(36).substring(2, 15); + let state = v4(); let params = new URLSearchParams(); params.set("response_type", "code"); params.set("client_id", process.env.DISCORD_CLIENT_ID); params.set("scope", "guilds identify email"); - params.set("state", randomAssString); + params.set("state", state); params.set("redirect_uri", discordRedirectUri()); params.set("prompt", "consent"); @@ -19,7 +19,7 @@ export async function GET(request: Request) { status: 302, headers: { Location: url, - "Set-Cookie": `state=${randomAssString}; Path=/;` + "Set-Cookie": `state=${state}; Path=/;` } }); } diff --git a/src/app/oauth/discord/oauth.ts b/src/app/oauth/discord/oauth.ts index 841141f..0f0e65a 100644 --- a/src/app/oauth/discord/oauth.ts +++ b/src/app/oauth/discord/oauth.ts @@ -39,5 +39,3 @@ export async function getDiscordGuilds(token: string) { const res: DiscordGuildResponse[] = await req.json(); return res.map((guild) => guild.id); } - -export const makeTicket = (): string => v4(); diff --git a/src/app/oauth/discord/redirect/route.ts b/src/app/oauth/discord/redirect/route.ts index 4aa711a..8925f8a 100644 --- a/src/app/oauth/discord/redirect/route.ts +++ b/src/app/oauth/discord/redirect/route.ts @@ -2,12 +2,12 @@ import { URLSearchParams } from "url"; import { discordRedirectUri, DiscordAccessTokenResponse, - makeTicket, getDiscordID, getDiscordGuilds } from "../oauth"; import { cookies } from "next/dist/client/components/headers"; import prisma from "@/prisma"; +import { v4 } from "uuid"; export async function GET(request: Request) { let url = new URL(request.url); @@ -51,21 +51,62 @@ export async function GET(request: Request) { if (!allowed) return new Response("not permitted to register account", { status: 403 }); - const user = await prisma.authTicket.create({ + // - create the discord auth data in prisma, which will make the user if it doesn't exist + // - get the user from the discord auth data + // - either create a new auth ticket or invalidate the old one + // - update the user to point to the new auth ticket + + const discordAuth = await prisma.discordAuth.upsert({ + where: { + id + }, + create: { + id, + accessToken: tokenBody.access_token, + refreshToken: tokenBody.refresh_token, + expiresAt: new Date(Date.now() + tokenBody.expires_in * 1000), + user: { + create: { + username: null + } + } + }, + update: { + accessToken: tokenBody.access_token, + refreshToken: tokenBody.refresh_token, + expiresAt: new Date(Date.now() + tokenBody.expires_in * 1000) + } + }); + + const user = await prisma.user.findFirst({ + where: { + id: discordAuth.userId + } + }); + + const authTicket = await prisma.authTicket.upsert({ + where: { + userId: user!.id + }, + create: { + userId: user!.id, + ticket: v4(), + expiresAt: new Date(Date.now() + 86400000) + }, + update: { + ticket: v4(), + expiresAt: new Date(Date.now() + 86400000) + } + }); + + await prisma.user.update({ + where: { + id: user!.id + }, data: { - username: null, - ticket: makeTicket(), - discordAuth: { - connectOrCreate: { - where: { - id - }, - create: { - id, - accessToken: tokenBody.access_token, - refreshToken: tokenBody.refresh_token, - refreshAt: new Date(Date.now() + tokenBody.expires_in * 1000) - } + authTicket: { + connect: { + id: authTicket.id } } } @@ -74,7 +115,7 @@ export async function GET(request: Request) { return new Response(null, { status: 302, headers: { - "Set-Cookie": `ticket=${user.ticket}; Path=/;`, + "Set-Cookie": `ticket=${authTicket.ticket}; Path=/;`, Location: "/register" } }); diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..2121d79 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,43 @@ +import prisma from "@/prisma"; +import { cookies } from "next/dist/client/components/headers"; + +export async function getUserFromRequest(request: Request) { + const authorization = request.headers + .get("authorization") + ?.replace("Bearer ", ""); + if (authorization === null) return null; + + const ticket = await prisma.authTicket.findFirst({ + where: { + ticket: authorization + } + }); + if (ticket === null) return null; + + const user = await prisma.user.findFirst({ + where: { + id: ticket.userId + } + }); + return user; +} + +export async function getUserFromPage() { + const cookieStore = cookies(); + const cookieTicket = cookieStore.get("ticket"); + if (cookieTicket === null) return null; + + const ticket = await prisma.authTicket.findFirst({ + where: { + ticket: cookieTicket?.value + } + }); + if (ticket === null) return null; + + const user = await prisma.user.findFirst({ + where: { + id: ticket.userId + } + }); + return user; +}