1.1.0 #29
+88
-2
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user