Setup account creation through discord OAuth2

This commit is contained in:
Dominic Ferrando
2026-08-16 00:00:50 -04:00
parent d4a450ab3e
commit 8db1db2ec6
3 changed files with 102 additions and 3 deletions
+88 -2
View File
@@ -3,7 +3,7 @@ import {
PlayerSchema, PlayerSchema,
} from "@blade-and-brawn/domain" } from "@blade-and-brawn/domain"
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import { Elysia, NotFoundError, status, t } from "elysia"; import { Elysia, NotFoundError, redirect, status, t } from "elysia";
import { import {
PrintfulError, PrintfulError,
WebflowError, WebflowError,
@@ -21,6 +21,7 @@ import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
import { StandardsService } from "./services/standards"; import { StandardsService } from "./services/standards";
import { EventsService, EventStatusSchema } from "./services/events"; import { EventsService, EventStatusSchema } from "./services/events";
import { AccountsService } from "./services/accounts"; import { AccountsService } from "./services/accounts";
import { Value } from "@sinclair/typebox/value";
// CONSTANTS // CONSTANTS
// ----------------------- // -----------------------
@@ -76,7 +77,7 @@ export const app = new Elysia()
], ],
}), }),
) )
.guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()), discordOAuthState: t.Optional(t.String()) }) })
.use(authPlugin) .use(authPlugin)
.error({ .error({
@@ -140,6 +141,91 @@ export const app = new Elysia()
}); });
}, { body: t.Object({ password: t.String() }) }) }, { body: t.Object({ password: t.String() }) })
.group("/auth/discord", (app) => app
.get("/login", async ({ cookie: { discordOAuthState } }) => {
const state = crypto.randomUUID();
discordOAuthState.set({
value: state,
path: "/",
maxAge: 60 * 10, // 10 min
sameSite: "lax",
httpOnly: true,
secure: env.NODE_ENV === "production"
});
const url = new URL("https://discord.com/api/oauth2/authorize");
url.searchParams.set("client_id", env.BOT_CLIENT_ID);
url.searchParams.set("redirect_uri", env.BOT_REDIRECT_URL);
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "identify email");
url.searchParams.set("state", state);
return redirect(url.toString(), 302);
})
.get("/callback", async ({ query, cookie: { discordOAuthState, auth }, jwt }) => {
if (query.error) throw status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error });
if (!query.state) throw status(400, "No Discord OAuth2 state query parameter provided");
if (query.state !== discordOAuthState.value) throw status(400, "Invalid Discord OAuth state");
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: env.BOT_CLIENT_ID,
client_secret: env.BOT_CLIENT_SECRET,
grant_type: "authorization_code",
code: query.code ?? "",
redirect_uri: env.BOT_REDIRECT_URL
}).toString()
});
if (!tokenRes.ok) throw status(502, { error: await tokenRes.json() });
const tokenResPayload = await tokenRes.json();
Value.Assert(t.Object({ access_token: t.String() }), tokenResPayload);
const identityRes = await fetch("https://discord.com/api/users/@me", {
headers: { "Authorization": `Bearer ${tokenResPayload.access_token}` }
});
if (!identityRes.ok) throw status(502, { error: await identityRes.json() });
const identityResPayload = await identityRes.json();
Value.Assert(t.Object({ id: t.String(), email: t.Optional(t.String()), username: t.String() }), identityResPayload);
// Create the account
const existingAccount = await s.Accounts.get(`@${identityResPayload.id}`);
if (!existingAccount) {
await s.Accounts.create(
identityResPayload.id,
identityResPayload.email,
identityResPayload.username
);
}
// auth.set({
// value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }),
// path: "/",
// maxAge: 60 * 60 * 24 * 7,
// sameSite: "lax",
// httpOnly: true,
// secure: env.NODE_ENV === "production",
// domain: env.NODE_ENV === "production" ?
// ".bladeandbrawn.com" :
// undefined
// });
}, {
query: t.Object({
code: t.Optional(t.String()),
state: t.Optional(t.String()),
error: t.Optional(t.String()),
error_description: t.Optional(t.String()),
})
})
)
// CALCULATOR // CALCULATOR
.group("/calculator", (app) => app .group("/calculator", (app) => app
// Non-authenticated // Non-authenticated
+11 -1
View File
@@ -13,9 +13,19 @@ export class AccountsService {
: ["accounts.id", "=", id]; : ["accounts.id", "=", id];
} }
async create(discordId: string, email?: string, name?: string) {
await db.insertInto("accounts")
.values({
discord_id: discordId,
name: name ?? "",
email: email ?? null
})
.execute();
}
async get(id: string) { async get(id: string) {
return await (db).selectFrom("accounts") return await (db).selectFrom("accounts")
.selectAll() .select(["discord_id", "name", "email", "gender"])
.where(...AccountsService.idComparison(id)) .where(...AccountsService.idComparison(id))
.executeTakeFirst(); .executeTakeFirst();
} }
+3
View File
@@ -26,6 +26,9 @@ export const env = {
MAX_WORKER_COUNT: requireEnv("MAX_WORKER_COUNT"), MAX_WORKER_COUNT: requireEnv("MAX_WORKER_COUNT"),
NODE_ENV: optionEnv("NODE_ENV", "development"), NODE_ENV: optionEnv("NODE_ENV", "development"),
LOG_LEVEL: optionEnv("LOG_LEVEL", "info"), LOG_LEVEL: optionEnv("LOG_LEVEL", "info"),
BOT_CLIENT_ID: requireEnv("BOT_CLIENT_ID"),
BOT_CLIENT_SECRET: requireEnv("BOT_CLIENT_SECRET"),
BOT_REDIRECT_URL: requireEnv("BOT_REDIRECT_URL"),
}; };
export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT); export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT);