Implement full discord OAuth api flow

This commit is contained in:
Dominic Ferrando
2026-08-16 01:51:12 -04:00
parent 8db1db2ec6
commit 17af2f3d1f
5 changed files with 70 additions and 41 deletions
+61 -32
View File
@@ -27,6 +27,7 @@ import { Value } from "@sinclair/typebox/value";
// -----------------------
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec
const JWT_EXP = "1d";
const JWT_EXP_SECONDS = 60 * 60 * 24; // keep in sync with JWT_EXP; used for cookie maxAge
// SERVICES
// -----------------------
@@ -52,12 +53,25 @@ const authPlugin = new Elysia({ name: "auth" })
.use(jwt({ name: "jwt", secret: env.AUTH_SECRET }))
.guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) })
.macro({
auth: {
authAdmin: {
async resolve({ jwt, cookie: { auth } }) {
const token = auth.value && await jwt.verify(auth.value);
if (!token || !token.sessionId) throw status(401, "Unauthorized");
if (!token || token.role !== "admin" || !token.sessionId) throw status(401, "Unauthorized");
return { sessionId: token.sessionId.toString() };
}
},
authUser: {
async resolve({ jwt, cookie: { auth } }) {
const token = auth.value && await jwt.verify(auth.value);
if (!token || !token.sessionId || (token.role !== "user" && token.role !== "admin")) throw status(401, "Unauthorized");
if (token.role === "user" && !token.accountId) throw status(401, "Unauthorized");
return {
sessionId: token.sessionId.toString(),
isAdmin: token.role === "admin",
accountId: token.role === "user" ? token.accountId!.toString() : null,
};
}
}
});
@@ -72,12 +86,18 @@ export const app = new Elysia()
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
// testing
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn-test\.webflow\.io$/i,
// development
"http://localhost:5173",
],
}),
)
.guard({ cookie: t.Cookie({ auth: t.Optional(t.String()), discordOAuthState: t.Optional(t.String()) }) })
.guard({
cookie: t.Cookie({
auth: t.Optional(t.String()),
authDiscord: t.Optional(t.String())
})
})
.use(authPlugin)
.error({
@@ -124,14 +144,14 @@ export const app = new Elysia()
.get("/health", () => ({ status: "ok" }))
// AUTHENTICATION
.post("/auth/login", async ({ jwt, body, cookie: { auth } }) => {
.post("/auth/admin/login", async ({ jwt, body, cookie: { auth } }) => {
const match = crypto.timingSafeEqual(sha256Sum(body.password), Buffer.from(env.ADMIN_PASSWORD, "hex"));
if (!match) throw status(401, "Invalid credentials");
auth.set({
value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }),
value: await jwt.sign({ role: "admin", sessionId: randomUUIDv7(), exp: JWT_EXP }),
path: "/",
maxAge: 60 * 60 * 24 * 7,
maxAge: JWT_EXP_SECONDS,
sameSite: "lax",
httpOnly: true,
secure: env.NODE_ENV === "production",
@@ -142,10 +162,10 @@ export const app = new Elysia()
}, { body: t.Object({ password: t.String() }) })
.group("/auth/discord", (app) => app
.get("/login", async ({ cookie: { discordOAuthState } }) => {
.get("/login", async ({ cookie: { authDiscord } }) => {
const state = crypto.randomUUID();
discordOAuthState.set({
authDiscord.set({
value: state,
path: "/",
maxAge: 60 * 10, // 10 min
@@ -163,11 +183,11 @@ export const app = new Elysia()
return redirect(url.toString(), 302);
})
.get("/callback", async ({ query, cookie: { discordOAuthState, auth }, jwt }) => {
.get("/callback", async ({ query, cookie: { authDiscord, 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");
if (query.state !== authDiscord.value) throw status(400, "Invalid Discord OAuth state");
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
@@ -182,7 +202,10 @@ export const app = new Elysia()
redirect_uri: env.BOT_REDIRECT_URL
}).toString()
});
if (!tokenRes.ok) throw status(502, { error: await tokenRes.json() });
if (!tokenRes.ok) {
const errorBody = await tokenRes.json().catch(() => null);
throw status(502, { error: errorBody ?? "Discord token exchange failed" });
}
const tokenResPayload = await tokenRes.json();
Value.Assert(t.Object({ access_token: t.String() }), tokenResPayload);
@@ -190,32 +213,38 @@ export const app = new Elysia()
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() });
if (!identityRes.ok) {
const errorBody = await identityRes.json().catch(() => null);
throw status(502, { error: errorBody ?? "Failed to fetch Discord identity" });
}
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(
let accountId = (await s.Accounts.get(`@${identityResPayload.id}`))?.id;
if (!accountId) {
accountId = (await s.Accounts.create(
identityResPayload.id,
identityResPayload.email,
identityResPayload.username
);
)).id;
}
// 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
// });
// Authenticate
auth.set({
value: await jwt.sign({ role: "user", sessionId: randomUUIDv7(), accountId, exp: JWT_EXP }),
path: "/",
maxAge: JWT_EXP_SECONDS,
sameSite: env.NODE_ENV === "production" ? "lax" : "none",
httpOnly: true,
secure: true,
domain: env.NODE_ENV === "production" ?
".bladeandbrawn.com" :
undefined
});
return redirect(env.BOT_LOGIN_REDIRECT_URL, 302);
}, {
query: t.Object({
code: t.Optional(t.String()),
@@ -238,7 +267,7 @@ export const app = new Elysia()
}),
})
// Authenticated
.guard({ auth: true })
.guard({ authAdmin: true })
.get("/standards/config", async () => {
return await s.Calculator.Standards.Config.get();
})
@@ -248,7 +277,7 @@ export const app = new Elysia()
body: t.Object({ standardsConfigId: t.String() })
})
)
.group("/standards", { auth: true }, (app) => app
.group("/standards", { authAdmin: true }, (app) => app
.post("/configs", async ({ body: { name, datasetId, params } }) => {
return await s.Standards.Configs.create(name, datasetId, params);
}, {
@@ -288,7 +317,7 @@ export const app = new Elysia()
)
// COMMERCE
.group("/commerce", { auth: true }, (app) => app
.group("/commerce", { authAdmin: true }, (app) => app
.group("/products", (app) => app
.get("/", async ({ query }) => {
const [pProducts, wProducts] = await Promise.all([
@@ -392,7 +421,7 @@ export const app = new Elysia()
)
// EVENTS
.group("/events", { auth: true }, (app) => app
.group("/events", { authAdmin: true }, (app) => app
.get("/", async ({ query }) => {
const events = await s.Events.list({
filter: { status: query.status, type: query.type, group: query.group },
@@ -436,7 +465,7 @@ export const app = new Elysia()
params: t.Object({ id: t.String() })
})
// Authenticated
.guard({ auth: true })
.guard({ authAdmin: true })
.get("/:id", async ({ params: { id } }) => {
const account = await s.Accounts.get(id);
if (!account) throw new NotFoundError("Account not found");