refactor database to split ticket/user/oauth

This commit is contained in:
Julian 2023-04-25 19:28:07 -04:00
parent ebea014083
commit 084a2f7618
Signed by: NotNite
GPG Key ID: BD91A5402CCEB08A
8 changed files with 185 additions and 43 deletions

View File

@ -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;

View File

@ -8,19 +8,29 @@ datasource db {
} }
model AuthTicket { model AuthTicket {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
username String? ticket String
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? discordAuth DiscordAuth?
} }
model DiscordAuth { model DiscordAuth {
id String @id id String @id
authTicket AuthTicket @relation(fields: [authTicketId], references: [id]) user User @relation(fields: [userId], references: [id])
authTicketId Int @unique userId Int @unique
accessToken String accessToken String
refreshToken String refreshToken String
refreshAt DateTime expiresAt DateTime
} }

View File

@ -1,6 +1,6 @@
import { NextApiRequest } from "next";
import * as ldap from "@/ldap"; import * as ldap from "@/ldap";
import prisma from "@/prisma"; import prisma from "@/prisma";
import { getUserFromRequest } from "@/auth";
type RequestBody = { type RequestBody = {
username: string; username: string;
@ -11,20 +11,14 @@ type RequestBody = {
}; };
export async function POST(request: Request) { export async function POST(request: Request) {
const authorization = request.headers const user = await getUserFromRequest(request);
.get("authorization")
?.replace("Bearer ", "");
if (authorization == null) return new Response(null, { status: 401 });
const user = await prisma.authTicket.findFirst({
where: {
ticket: authorization
}
});
if (user == null) return new Response(null, { status: 401 }); 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 } = const { username, displayName, email, password, avatarBase64 } =
(await request.json()) as RequestBody; (await request.json()) as RequestBody;
@ -89,12 +83,12 @@ export async function POST(request: Request) {
await ldap.createUser(username, displayName, email, avatarBuf); await ldap.createUser(username, displayName, email, avatarBuf);
await ldap.setPassword(username, password); await ldap.setPassword(username, password);
await prisma.authTicket.update({ await prisma.user.update({
where: { where: {
id: user.id id: user.id
}, },
data: { data: {
username: username username
} }
}); });

11
src/app/me/page.tsx Normal file
View File

@ -0,0 +1,11 @@
import { getUserFromPage } from "@/auth";
export default async function Page() {
const user = await getUserFromPage();
if (!user) {
return <p>Not logged in</p>;
}
return <p>{user.username}</p>;
}

View File

@ -1,15 +1,15 @@
import { discordRedirectUri } from "../oauth"; import { discordRedirectUri } from "../oauth";
import { v4 } from "uuid";
export async function GET(request: Request) { export async function GET(request: Request) {
let url = `https://discord.com/oauth2/authorize`; let url = `https://discord.com/oauth2/authorize`;
let state = v4();
let randomAssString = Math.random().toString(36).substring(2, 15);
let params = new URLSearchParams(); let params = new URLSearchParams();
params.set("response_type", "code"); params.set("response_type", "code");
params.set("client_id", process.env.DISCORD_CLIENT_ID); params.set("client_id", process.env.DISCORD_CLIENT_ID);
params.set("scope", "guilds identify email"); params.set("scope", "guilds identify email");
params.set("state", randomAssString); params.set("state", state);
params.set("redirect_uri", discordRedirectUri()); params.set("redirect_uri", discordRedirectUri());
params.set("prompt", "consent"); params.set("prompt", "consent");
@ -19,7 +19,7 @@ export async function GET(request: Request) {
status: 302, status: 302,
headers: { headers: {
Location: url, Location: url,
"Set-Cookie": `state=${randomAssString}; Path=/;` "Set-Cookie": `state=${state}; Path=/;`
} }
}); });
} }

View File

@ -39,5 +39,3 @@ export async function getDiscordGuilds(token: string) {
const res: DiscordGuildResponse[] = await req.json(); const res: DiscordGuildResponse[] = await req.json();
return res.map((guild) => guild.id); return res.map((guild) => guild.id);
} }
export const makeTicket = (): string => v4();

View File

@ -2,12 +2,12 @@ import { URLSearchParams } from "url";
import { import {
discordRedirectUri, discordRedirectUri,
DiscordAccessTokenResponse, DiscordAccessTokenResponse,
makeTicket,
getDiscordID, getDiscordID,
getDiscordGuilds getDiscordGuilds
} from "../oauth"; } from "../oauth";
import { cookies } from "next/dist/client/components/headers"; import { cookies } from "next/dist/client/components/headers";
import prisma from "@/prisma"; import prisma from "@/prisma";
import { v4 } from "uuid";
export async function GET(request: Request) { export async function GET(request: Request) {
let url = new URL(request.url); let url = new URL(request.url);
@ -51,21 +51,62 @@ export async function GET(request: Request) {
if (!allowed) if (!allowed)
return new Response("not permitted to register account", { status: 403 }); 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: { data: {
username: null, authTicket: {
ticket: makeTicket(), connect: {
discordAuth: { id: authTicket.id
connectOrCreate: {
where: {
id
},
create: {
id,
accessToken: tokenBody.access_token,
refreshToken: tokenBody.refresh_token,
refreshAt: new Date(Date.now() + tokenBody.expires_in * 1000)
}
} }
} }
} }
@ -74,7 +115,7 @@ export async function GET(request: Request) {
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
"Set-Cookie": `ticket=${user.ticket}; Path=/;`, "Set-Cookie": `ticket=${authTicket.ticket}; Path=/;`,
Location: "/register" Location: "/register"
} }
}); });

43
src/auth.ts Normal file
View File

@ -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;
}