Setup account creation through discord OAuth2
This commit is contained in:
+88
-2
@@ -3,7 +3,7 @@ import {
|
||||
PlayerSchema,
|
||||
} from "@blade-and-brawn/domain"
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { Elysia, NotFoundError, status, t } from "elysia";
|
||||
import { Elysia, NotFoundError, redirect, status, t } from "elysia";
|
||||
import {
|
||||
PrintfulError,
|
||||
WebflowError,
|
||||
@@ -21,6 +21,7 @@ import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
|
||||
import { StandardsService } from "./services/standards";
|
||||
import { EventsService, EventStatusSchema } from "./services/events";
|
||||
import { AccountsService } from "./services/accounts";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
|
||||
// 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)
|
||||
|
||||
.error({
|
||||
@@ -140,6 +141,91 @@ export const app = new Elysia()
|
||||
});
|
||||
}, { 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
|
||||
.group("/calculator", (app) => app
|
||||
// Non-authenticated
|
||||
|
||||
@@ -13,9 +13,19 @@ export class AccountsService {
|
||||
: ["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) {
|
||||
return await (db).selectFrom("accounts")
|
||||
.selectAll()
|
||||
.select(["discord_id", "name", "email", "gender"])
|
||||
.where(...AccountsService.idComparison(id))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ export const env = {
|
||||
MAX_WORKER_COUNT: requireEnv("MAX_WORKER_COUNT"),
|
||||
NODE_ENV: optionEnv("NODE_ENV", "development"),
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user