refactor database to split ticket/user/oauth
This commit is contained in:
parent
ebea014083
commit
084a2f7618
|
@ -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;
|
|
@ -9,18 +9,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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=/;`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
|
|
|
@ -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,12 +51,12 @@ 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
|
||||||
data: {
|
// - get the user from the discord auth data
|
||||||
username: null,
|
// - either create a new auth ticket or invalidate the old one
|
||||||
ticket: makeTicket(),
|
// - update the user to point to the new auth ticket
|
||||||
discordAuth: {
|
|
||||||
connectOrCreate: {
|
const discordAuth = await prisma.discordAuth.upsert({
|
||||||
where: {
|
where: {
|
||||||
id
|
id
|
||||||
},
|
},
|
||||||
|
@ -64,9 +64,50 @@ export async function GET(request: Request) {
|
||||||
id,
|
id,
|
||||||
accessToken: tokenBody.access_token,
|
accessToken: tokenBody.access_token,
|
||||||
refreshToken: tokenBody.refresh_token,
|
refreshToken: tokenBody.refresh_token,
|
||||||
refreshAt: new Date(Date.now() + tokenBody.expires_in * 1000)
|
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: {
|
||||||
|
authTicket: {
|
||||||
|
connect: {
|
||||||
|
id: authTicket.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue