diff --git a/apps/website/.example.dev.vars b/apps/website/.example.dev.vars new file mode 100644 index 0000000..754628a --- /dev/null +++ b/apps/website/.example.dev.vars @@ -0,0 +1,6 @@ +PRO= + +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_BOT_TOKEN= +DISCORD_REDIRECT_URI=/auth/callback/discord diff --git a/apps/website/.gitignore b/apps/website/.gitignore index 6d4c0aa..57729bd 100644 --- a/apps/website/.gitignore +++ b/apps/website/.gitignore @@ -19,3 +19,6 @@ pnpm-debug.log* # macOS-specific files .DS_Store + +.dev.vars +.wrangler/ diff --git a/apps/website/astro.config.ts b/apps/website/astro.config.ts index d3d8d91..2c9091c 100644 --- a/apps/website/astro.config.ts +++ b/apps/website/astro.config.ts @@ -1,37 +1,29 @@ import path from "path"; import { fileURLToPath } from "url"; import { defineConfig } from "astro/config"; +import preact from "@astrojs/preact"; import sitemap from "@astrojs/sitemap"; -import robotsTxt from "astro-robots-txt"; -import compress from "astro-compress"; import tailwind from "@astrojs/tailwind"; +import cloudflare from "@astrojs/cloudflare"; + import { CONFIG } from "./src/config"; -import image from "@astrojs/image"; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); // https://astro.build/config export default defineConfig({ site: CONFIG.origin, - base: "/", - trailingSlash: "always", - output: "static", - integrations: [sitemap(), robotsTxt({ - policy: [{ - userAgent: "*" - }], - sitemap: true - }), compress({ - css: true, - html: true, - img: true, - js: true, - svg: true - }), tailwind(), image()], + integrations: [sitemap(), tailwind(), preact()], + output: "server", + adapter: cloudflare(), + security: { + checkOrigin: true, + }, vite: { resolve: { alias: { - "~": path.resolve(__dirname, "./src") - } - } - } -}); \ No newline at end of file + "~": path.resolve(__dirname, "./src"), + }, + }, + }, +}); diff --git a/apps/website/auth.config.ts b/apps/website/auth.config.ts new file mode 100644 index 0000000..8260a06 --- /dev/null +++ b/apps/website/auth.config.ts @@ -0,0 +1,50 @@ +import Discord from "@auth/core/providers/discord"; +import { defineConfig } from "auth-astro"; +import type { User } from "~/env"; + +import dotenv from "dotenv"; + +dotenv.config(); + +const { env } = process; + +export default defineConfig({ + secret: env.AUTH_SECRET, + providers: [ + Discord({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + authorization: + "https://discord.com/api/oauth2/authorize?scope=guilds+identify+email", + }), + ], + callbacks: { + async jwt({ token, account, profile }) { + if (account && profile) { + token.accessToken = account.access_token; + token.id = profile.id; + + token.name = profile.username as string; + token.global_name = profile.global_name; + } + + return token; + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id as string; + (session.user as unknown as User).global_name = + token.global_name as string; + + (session.user as unknown as User).discordAccessToken = + token.accessToken as string; + } + + return session; + }, + }, + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + }, +}); diff --git a/apps/website/bun.lockb b/apps/website/bun.lockb new file mode 100755 index 0000000..9cfc3b0 Binary files /dev/null and b/apps/website/bun.lockb differ diff --git a/apps/website/migrations/0001_auth.sql b/apps/website/migrations/0001_auth.sql new file mode 100644 index 0000000..799cb93 --- /dev/null +++ b/apps/website/migrations/0001_auth.sql @@ -0,0 +1,18 @@ +-- Migration number: 0001 2024-08-03T15:43:57.349Z + +CREATE TABLE IF NOT EXISTS user ( + id TEXT NOT NULL PRIMARY KEY, + discord_id TEXT NOT NULL UNIQUE, + username TEXT NOT NULL, + avatar TEXT NOT NULL, + access_token TEXT NOT NULL, + access_token_expiration NUMBER NOT NULL, + refresh_token TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS session ( + id TEXT NOT NULL PRIMARY KEY, + expires_at INTEGER NOT NULL, + user_id TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user (id) +); diff --git a/apps/website/package.json b/apps/website/package.json index c979ac4..5de2235 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -9,16 +9,19 @@ "astro": "astro" }, "dependencies": { - "@astrojs/image": "^0.16.5", + "@astrojs/cloudflare": "^11.0.1", + "@astrojs/preact": "^3.5.1", "@astrojs/prefetch": "^0.2.1", - "@astrojs/sitemap": "^1.2.1", - "@astrojs/tailwind": "^3.1.1", + "@astrojs/sitemap": "^3.1.6", + "@astrojs/tailwind": "^5.1.0", + "@lucia-auth/adapter-sqlite": "^3.0.2", "@tailwindcss/typography": "^0.5.9", - "astro": "^2.2.1", - "astro-compress": "2.2.11", + "arctic": "^1.9.2", + "astro": "^4.11.5", "astro-google-fonts-optimizer": "^0.2.2", "astro-icon": "^0.8.0", - "astro-robots-txt": "^0.4.1", + "lucia": "^3.2.0", + "preact": "^10.23.1", "tailwindcss": "^3.3.1" }, "devDependencies": { diff --git a/apps/website/src/components/Container.astro b/apps/website/src/components/Container.astro new file mode 100644 index 0000000..21ead53 --- /dev/null +++ b/apps/website/src/components/Container.astro @@ -0,0 +1,7 @@ +--- +const { class: className } = Astro.props; +--- + +
+ +
diff --git a/apps/website/src/components/Settings.astro b/apps/website/src/components/Settings.astro new file mode 100644 index 0000000..257b943 --- /dev/null +++ b/apps/website/src/components/Settings.astro @@ -0,0 +1,40 @@ + diff --git a/apps/website/src/components/dashboard/GuildInfo.astro b/apps/website/src/components/dashboard/GuildInfo.astro new file mode 100644 index 0000000..de06da9 --- /dev/null +++ b/apps/website/src/components/dashboard/GuildInfo.astro @@ -0,0 +1,25 @@ +--- +import { Image } from "astro:assets"; + +interface Props { + id: string; + name: string; + icon: string; +} + +const { id, name, icon } = Astro.props; +--- + +
+ icon + +

+ {name} +

+
diff --git a/apps/website/src/components/dashboard/UserInfo.astro b/apps/website/src/components/dashboard/UserInfo.astro new file mode 100644 index 0000000..88cf86b --- /dev/null +++ b/apps/website/src/components/dashboard/UserInfo.astro @@ -0,0 +1,19 @@ +--- +import { Image } from "astro:assets"; + +const user = Astro.locals.user; +--- + +
+ icon + +

+ @{user.username} +

+
diff --git a/apps/website/src/components/preact/Guild.tsx b/apps/website/src/components/preact/Guild.tsx new file mode 100644 index 0000000..6195fc8 --- /dev/null +++ b/apps/website/src/components/preact/Guild.tsx @@ -0,0 +1,34 @@ +import type { Guild } from "~/env"; + +interface Props { + guild: Guild; + mutual: boolean; +} + +export default function Guild({ guild, mutual }: Props) { + return ( + +
+ icon + +

{guild.name}

+
+
+ ); +} diff --git a/apps/website/src/components/preact/GuildSelector.tsx b/apps/website/src/components/preact/GuildSelector.tsx new file mode 100644 index 0000000..f9d89ee --- /dev/null +++ b/apps/website/src/components/preact/GuildSelector.tsx @@ -0,0 +1,31 @@ +import { Component } from "preact"; +import Guild from "./Guild"; +import LoadingGuild from "./LoadingGuild"; +import type { MutualeGuild } from "~/env"; + +interface State { + guilds: MutualeGuild[] | null; +} + +export default class GuildSelector extends Component<{}, State> { + constructor(props) { + super(props); + + this.state = { + guilds: null, + }; + } + + async componentDidMount() { + const response = await fetch("/api/user/guilds"); + const data = await response.json(); + this.setState({ guilds: data }); + } + + render() { + const { guilds } = this.state; + if (!guilds) return [...Array(15)].map(() => ); + + return guilds.map((g) => ); + } +} diff --git a/apps/website/src/components/preact/LoadingGuild.tsx b/apps/website/src/components/preact/LoadingGuild.tsx new file mode 100644 index 0000000..e575bcf --- /dev/null +++ b/apps/website/src/components/preact/LoadingGuild.tsx @@ -0,0 +1,9 @@ +export default function LoadingGuild() { + return ( +
+
+
+
+
+ ); +} diff --git a/apps/website/src/env.d.ts b/apps/website/src/env.d.ts index f964fe0..6577947 100644 --- a/apps/website/src/env.d.ts +++ b/apps/website/src/env.d.ts @@ -1 +1,36 @@ /// + +type D1Database = import("@cloudflare/workers-types").D1Database; +type ENV = { + GITHUB_APP_NAME: string; + DISCORD_CLIENT_ID: string; + DISCORD_CLIENT_SECRET: string; + DISCORD_BOT_TOKEN: string; + DISCORD_REDIRECT_URI: string; + DB: D1Database; +}; + +export interface Guild { + id: string; + name: string; + icon: string; + permissions: string; + owner: boolean; +} + +export interface MutualeGuild { + mutual: boolean; + guild: Guild; +} + +type Runtime = import("@astrojs/cloudflare").Runtime; + +// https://github.com/withastro/astro/issues/7394#issuecomment-1975657601 +declare namespace App { + interface Locals extends Runtime { + discord: import("arctic").Discord; + lucia: import("lucia").Lucia; + session: import("lucia").Session | null; + user: import("lucia").User | null; + } +} diff --git a/apps/website/src/lib/auth.ts b/apps/website/src/lib/auth.ts new file mode 100644 index 0000000..c7fa454 --- /dev/null +++ b/apps/website/src/lib/auth.ts @@ -0,0 +1,54 @@ +import { Lucia } from "lucia"; +import { D1Adapter } from "@lucia-auth/adapter-sqlite"; +import { Discord } from "arctic"; + +export function initializeLucia(D1: D1Database) { + const adapter = new D1Adapter(D1, { + user: "user", + session: "session", + }); + return new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: import.meta.env.PROD, + }, + }, + getUserAttributes: (attributes) => { + return { + discordId: attributes.discord_id, + username: attributes.username, + avatar: attributes.avatar, + sensitive: { + accessToken: attributes.access_token, + refreshToken: attributes.refresh_token, + accessTokenExpiresAt: attributes.access_token_expiration, + }, + }; + }, + }); +} + +export function initializeDiscord( + discordClientId: string, + discordClientSecret: string, + redirectUri: string +) { + return new Discord(discordClientId, discordClientSecret, redirectUri); +} + +declare module "lucia" { + interface Register { + Lucia: ReturnType; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + discord_id: number; + username: string; + avatar: string; + access_token: string; + refresh_token: string; + access_token_expiration: number; +} diff --git a/apps/website/src/lib/guilds.ts b/apps/website/src/lib/guilds.ts new file mode 100644 index 0000000..502c7da --- /dev/null +++ b/apps/website/src/lib/guilds.ts @@ -0,0 +1,63 @@ +import type { Guild, MutualeGuild } from "~/env"; +import { isUserEligible } from "./user"; + +export async function getGuild( + user, + guildId: string, + env +): Promise { + const guilds = await getUserGuilds(user); + const guild = guilds.find((g) => g.id === guildId); + + if (!guild || !(await isMutualGuild(guild, env))) return undefined; + + return guild; +} + +export async function getUserGuilds(user): Promise { + const discordApiGuildsResponse = await fetch( + "https://discord.com/api/v10/users/@me/guilds", + { + headers: { + Authorization: `Bearer ${user.sensitive.accessToken as string}`, + "Cache-Control": "max-age=300", + }, + } + ); + + return (await discordApiGuildsResponse.json()) as Guild[]; +} + +export async function filterUserGuilds( + guilds: Guild[], + env +): Promise { + const filtered = guilds + .filter(isUserEligible) + .sort((a, b) => Number(b.owner) - Number(a.owner)); + + const result: MutualeGuild[] = []; + + for (const guild of filtered) { + const mutual = await isMutualGuild(guild, env); + + result.push({ mutual, guild }); + } + + result.sort((a, b) => Number(b.mutual) - Number(a.mutual)); + + return result; +} + +export async function isMutualGuild(guild: Guild, env): Promise { + const res = await fetch( + `https://discord.com/api/v10/guilds/${guild.id}/members/${env.DISCORD_CLIENT_ID}`, + { + headers: { + Authorization: `Bot ${env.DISCORD_BOT_TOKEN}`, + }, + } + ); + + return res.ok; +} diff --git a/apps/website/src/lib/user.ts b/apps/website/src/lib/user.ts new file mode 100644 index 0000000..a98dab3 --- /dev/null +++ b/apps/website/src/lib/user.ts @@ -0,0 +1,6 @@ +import type { Guild } from "~/env"; + +// Checks if user has AMDINISTRATOR permissions in the guild +export function isUserEligible(guild: Guild): boolean { + return (BigInt(guild.permissions) & 0x8n) == 0x8n; +} diff --git a/apps/website/src/middleware.ts b/apps/website/src/middleware.ts new file mode 100644 index 0000000..7c50700 --- /dev/null +++ b/apps/website/src/middleware.ts @@ -0,0 +1,40 @@ +import { initializeLucia, initializeDiscord } from "./lib/auth"; +import { defineMiddleware } from "astro:middleware"; + +export const onRequest = defineMiddleware(async (context, next) => { + const lucia = initializeLucia(context.locals.runtime.env.DB); + context.locals.lucia = lucia; + const discord = initializeDiscord( + context.locals.runtime.env.DISCORD_CLIENT_ID, + context.locals.runtime.env.DISCORD_CLIENT_SECRET, + context.locals.runtime.env.DISCORD_REDIRECT_URI + ); + context.locals.discord = discord; + const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + context.locals.user = null; + context.locals.session = null; + return next(); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + } + context.locals.session = session; + context.locals.user = user; + return next(); +}); diff --git a/apps/website/src/pages/api/user/guilds.ts b/apps/website/src/pages/api/user/guilds.ts new file mode 100644 index 0000000..8328a71 --- /dev/null +++ b/apps/website/src/pages/api/user/guilds.ts @@ -0,0 +1,16 @@ +import type { APIRoute } from "astro"; +import { filterUserGuilds, getUserGuilds } from "~/lib/guilds"; + +export const GET: APIRoute = async ({ locals }) => { + const user = locals.user; + if (!user) { + return new Response(null, { + status: 401, + }); + } + + const guilds = await getUserGuilds(user); + const res = await filterUserGuilds(guilds, locals.runtime.env); + + return Response.json(res); +}; diff --git a/apps/website/src/pages/auth/callback/discord.ts b/apps/website/src/pages/auth/callback/discord.ts new file mode 100644 index 0000000..e180945 --- /dev/null +++ b/apps/website/src/pages/auth/callback/discord.ts @@ -0,0 +1,100 @@ +import { OAuth2RequestError } from "arctic"; +import type { APIContext } from "astro"; +import { generateIdFromEntropySize } from "lucia"; + +type UserRow = { + id: string; +}; + +export async function GET(context: APIContext): Promise { + const code = context.url.searchParams.get("code"); + const state = context.url.searchParams.get("state"); + const storedState = context.cookies.get("discord_oauth_state")?.value ?? null; + if (!code || !state || !storedState || state !== storedState) { + return new Response(null, { + status: 400, + }); + } + + try { + const tokens = await context.locals.discord.validateAuthorizationCode(code); + const disocrdUserResponse = await fetch( + "https://discord.com/api/users/@me", + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + } + ); + + const discordUser: DiscordUser = await disocrdUserResponse.json(); + + const existingUser = await context.locals.runtime.env.DB.prepare( + "SELECT * FROM user WHERE discord_id = ?1" + ) + .bind(discordUser.id) + .first(); + + if (existingUser) { + const session = await context.locals.lucia.createSession( + existingUser.id, + {} + ); + + const sessionCookie = context.locals.lucia.createSessionCookie( + session.id + ); + + context.cookies.set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + return context.redirect("/dashboard"); + } + + const userId = generateIdFromEntropySize(10); // 16 characters long + + await context.locals.runtime.env.DB.prepare( + "INSERT INTO user (id, discord_id, username, avatar, access_token, access_token_expiration, refresh_token) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" + ) + .bind( + userId, + discordUser.id, + discordUser.username, + discordUser.avatar, + tokens.accessToken, + tokens.accessTokenExpiresAt.getTime(), + tokens.refreshToken + ) + .run(); + + const session = await context.locals.lucia.createSession(userId, {}); + const sessionCookie = context.locals.lucia.createSessionCookie(session.id); + context.cookies.set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + return context.redirect("/dashboard"); + } catch (e) { + console.error(e.message); + + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400, + }); + } + return new Response(null, { + status: 500, + }); + } +} + +interface DiscordUser { + id: string; + username: string; + avatar: string; +} diff --git a/apps/website/src/pages/auth/login.ts b/apps/website/src/pages/auth/login.ts new file mode 100644 index 0000000..1e63010 --- /dev/null +++ b/apps/website/src/pages/auth/login.ts @@ -0,0 +1,20 @@ +import { generateState } from "arctic"; + +import type { APIContext } from "astro"; + +export async function GET(context: APIContext): Promise { + const state = generateState(); + const url = await context.locals.discord.createAuthorizationURL(state, { + scopes: ["identify", "guilds"], + }); + + context.cookies.set("discord_oauth_state", state, { + path: "/", + secure: import.meta.env.PROD, + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }); + + return context.redirect(url.toString()); +} diff --git a/apps/website/src/pages/auth/logout.ts b/apps/website/src/pages/auth/logout.ts new file mode 100644 index 0000000..4cbaaa7 --- /dev/null +++ b/apps/website/src/pages/auth/logout.ts @@ -0,0 +1,20 @@ +import type { APIContext } from "astro"; + +export async function GET(context: APIContext): Promise { + if (!context.locals.session) { + return new Response(null, { + status: 401, + }); + } + + await context.locals.lucia.invalidateSession(context.locals.session.id); + + const sessionCookie = context.locals.lucia.createBlankSessionCookie(); + context.cookies.set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + + return context.redirect("/"); +} diff --git a/apps/website/src/pages/dashboard/guilds/[id].astro b/apps/website/src/pages/dashboard/guilds/[id].astro new file mode 100644 index 0000000..fa2a750 --- /dev/null +++ b/apps/website/src/pages/dashboard/guilds/[id].astro @@ -0,0 +1,36 @@ +--- +import GuildInfo from "~/components/dashboard/GuildInfo.astro"; +import UserInfo from "~/components/dashboard/UserInfo.astro"; +import Layout from "~/layouts/Layout.astro"; +import { getGuild } from "~/lib/guilds"; +import { isUserEligible } from "~/lib/user"; + +const { id } = Astro.params; + +const user = Astro.locals.user; +if (!user || !id) { + return Astro.redirect("/auth/login"); +} + +const guild = await getGuild(user, id, Astro.locals.runtime.env); +if (!guild) { + return Astro.redirect("/dashboard"); +} + +const eligible = isUserEligible(guild); +if (!eligible) { + return Astro.redirect("/dashboard"); +} +--- + + +
+ + +

on

+ + +
+ + You can manage guild {guild.name} with id {guild.id} +
diff --git a/apps/website/src/pages/dashboard/index.astro b/apps/website/src/pages/dashboard/index.astro new file mode 100644 index 0000000..5533745 --- /dev/null +++ b/apps/website/src/pages/dashboard/index.astro @@ -0,0 +1,22 @@ +--- +import Layout from "~/layouts/Layout.astro"; +import Container from "~/components/Container.astro"; +import GuildSelector from "~/components/preact/GuildSelector"; +import UserInfo from "~/components/dashboard/UserInfo.astro"; + +if (!Astro.locals.user) { + return Astro.redirect("/auth/login"); +} +--- + + + + + +
+ +
+
+
diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro index 8009f4b..fa5021e 100644 --- a/apps/website/src/pages/index.astro +++ b/apps/website/src/pages/index.astro @@ -2,8 +2,11 @@ import Layout from "~/layouts/Layout.astro"; import Invite from "~/components/Invite.astro"; import Computer from "~/components/Computer.astro"; -import { Image } from "@astrojs/image/components"; +import Settings from "~/components/Settings.astro"; import Logo from "~/assets/logo.png"; +import { Image } from "astro:assets"; + +export const prerender = true; --- @@ -32,15 +35,22 @@ import Logo from "~/assets/logo.png";
Add to Discord + + + Dashboard diff --git a/apps/website/tailwind.config.cjs b/apps/website/tailwind.config.cjs index af6fbd3..160ad84 100644 --- a/apps/website/tailwind.config.cjs +++ b/apps/website/tailwind.config.cjs @@ -1,15 +1,18 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], - theme: { - extend: { - colors: { - dark: "#111111", - }, - fontFamily: { - body: ["Montserrat", "sans-serif"], - }, - }, - }, - plugins: [], + content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], + theme: { + extend: { + colors: { + dark: "#111111", + "dark-100": "#1A1A1A", + blue: "#5865F2", + gold: "#FFA500", + }, + fontFamily: { + body: ["Montserrat", "sans-serif"], + }, + }, + }, + plugins: [], }; diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 8543cd5..f35cf4f 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "astro/tsconfigs/base", + "extends": "astro/tsconfigs/strictest", "compilerOptions": { "strictNullChecks": true, "allowJs": true, "baseUrl": ".", "paths": { "~/*": ["src/*"] - } + }, + "types": ["./src/env.d.ts"] } } diff --git a/apps/website/worker-configuration.d.ts b/apps/website/worker-configuration.d.ts new file mode 100644 index 0000000..94aec6d --- /dev/null +++ b/apps/website/worker-configuration.d.ts @@ -0,0 +1,11 @@ +// Generated by Wrangler on Sat Aug 03 2024 17:34:42 GMT+0200 (Central European Summer Time) +// by running `wrangler types` + +interface Env { + PRO: string; + DISCORD_CLIENT_ID: string; + DISCORD_CLIENT_SECRET: string; + DISCORD_BOT_TOKEN: string; + DISCORD_REDIRECT_URI: string; + DB: D1Database; +} diff --git a/apps/website/wrangler.toml b/apps/website/wrangler.toml new file mode 100644 index 0000000..644039c --- /dev/null +++ b/apps/website/wrangler.toml @@ -0,0 +1,6 @@ +name = "roles-bot" + +[[d1_databases]] +binding = "DB" # i.e. available in your Worker on env.DB +database_name = "roles-bot-dashboard" +database_id = "fdc6f902-9142-44bf-9be9-b38d351ffd27"