diff --git a/.gitignore b/.gitignore index 8f322f0..519c9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +database.db* diff --git a/.vsls.json b/.vsls.json new file mode 100644 index 0000000..e46502d --- /dev/null +++ b/.vsls.json @@ -0,0 +1,3 @@ +{ + "gitignore": "none" +} diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 0000000..1eb7543 --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,12 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + DISCORD_CLIENT_ID: string; + DISCORD_CLIENT_SECRET: string; + DISCORD_ALLOWED_GUILDS: string; + BASE_DOMAIN: string; + } + } +} + +export {}; diff --git a/package-lock.json b/package-lock.json index a3b75b8..afa6e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "gluestick", "version": "0.1.0", "dependencies": { + "@prisma/client": "^4.13.0", "@types/node": "18.16.0", "@types/react": "18.0.38", "@types/react-dom": "18.0.11", @@ -16,7 +17,12 @@ "next": "13.3.1", "react": "18.2.0", "react-dom": "18.2.0", - "typescript": "5.0.4" + "typescript": "5.0.4", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/uuid": "^9.0.1", + "prisma": "^4.13.0" } }, "node_modules/@babel/runtime": { @@ -311,6 +317,38 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@prisma/client": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -362,6 +400,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "node_modules/@typescript-eslint/parser": { "version": "5.59.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", @@ -2673,6 +2717,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.13.0" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3216,6 +3277,14 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3474,6 +3543,25 @@ "tslib": "^2.4.0" } }, + "@prisma/client": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", + "requires": { + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + } + }, + "@prisma/engines": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" + }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -3525,6 +3613,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "@typescript-eslint/parser": { "version": "5.59.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", @@ -5128,6 +5222,15 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, + "prisma": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", + "devOptional": true, + "requires": { + "@prisma/engines": "4.13.0" + } + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5491,6 +5594,11 @@ "punycode": "^2.1.0" } }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0a41d71..5840de6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@prisma/client": "^4.13.0", "@types/node": "18.16.0", "@types/react": "18.0.38", "@types/react-dom": "18.0.11", @@ -17,6 +18,11 @@ "next": "13.3.1", "react": "18.2.0", "react-dom": "18.2.0", - "typescript": "5.0.4" + "typescript": "5.0.4", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/uuid": "^9.0.1", + "prisma": "^4.13.0" } } diff --git a/prisma/migrations/20230425043204_init/migration.sql b/prisma/migrations/20230425043204_init/migration.sql new file mode 100644 index 0000000..1e4bb67 --- /dev/null +++ b/prisma/migrations/20230425043204_init/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "AuthTicket" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT, + "ticket" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "DiscordAuth" ( + "id" TEXT NOT NULL PRIMARY KEY, + "authTicketId" INTEGER NOT NULL, + "accessToken" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL, + "refreshAt" DATETIME NOT NULL, + CONSTRAINT "DiscordAuth_authTicketId_fkey" FOREIGN KEY ("authTicketId") REFERENCES "AuthTicket" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "DiscordAuth_authTicketId_key" ON "DiscordAuth"("authTicketId"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..5069163 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,26 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./database.db" +} + +model AuthTicket { + id Int @id @default(autoincrement()) + username String? + ticket String + discordAuth DiscordAuth? +} + +model DiscordAuth { + id String @id + + authTicket AuthTicket @relation(fields: [authTicketId], references: [id]) + authTicketId Int @unique + + accessToken String + refreshToken String + refreshAt DateTime +} diff --git a/src/app/api/hello/route.ts b/src/app/api/hello/route.ts deleted file mode 100644 index b500e2c..0000000 --- a/src/app/api/hello/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET(request: Request) { - return new Response(":3"); -} diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..408ac7b --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/app/oauth/discord/login/route.ts b/src/app/oauth/discord/login/route.ts new file mode 100644 index 0000000..1ad7215 --- /dev/null +++ b/src/app/oauth/discord/login/route.ts @@ -0,0 +1,25 @@ +import { discordRedirectUri } from "../oauth"; + +export async function GET(request: Request) { + let url = `https://discord.com/oauth2/authorize`; + + let randomAssString = Math.random().toString(36).substring(2, 15); + + 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("redirect_uri", discordRedirectUri()); + params.set("prompt", "consent"); + + url += "?" + params.toString(); + + return new Response(null, { + status: 302, + headers: { + Location: url, + "Set-Cookie": `state=${randomAssString}; Path=/;` + } + }); +} diff --git a/src/app/oauth/discord/oauth.ts b/src/app/oauth/discord/oauth.ts new file mode 100644 index 0000000..5b1c415 --- /dev/null +++ b/src/app/oauth/discord/oauth.ts @@ -0,0 +1,25 @@ +import { v4 } from "uuid"; + +export type DiscordAccessTokenResponse = { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; +}; + +export function discordRedirectUri() { + return `${process.env.BASE_DOMAIN}oauth/discord/redirect`; +} + +export async function getDiscordID(token: string) { + const req = await fetch("https://discord.com/api/users/@me", { + headers: { + Authorization: `Bearer ${token}` + } + }); + const res: { id: string } = await req.json(); + return res.id; +} + +export const makeTicket = (): string => v4(); diff --git a/src/app/oauth/discord/redirect/route.ts b/src/app/oauth/discord/redirect/route.ts new file mode 100644 index 0000000..5dab15e --- /dev/null +++ b/src/app/oauth/discord/redirect/route.ts @@ -0,0 +1,72 @@ +import { URLSearchParams } from "url"; +import { + discordRedirectUri, + DiscordAccessTokenResponse, + makeTicket, + getDiscordID +} from "../oauth"; +import { cookies } from "next/dist/client/components/headers"; +import prisma from "@/prisma"; + +export async function GET(request: Request) { + let url = new URL(request.url); + let code = url.searchParams.get("code"); + let state = url.searchParams.get("state"); + + if (code === null || state === null) + return new Response("missing code/state", { status: 400 }); + console.log(`code: ${code}, state: ${state}`); + + const cookieStore = cookies(); + let cookieState = cookieStore.get("state"); + // prevent forgery + console.log(`state: ${state}, cookieState: ${cookieState?.value}`); + if (cookieState?.value !== state) + return new Response("state is invalid", { status: 400 }); + + let form = new URLSearchParams(); + form.append("client_id", process.env.DISCORD_CLIENT_ID); + form.append("client_secret", process.env.DISCORD_CLIENT_SECRET); + form.append("grant_type", "authorization_code"); + form.append("code", code); + form.append("redirect_uri", discordRedirectUri()); + + let tokenResponse = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: form.toString() + }); + if (!tokenResponse.ok) throw "baby"; + let tokenBody: DiscordAccessTokenResponse = await tokenResponse.json(); + const id = await getDiscordID(tokenBody.access_token); + + const user = await prisma.authTicket.create({ + 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) + } + } + } + } + }); + + return new Response(null, { + status: 302, + headers: { + "Set-Cookie": `ticket=${user.ticket}; Path=/;`, + Location: "/register" + } + }); +} diff --git a/src/app/page.module.css b/src/app/page.module.css index 58777be..f2da1ff 100644 --- a/src/app/page.module.css +++ b/src/app/page.module.css @@ -6,3 +6,22 @@ width: 100vw; height: 100vh; } + +.form { + display: flex; + flex-direction: column; +} + +.form div { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.form div label { + margin-right: 1rem; +} + +.form div input { + width: 15rem; +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 76cd329..ed67766 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,11 @@ import styles from "./page.module.css"; export default function Home() { return (
-

:3

+

+ :3 +
+ login debug +

); } diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..a4e24da --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,48 @@ +import { cookies } from "next/dist/client/components/headers"; +import styles from "../page.module.css"; + +export default function Page() { + const cookieStore = cookies(); + const ticket = cookieStore.get("ticket"); + if (ticket === null) { + return
Ticket is null?
; + } + + return ( +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ ); +} diff --git a/src/prisma.ts b/src/prisma.ts new file mode 100644 index 0000000..c957ceb --- /dev/null +++ b/src/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from "@prisma/client"; +const prisma = new PrismaClient(); +export default prisma;