Compare commits
8 commits
18c6535d1c
...
89507f8412
Author | SHA1 | Date | |
---|---|---|---|
89507f8412 | |||
ed6195e1e2 | |||
d022d9fcf6 | |||
b28d381948 | |||
68e8218b1b | |||
95fee833a1 | |||
ffaf8d989e | |||
6b388729d9 |
54 changed files with 3482 additions and 3231 deletions
|
@ -4,5 +4,8 @@
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"plugins": ["solid"],
|
"plugins": ["solid"],
|
||||||
"extends": ["eslint:recommended", "plugin:solid/typescript"]
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:solid/typescript"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
27
.github/workflows/playwright.yml
vendored
Normal file
27
.github/workflows/playwright.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install -g pnpm && pnpm install
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: pnpm exec playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: pnpm exec playwright test
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
src/drizzle/migrations
|
src/drizzle/migrations
|
||||||
|
log
|
||||||
|
|
||||||
dist
|
dist
|
||||||
.vinxi
|
.vinxi
|
||||||
|
@ -27,3 +28,9 @@ gitignore
|
||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|
10
README.md
10
README.md
|
@ -46,26 +46,24 @@ To get started with li'l Judd, follow the instructions below.
|
||||||
Create a `.env` file in the root directory and add the following variables:
|
Create a `.env` file in the root directory and add the following variables:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_DISCORD_CLIENT=your_discord_client_id
|
VITE_DISCORD_CLIENT_ID=your_discord_client_id
|
||||||
VITE_DISCORD_CLIENT_SECRET=your_discord_client_secret
|
VITE_DISCORD_CLIENT_SECRET=your_discord_client_secret
|
||||||
VITE_DISCORD_BOT_TOKEN=your_discord_bot_token
|
VITE_DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||||
VITE_DISCORD_BOT_PERMISSIONS=18977581952080
|
VITE_DISCORD_OAUTH2_PERMISSIONS=18977581952080
|
||||||
|
|
||||||
VITE_AUTH_SECRET=your_auth_secret
|
VITE_AUTH_SECRET=your_auth_secret
|
||||||
|
|
||||||
VITE_DATABASE_URL=your_database_url
|
VITE_DATABASE_URL=your_database_url
|
||||||
```
|
```
|
||||||
|
|
||||||
Recieve your discord applications client id & secret, as well as the bot token from the [Applications dashboard](https://discord.com/developers/applications/).
|
Recieve your discord applications `CLIENT_ID` & `CLIENT_SECRET`, as well as the bot token from the [Applications dashboard](https://discord.com/developers/applications/).
|
||||||
|
|
||||||
How to generate your `VITE_AUTH_SECRET` with [`openssl rand -base64 32`](https://authjs.dev/reference/core#secret).
|
Your `VITE_AUTH_REDIRECT_URL` should look like this: `https://<hostname>/api/auth/callback/discord`.
|
||||||
|
|
||||||
Composite your `VITE_DATABASE_URL` like [`postgres://postgres:adminadmin@0.0.0.0:5432/db`](https://orm.drizzle.team/docs/get-started-postgresql#postgresjs).
|
Composite your `VITE_DATABASE_URL` like [`postgres://postgres:adminadmin@0.0.0.0:5432/db`](https://orm.drizzle.team/docs/get-started-postgresql#postgresjs).
|
||||||
|
|
||||||
#### Development
|
#### Development
|
||||||
|
|
||||||
Specify `VITE_AUTH_REDIRECT_PROXY_URL` only if necessary, particularly when setting up a reverse proxy to test authentication with callbacks to your development box. [Auth.js Docs Reference](https://authjs.dev/reference/nextjs/#redirectproxyurl)
|
|
||||||
|
|
||||||
The duplicate `DATABASE_URL` is only needed for Drizzle Studio.
|
The duplicate `DATABASE_URL` is only needed for Drizzle Studio.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
5
app.config.ts
Normal file
5
app.config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
middleware: "./src/middleware.ts",
|
||||||
|
});
|
|
@ -1,25 +0,0 @@
|
||||||
GET https://discord.com/api/users/@me
|
|
||||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
GET https://discord.com/api/users/@me/guilds
|
|
||||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
GET https://discord.com/api/users/@me/guilds/{{$dotenv DISCORD_GUILD_ID}}/member
|
|
||||||
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
GET https://discord.com/api/guilds/{{$dotenv DISCORD_GUILD_ID}}
|
|
||||||
Authorization: Bot {{$dotenv DISCORD_BOT_TOKEN}}
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
POST https://discord.com/api/oauth2/token/revoke
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Basic {{$dotenv DISCORD_CLIENT_ID}}:{{$dotenv DISCORD_CLIENT_SECRET}}
|
|
||||||
|
|
||||||
token={{$dotenv DISCORD_ACCESS_TOKEN}}&token_type_hint=access_token
|
|
|
@ -6,6 +6,6 @@ export default {
|
||||||
out: "./src/drizzle/migrations",
|
out: "./src/drizzle/migrations",
|
||||||
driver: "pg",
|
driver: "pg",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
connectionString: process.env.DATABASE_URL ?? "",
|
connectionString: process.env.DATABASE_URL!,
|
||||||
},
|
},
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|
270
e2e/auth.spec.ts
Normal file
270
e2e/auth.spec.ts
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { expect, test, type BrowserContext, type Page } from "@playwright/test";
|
||||||
|
import "dotenv/config";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import { Lucia, type Cookie } from "lucia";
|
||||||
|
import createClient from "openapi-fetch";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import * as schema from "~/drizzle/schema";
|
||||||
|
import type * as discord from "~/types/discord";
|
||||||
|
import type * as liljudd from "~/types/liljudd";
|
||||||
|
|
||||||
|
const unencoded = `${process.env.DISCORD_CLIENT_ID}:${process.env.DISCORD_CLIENT_SECRET}`;
|
||||||
|
const encoded = btoa(unencoded);
|
||||||
|
|
||||||
|
const queryClient = postgres(process.env.DATABASE_URL!);
|
||||||
|
const db = drizzle(queryClient, {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const adapter = new DrizzlePostgreSQLAdapter(db, schema.sessions, schema.users);
|
||||||
|
export const lucia = new Lucia(adapter, {
|
||||||
|
getUserAttributes: (attributes) => attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
let context: BrowserContext;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
let sessionCookie: Cookie | undefined;
|
||||||
|
|
||||||
|
let userId = createId();
|
||||||
|
let guildId: bigint;
|
||||||
|
|
||||||
|
test.describe.serial("User auth process", () => {
|
||||||
|
test.beforeAll(() => {
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
"DISCORD_CLIENT_ID",
|
||||||
|
"DISCORD_CLIENT_SECRET",
|
||||||
|
"DATABASE_URL",
|
||||||
|
"DISCORD_BOT_TOKEN",
|
||||||
|
].filter((e) => typeof process.env[e] === "undefined").length,
|
||||||
|
{ message: "Please specify all env vars." },
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
if (!sessionCookie) return;
|
||||||
|
|
||||||
|
const sameSiteProps = {
|
||||||
|
lax: "Lax",
|
||||||
|
strict: "Strict",
|
||||||
|
none: "None",
|
||||||
|
} as const;
|
||||||
|
const expires = sessionCookie.attributes.expires
|
||||||
|
? sessionCookie.attributes.expires.getTime() / 1000
|
||||||
|
: undefined;
|
||||||
|
const sameSite = sessionCookie.attributes.sameSite
|
||||||
|
? sameSiteProps[sessionCookie.attributes.sameSite]
|
||||||
|
: undefined;
|
||||||
|
await context.addCookies([
|
||||||
|
{
|
||||||
|
name: sessionCookie.name,
|
||||||
|
value: sessionCookie.value,
|
||||||
|
...sessionCookie.attributes,
|
||||||
|
sameSite,
|
||||||
|
expires,
|
||||||
|
secure: false,
|
||||||
|
domain: "localhost",
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll("Delete DB entries", async () => {
|
||||||
|
await db.delete(schema.users).where(eq(schema.users.id, userId)).execute();
|
||||||
|
await db
|
||||||
|
.delete(schema.guilds)
|
||||||
|
.where(eq(schema.guilds.id, guildId))
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Landing page", async () => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("load");
|
||||||
|
|
||||||
|
expect(await page.screenshot()).toMatchSnapshot("landing_page.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Unauthorized Access Redirect Test", async () => {
|
||||||
|
await page.goto("/config");
|
||||||
|
await page.waitForURL("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Generate auth session for further tests", async ({ browser }) => {
|
||||||
|
const { GET } = createClient<discord.paths>({
|
||||||
|
baseUrl: "https://discord.com/api/v10",
|
||||||
|
});
|
||||||
|
const discordUserResponse = await GET("/users/@me", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (discordUserResponse.error) throw discordUserResponse.error;
|
||||||
|
const discordUser = discordUserResponse.data;
|
||||||
|
|
||||||
|
const browserName = browser.browserType().name() as
|
||||||
|
| "chromium"
|
||||||
|
| "webkit"
|
||||||
|
| "firefox";
|
||||||
|
|
||||||
|
userId = discordUser.id + userId.slice(discordUser.id.length);
|
||||||
|
userId = userId.slice(0, -browserName.length) + browserName;
|
||||||
|
|
||||||
|
enum BrowserIds {
|
||||||
|
chromium,
|
||||||
|
webkit,
|
||||||
|
firefox,
|
||||||
|
}
|
||||||
|
guildId = BigInt(discordUser.id) ^ BigInt(BrowserIds[browserName]);
|
||||||
|
|
||||||
|
await db.insert(schema.users).values({
|
||||||
|
id: userId,
|
||||||
|
discord_id: discordUser.id,
|
||||||
|
name: discordUser.global_name,
|
||||||
|
image: discordUser.avatar,
|
||||||
|
});
|
||||||
|
const session = await lucia.createSession(
|
||||||
|
userId,
|
||||||
|
{},
|
||||||
|
{ sessionId: createId() },
|
||||||
|
);
|
||||||
|
sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
await db
|
||||||
|
.insert(schema.discordTokens)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
accessToken: "tokens.accessToken",
|
||||||
|
expiresAt: sessionCookie.attributes.expires ?? new Date(),
|
||||||
|
refreshToken: "tokens.refreshToken",
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Landing page when logged in", async () => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForLoadState("load");
|
||||||
|
|
||||||
|
expect(await page.screenshot()).toMatchSnapshot(
|
||||||
|
"landing_page_logged_in.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test Api", async () => {
|
||||||
|
const { GET, POST, PUT } = createClient<liljudd.paths>({
|
||||||
|
baseUrl: "http://localhost:3000/",
|
||||||
|
});
|
||||||
|
|
||||||
|
const createConfigResponse = await POST("/api/{guildId}/config", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
guildId: guildId.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
Origin: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createConfigResponse.error)
|
||||||
|
throw new Error(createConfigResponse.error.error);
|
||||||
|
|
||||||
|
let getConfigResponse = await GET("/api/{guildId}/config", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
guildId: guildId.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
Origin: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);
|
||||||
|
|
||||||
|
switch (getConfigResponse.data?.checksum) {
|
||||||
|
case "209a644c31a5ef123c432c2885d231a2e0efc4de": // chromium
|
||||||
|
case "aead21e132a94ab897ec28e0f0c337a66207bad3": // webkit
|
||||||
|
case "c3e2ff2ce5a8936234552125a54c2fe1ce1a35da": // firefox
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
"Before guild GET checksum didn't matched known ones: " +
|
||||||
|
getConfigResponse.data?.checksum,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const putTimePlanningResponse = await PUT("/api/{guildId}/timePlanning", {
|
||||||
|
body: {
|
||||||
|
enabled: true,
|
||||||
|
channelId: "1234567890123456789",
|
||||||
|
rolesEnabled: true,
|
||||||
|
isAvailableRoleId: "1234567890123456789",
|
||||||
|
wantsToBeNotifieRoledId: "1234567890123456789",
|
||||||
|
messageIds: {
|
||||||
|
"0": "1234567890123456789",
|
||||||
|
"1": "1234567890123456789",
|
||||||
|
"2": "1234567890123456789",
|
||||||
|
"3": "1234567890123456789",
|
||||||
|
"4": "1234567890123456789",
|
||||||
|
"5": "1234567890123456789",
|
||||||
|
"6": "1234567890123456789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
guildId: guildId.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
Origin: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (putTimePlanningResponse.error)
|
||||||
|
throw new Error(putTimePlanningResponse.error.error);
|
||||||
|
|
||||||
|
getConfigResponse = await GET("/api/{guildId}/config", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
guildId: guildId.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${encoded}`,
|
||||||
|
Origin: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);
|
||||||
|
|
||||||
|
switch (getConfigResponse.data?.checksum) {
|
||||||
|
case "681c8324b21096255d942bb78bd6655da90d352e": // chromium
|
||||||
|
case "a2fb3601b7d0949b1ceada3b3ac0ba408c6159bb": // webkit
|
||||||
|
case "bf20daba95e8f3ddd17cc64e8a7ba184b68ad37b": // firefox
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
"After guild GET checksum didn't matched known ones: " +
|
||||||
|
getConfigResponse.data?.checksum,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
BIN
e2e/auth.spec.ts-snapshots/landing-page-chromium-linux.png
Normal file
BIN
e2e/auth.spec.ts-snapshots/landing-page-chromium-linux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 KiB |
BIN
e2e/auth.spec.ts-snapshots/landing-page-firefox-linux.png
Normal file
BIN
e2e/auth.spec.ts-snapshots/landing-page-firefox-linux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 328 KiB |
Binary file not shown.
After Width: | Height: | Size: 227 KiB |
Binary file not shown.
After Width: | Height: | Size: 331 KiB |
Binary file not shown.
After Width: | Height: | Size: 716 KiB |
BIN
e2e/auth.spec.ts-snapshots/landing-page-webkit-linux.png
Normal file
BIN
e2e/auth.spec.ts-snapshots/landing-page-webkit-linux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 710 KiB |
40
package.json
40
package.json
|
@ -10,12 +10,11 @@
|
||||||
"discord-openapi-gen": "openapi-typescript https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json -o ./src/types/discord.d.ts",
|
"discord-openapi-gen": "openapi-typescript https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json -o ./src/types/discord.d.ts",
|
||||||
"liljudd-openapi-gen": "openapi-typescript ./public/api/specs/liljudd.json -o ./src/types/liljudd.d.ts",
|
"liljudd-openapi-gen": "openapi-typescript ./public/api/specs/liljudd.json -o ./src/types/liljudd.d.ts",
|
||||||
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck --preserveSymLinks",
|
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck --preserveSymLinks",
|
||||||
"drizzle-studio": "drizzle-kit studio"
|
"drizzle-studio": "drizzle-kit studio",
|
||||||
|
"test": "pnpm exec playwright test",
|
||||||
|
"test-ui": "pnpm exec playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.19.0",
|
|
||||||
"@auth/drizzle-adapter": "^0.6.3",
|
|
||||||
"@auth/solid-start": "0.1.2",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
||||||
|
@ -23,29 +22,42 @@
|
||||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
||||||
|
"@lucia-auth/adapter-drizzle": "^1.0.2",
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@solidjs/meta": "^0.29.3",
|
"@solidjs/meta": "^0.29.3",
|
||||||
"@solidjs/router": "^0.12.0",
|
"@solidjs/router": "^0.12.4",
|
||||||
"@solidjs/start": "^0.5.4",
|
"@solidjs/start": "^0.6.0",
|
||||||
"drizzle-orm": "^0.29.3",
|
"arctic": "^1.2.1",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"drizzle-orm": "^0.29.4",
|
||||||
|
"http-status": "^1.7.4",
|
||||||
|
"json-stable-stringify": "^1.1.1",
|
||||||
|
"lucia": "^3.0.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"openapi-fetch": "^0.8.2",
|
"object-hash": "^3.0.0",
|
||||||
|
"openapi-fetch": "^0.9.2",
|
||||||
"postgres": "^3.4.3",
|
"postgres": "^3.4.3",
|
||||||
"solid-js": "^1.8.14",
|
"solid-js": "^1.8.15",
|
||||||
"vinxi": "^0.2.1"
|
"vinxi": "^0.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@playwright/test": "^1.42.0",
|
||||||
"dotenv": "^16.4.2",
|
"@types/json-stable-stringify": "^1.0.36",
|
||||||
|
"@types/node": "^20.11.22",
|
||||||
|
"@types/object-hash": "^3.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-kit": "^0.20.14",
|
"drizzle-kit": "^0.20.14",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-solid": "^0.13.1",
|
"eslint-plugin-solid": "^0.13.1",
|
||||||
|
"h3": "^1.11.1",
|
||||||
"openapi-typescript": "^6.7.4",
|
"openapi-typescript": "^6.7.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.71.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
|
77
playwright.config.ts
Normal file
77
playwright.config.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm start",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
3639
pnpm-lock.yaml
3639
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -10,40 +10,67 @@
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/boot/config": {
|
"/api/boot": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Guild configs"],
|
"tags": ["Bot bootup"],
|
||||||
"summary": "Find a guild's config by ID",
|
"summary": "Retrieve all guild's configs",
|
||||||
"description": "Returns a single guild's config.",
|
"description": "Returns all guild's configs.",
|
||||||
"operationId": "getGuildsFromBoot",
|
"operationId": "getGuildsForBoot",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "successful operation",
|
"description": "successful operation",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/bootConfig"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/guildConfig"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/{guildId}/config": {
|
"/api/{guildId}/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Guild configs"],
|
"tags": ["Guild config"],
|
||||||
"summary": "Find a guild's config by ID",
|
"summary": "Find a guild's config by ID",
|
||||||
"description": "Returns a single guild's config.",
|
"description": "Returns a single guild's config.",
|
||||||
"operationId": "getGuildById",
|
"operationId": "getGuildById",
|
||||||
|
@ -71,28 +98,52 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"delete": {
|
"post": {
|
||||||
"tags": ["Guild configs"],
|
"tags": ["Guild config"],
|
||||||
"summary": "Deletes a guild's config by ID",
|
"summary": "Creates a guild's config by ID",
|
||||||
"description": "Delete a guild's config when the bot is removed from the guild.",
|
"description": "Create a guild's config when the bot is has joined a new guild.",
|
||||||
"operationId": "deleteGuildById",
|
"operationId": "postGuildById",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "guildId",
|
"name": "guildId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "ID of guild config to delete",
|
"description": "ID of guild's config to create",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -105,30 +156,112 @@
|
||||||
"description": "successful operation"
|
"description": "successful operation"
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
},
|
"delete": {
|
||||||
"/api/{guildId}/tp_messages": {
|
"tags": ["Guild config"],
|
||||||
"get": {
|
"summary": "Deletes a guild's config by ID",
|
||||||
"tags": ["Time planning messages"],
|
"description": "Delete a guild's config when the bot is removed from the guild.",
|
||||||
"summary": "Find the tp_messages of guild by ID",
|
"operationId": "deleteGuildById",
|
||||||
"description": "Returns tp_messages for a guild",
|
|
||||||
"operationId": "getTp_messagesOfGuildById",
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "guildId",
|
"name": "guildId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "ID of guild's tp_messages to return",
|
"description": "ID of guild's config to delete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "varchar(20)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "successful operation"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/{guildId}/timePlanning": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["Time planning messages"],
|
||||||
|
"summary": "Find the timePlanning of guild by ID",
|
||||||
|
"description": "Returns timePlanning for a guild",
|
||||||
|
"operationId": "gettimePlanningOfGuildById",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "guildId",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of guild's timePlanning to return",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -142,37 +275,58 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/tp_messages"
|
"$ref": "#/components/schemas/timePlanning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"204": {
|
|
||||||
"description": "Time planning not enabled for this guild"
|
|
||||||
},
|
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"tags": ["Time planning messages"],
|
"tags": ["Time planning messages"],
|
||||||
"summary": "Put message IDs for tp_messages of guild by ID",
|
"summary": "Put new message IDs for timePlanning of guild by ID",
|
||||||
"description": "Returns tp_messages for a guild",
|
"description": "Returns timePlanning for a guild",
|
||||||
"operationId": "putTp_messagesOfGuildById",
|
"operationId": "puttimePlanningOfGuildById",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "guildId",
|
"name": "guildId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "ID of guild's tp_messages to return",
|
"description": "ID of guild's timePlanning to return",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -180,30 +334,55 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Put new message IDs for timePlanning in channel",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/timePlanning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"204": {
|
||||||
"description": "successful operation",
|
"description": "successful operation"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID supplied",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/tp_messages"
|
"$ref": "#/components/schemas/error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"204": {
|
"401": {
|
||||||
"description": "Time planning not enabled for this guild"
|
"description": "Unauthorized",
|
||||||
},
|
"content": {
|
||||||
"400": {
|
"application/json": {
|
||||||
"description": "Invalid ID supplied"
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -212,13 +391,13 @@
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Matches"],
|
"tags": ["Matches"],
|
||||||
"summary": "Find all matches of guild by ID",
|
"summary": "Find all matches of guild by ID",
|
||||||
"description": "Returns tp_messages for a guild",
|
"description": "Returns timePlanning for a guild",
|
||||||
"operationId": "getMatchesOfGuildById",
|
"operationId": "getMatchesOfGuildById",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "guildId",
|
"name": "guildId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "ID of guild's tp_messages to return",
|
"description": "ID of guild's timePlanning to return",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -232,36 +411,66 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"required": ["matches", "timezone"],
|
||||||
"$ref": "#/components/schemas/tp_messages"
|
"properties": {
|
||||||
|
"matches": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/match"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "text",
|
||||||
|
"example": "Europe/Berlin"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"204": {
|
|
||||||
"description": "Time planning not enabled for this guild"
|
|
||||||
},
|
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"/api/{guildId}/matches/{channelId}": {
|
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["Matches"],
|
"tags": ["Matches"],
|
||||||
"summary": "Save a new created match in channel of guild by IDs",
|
"summary": "Save a new created match of guild by ID",
|
||||||
"description": "Returns tp_messages for a guild",
|
"description": "Returns timePlanning for a guild",
|
||||||
"operationId": "postMatchOfGuildById",
|
"operationId": "postMatchOfGuildById",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
@ -273,108 +482,69 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "varchar(20)"
|
"format": "varchar(20)"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "channelId",
|
|
||||||
"in": "path",
|
|
||||||
"description": "ID of match's channel to set",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"requestBody": {
|
||||||
"200": {
|
"description": "Save a new created match in channel",
|
||||||
"description": "successful operation",
|
"content": {
|
||||||
"content": {
|
"application/json": {
|
||||||
"application/json": {
|
"schema": {
|
||||||
"schema": {
|
"type": "object",
|
||||||
"$ref": "#/components/schemas/tp_messages"
|
"required": ["match", "timezone"],
|
||||||
|
"properties": {
|
||||||
|
"match": {
|
||||||
|
"$ref": "#/components/schemas/match"
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "text",
|
||||||
|
"example": "Europe/Berlin",
|
||||||
|
"description": "Has to match guild tz"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
"204": {
|
"204": {
|
||||||
"description": "Time planning not enabled for this guild"
|
"description": "successful operation"
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid ID supplied"
|
"description": "Invalid ID supplied",
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Guild not found"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/{guildId}/matches/{channelId}/{matchMessageId}": {
|
|
||||||
"put": {
|
|
||||||
"tags": ["Matches"],
|
|
||||||
"summary": "Set state for match of guild by IDs",
|
|
||||||
"description": "Returns tp_messages for a guild",
|
|
||||||
"operationId": "putMatchOfGuildById",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "guildId",
|
|
||||||
"in": "path",
|
|
||||||
"description": "ID of guild's tp_messages to return",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "channelId",
|
|
||||||
"in": "path",
|
|
||||||
"description": "ID of match's channel to set",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "matchMessageId",
|
|
||||||
"in": "path",
|
|
||||||
"description": "ID of match's message Id to set",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "successful operation",
|
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/tp_messages"
|
"$ref": "#/components/schemas/error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"204": {
|
"401": {
|
||||||
"description": "Time planning not enabled for this guild"
|
"description": "Unauthorized",
|
||||||
},
|
"content": {
|
||||||
"400": {
|
"application/json": {
|
||||||
"description": "Invalid ID supplied"
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Guild not found"
|
"description": "Guild not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -382,27 +552,12 @@
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"bootConfig": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"guilds": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/guildConfig"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"accessToken": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"guildConfig": {
|
"guildConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": ["guildId", "timezone", "features", "matches", "checksum"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"guildID": {
|
"guildId": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/id"
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789"
|
|
||||||
},
|
},
|
||||||
"timezone": {
|
"timezone": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -411,14 +566,24 @@
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": ["timePlanning"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"time_planning": {
|
"timePlanning": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"channelId",
|
||||||
|
"targetMinute",
|
||||||
|
"targetHour",
|
||||||
|
"targetDay",
|
||||||
|
"roles"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"channelID": {
|
"enabled": {
|
||||||
"type": "string",
|
"type": "boolean"
|
||||||
"format": "varchar(20)",
|
},
|
||||||
"example": "1234567890123456789"
|
"channelId": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
},
|
},
|
||||||
"targetMinute": {
|
"targetMinute": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
@ -434,21 +599,20 @@
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"isAvailableRoleId",
|
||||||
|
"wantsToBeNotifieRoledId"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"isAvailableRoleId": {
|
"isAvailableRoleId": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789",
|
|
||||||
"nullable": true
|
|
||||||
},
|
},
|
||||||
"wantsToBeNotifieRoledId": {
|
"wantsToBeNotifieRoledId": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789",
|
|
||||||
"nullable": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -461,85 +625,132 @@
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/match"
|
"$ref": "#/components/schemas/match"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"match": {
|
"match": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"channelId",
|
||||||
|
"matchType",
|
||||||
|
"createrId",
|
||||||
|
"roleId",
|
||||||
|
"opponentName",
|
||||||
|
"messageId",
|
||||||
|
"utc_ts"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"channelID": {
|
"channelId": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/id"
|
||||||
"format": "varcharq(20)",
|
},
|
||||||
"example": "1234567890123456789"
|
"createrId": {
|
||||||
|
"$ref": "#/components/schemas/id"
|
||||||
|
},
|
||||||
|
"roleId": {
|
||||||
|
"$ref": "#/components/schemas/id"
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"$ref": "#/components/schemas/id"
|
||||||
},
|
},
|
||||||
"matchType": {
|
"matchType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "varchar(50)",
|
"format": "varchar(50)",
|
||||||
"example": "Scrim"
|
"example": "Scrim"
|
||||||
},
|
},
|
||||||
"createrId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789"
|
|
||||||
},
|
|
||||||
"roleId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789"
|
|
||||||
},
|
|
||||||
"opponentName": {
|
"opponentName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "varchar(100)",
|
"format": "varchar(100)",
|
||||||
"example": "?"
|
"example": "?"
|
||||||
},
|
},
|
||||||
"messsageId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789"
|
|
||||||
},
|
|
||||||
"utc_ts": {
|
"utc_ts": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "1706180188"
|
"example": "2020-01-01T00:00:00Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tp_messages": {
|
"timePlanning": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"channelId",
|
||||||
|
"rolesEnabled",
|
||||||
|
"isAvailableRoleId",
|
||||||
|
"wantsToBeNotifieRoledId",
|
||||||
|
"messageIds"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"guildId": {
|
"enabled": {
|
||||||
"type": "string",
|
"type": "boolean"
|
||||||
"format": "varchar(20)",
|
|
||||||
"example": "1234567890123456789"
|
|
||||||
},
|
},
|
||||||
"channelId": {
|
"channelId": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"format": "varchar(20)",
|
},
|
||||||
"example": "1234567890123456789"
|
"rolesEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isAvailableRoleId": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
|
},
|
||||||
|
"wantsToBeNotifieRoledId": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
},
|
},
|
||||||
"messageIds": {
|
"messageIds": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"required": ["0", "1", "2", "3", "4", "5", "6"],
|
||||||
"type": "string",
|
"properties": {
|
||||||
"format": "varchar(20)"
|
"0": {
|
||||||
},
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"example": [
|
},
|
||||||
"1234567890123456789",
|
"1": {
|
||||||
"1234567890123456789",
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"1234567890123456789",
|
},
|
||||||
"1234567890123456789",
|
"2": {
|
||||||
"1234567890123456789",
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
"1234567890123456789",
|
},
|
||||||
"1234567890123456789"
|
"3": {
|
||||||
]
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"$ref": "#/components/schemas/idOrNull"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{7,20}$",
|
||||||
|
"example": "1234567890123456789"
|
||||||
|
},
|
||||||
|
"idOrNull": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{7,20}$",
|
||||||
|
"example": "1234567890123456789",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "object",
|
||||||
|
"required": "error",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
"api_key": {
|
"basicAuth": {
|
||||||
"type": "apiKey",
|
"type": "http",
|
||||||
"name": "api_key",
|
"scheme": "basic"
|
||||||
"in": "header"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"guilds": [
|
|
||||||
{
|
|
||||||
"guildID": "some ID",
|
|
||||||
"UTCOffset": 0,
|
|
||||||
"features": {
|
|
||||||
"time_planning": {
|
|
||||||
"channelID": "some ID",
|
|
||||||
"targetWeekday": 0,
|
|
||||||
"targetHour": 0,
|
|
||||||
"targetMinute": 0,
|
|
||||||
"isAvailableRoleId": "some ID",
|
|
||||||
"wantsToBeNotifieRoledId": "some ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"matches": [
|
|
||||||
{
|
|
||||||
"channelID": "some ID",
|
|
||||||
"matchType": "",
|
|
||||||
"createrId": "some ID",
|
|
||||||
"roleId": "some ID",
|
|
||||||
"opponentName": "",
|
|
||||||
"messsageId": "",
|
|
||||||
"plannedFor": 1704314625000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"accessToken": "some Token"
|
|
||||||
}
|
|
|
@ -1,22 +1,24 @@
|
||||||
import { faCirclePlus } from "@fortawesome/pro-regular-svg-icons";
|
import { faCirclePlus } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { JSX, Show, Suspense } from "solid-js";
|
import { JSX, Show } from "solid-js";
|
||||||
import "../styles/components/NavBar.scss";
|
import "../styles/components/NavBar.scss";
|
||||||
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||||
import NavUser from "./NavUser";
|
import NavUser from "./NavUser";
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_CLIENT_ID found!");
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_OAUTH2_PERMISSIONS found!");
|
||||||
|
|
||||||
export function Li(props: {
|
export function Li(props: {
|
||||||
href: string;
|
href: string;
|
||||||
action?: () => void;
|
rel?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li class="navElem flex-row thick">
|
<li class="navElem flex-row thick">
|
||||||
<a
|
<a class="flex-row" href={props.href} rel={props.rel}>
|
||||||
class="flex-row"
|
|
||||||
href={props.href}
|
|
||||||
onClick={() => props.action && props.action()}
|
|
||||||
>
|
|
||||||
{props.children ?? <></>}
|
{props.children ?? <></>}
|
||||||
<Show when={props.name}>
|
<Show when={props.name}>
|
||||||
<span>{props.name}</span>
|
<span>{props.name}</span>
|
||||||
|
@ -40,14 +42,12 @@ function NavBar() {
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="flex-row responsive thick">
|
<ul class="flex-row responsive thick">
|
||||||
<Li
|
<Li
|
||||||
href={`https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_BOT_PERMISSIONS}&scope=bot`}
|
href={`https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS}&scope=bot`}
|
||||||
name="Invite to your server"
|
name="Invite to your server"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
|
<FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
|
||||||
</Li>
|
</Li>
|
||||||
<Suspense>
|
<NavUser />
|
||||||
<NavUser />
|
|
||||||
</Suspense>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,70 +1,40 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import { signIn, signOut } from "@auth/solid-start/client";
|
|
||||||
import {
|
import {
|
||||||
faArrowRightFromBracket,
|
faArrowRightFromBracket,
|
||||||
faArrowRightToBracket,
|
faArrowRightToBracket,
|
||||||
faGear,
|
faGear,
|
||||||
} from "@fortawesome/pro-regular-svg-icons";
|
} from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { eq } from "drizzle-orm";
|
import { cache, createAsync } from "@solidjs/router";
|
||||||
import { Show, createResource } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import db from "~/drizzle";
|
|
||||||
import { users } from "~/drizzle/schema";
|
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||||
import { Li } from "./NavBar";
|
import { Li } from "./NavBar";
|
||||||
|
|
||||||
const initialUser = {
|
|
||||||
id: "",
|
|
||||||
name: null as string | null,
|
|
||||||
email: "",
|
|
||||||
emailVerified: null as Date | null,
|
|
||||||
image: null as string | null,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent();
|
const event = getRequestEvent();
|
||||||
if (!event)
|
|
||||||
return { success: false, message: "No request event!", ...initialUser };
|
|
||||||
|
|
||||||
const session = await getSession(event.request, authOptions);
|
return event?.nativeEvent.context.user;
|
||||||
if (!session?.user?.id)
|
|
||||||
return { success: false, message: "No user with id!", ...initialUser };
|
|
||||||
|
|
||||||
const user = (
|
|
||||||
await db
|
|
||||||
.selectDistinct()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, session.user?.id))
|
|
||||||
.limit(1)
|
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
console.log("userInfo", "success");
|
|
||||||
|
|
||||||
return { success: true, message: "", ...user };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedUser = cache(() => getUser(), "userInfo");
|
||||||
|
|
||||||
function NavUser() {
|
function NavUser() {
|
||||||
const [user] = createResource(async () => {
|
const user = createAsync(() => cachedUser());
|
||||||
const user = await getUser();
|
const pfp = () => {
|
||||||
|
const thisUser = user();
|
||||||
|
if (!thisUser?.id) return "";
|
||||||
|
|
||||||
if (!user.success) console.error("userInfo", user.message);
|
return thisUser.image
|
||||||
|
? `https://cdn.discordapp.com/avatars/${thisUser.discord_id}/${thisUser.image}.png`
|
||||||
return user;
|
: `https://cdn.discordapp.com/embed/avatars/${(parseInt(thisUser.discord_id) >> 22) % 6}.png`;
|
||||||
});
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={user()?.id}
|
when={user()?.id}
|
||||||
fallback={
|
fallback={
|
||||||
<Li
|
<Li href="/api/auth/login" name="Login" rel="external">
|
||||||
href="#"
|
|
||||||
name="Login"
|
|
||||||
action={() => signIn("discord", { callbackUrl: "/config" })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
class="secondary"
|
class="secondary"
|
||||||
icon={faArrowRightToBracket}
|
icon={faArrowRightToBracket}
|
||||||
|
@ -75,11 +45,11 @@ function NavUser() {
|
||||||
>
|
>
|
||||||
<Li href="/config">
|
<Li href="/config">
|
||||||
<div class="swap lower">
|
<div class="swap lower">
|
||||||
<img class="primary" src={user()?.image ?? ""} alt="User pfp" />
|
<img class="primary" src={pfp()} alt="User pfp" />
|
||||||
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
|
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
|
||||||
</div>
|
</div>
|
||||||
</Li>
|
</Li>
|
||||||
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
|
<Li href="/api/auth/logout" rel="external">
|
||||||
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
||||||
</Li>
|
</Li>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -2,7 +2,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
|
|
||||||
const queryClient = postgres(import.meta.env.VITE_DATABASE_URL ?? "");
|
if (typeof import.meta.env.VITE_DATABASE_URL === "undefined")
|
||||||
|
throw new Error("No env VITE_DATABASE_URL found!");
|
||||||
|
|
||||||
|
const queryClient = postgres(import.meta.env.VITE_DATABASE_URL);
|
||||||
const db = drizzle(queryClient, {
|
const db = drizzle(queryClient, {
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import type { AdapterAccount } from "@auth/core/adapters";
|
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
bigint,
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
|
||||||
pgTable,
|
pgTable,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
serial,
|
serial,
|
||||||
|
@ -13,130 +12,90 @@ import {
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
id: text("id").notNull().primaryKey(),
|
id: varchar("id", { length: 24 }).primaryKey(),
|
||||||
|
discord_id: text("discord_id").notNull(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
email: text("email").notNull(),
|
|
||||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accounts = pgTable(
|
|
||||||
"account",
|
|
||||||
{
|
|
||||||
userId: text("userId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
type: text("type").$type<AdapterAccount["type"]>().notNull(),
|
|
||||||
provider: text("provider").notNull(),
|
|
||||||
providerAccountId: text("providerAccountId").notNull(),
|
|
||||||
refresh_token: text("refresh_token"),
|
|
||||||
access_token: text("access_token"),
|
|
||||||
expires_at: integer("expires_at"),
|
|
||||||
token_type: text("token_type"),
|
|
||||||
scope: text("scope"),
|
|
||||||
id_token: text("id_token"),
|
|
||||||
session_state: text("session_state"),
|
|
||||||
},
|
|
||||||
(account) => ({
|
|
||||||
compoundKey: primaryKey({
|
|
||||||
columns: [account.provider, account.providerAccountId],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sessions = pgTable("session", {
|
export const sessions = pgTable("session", {
|
||||||
sessionToken: text("sessionToken").notNull().primaryKey(),
|
id: varchar("id", { length: 24 }).primaryKey(),
|
||||||
userId: text("userId")
|
userId: varchar("user_id", { length: 24 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
expiresAt: timestamp("expires_at", {
|
||||||
|
withTimezone: true,
|
||||||
|
mode: "date",
|
||||||
|
}).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const verificationTokens = pgTable(
|
export const discordTokens = pgTable("tokens", {
|
||||||
"verificationToken",
|
userId: varchar("user_id", { length: 24 })
|
||||||
{
|
.primaryKey()
|
||||||
identifier: text("identifier").notNull(),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
token: text("token").notNull(),
|
refreshToken: text("refresh_token").notNull(),
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
accessToken: text("access_token").notNull(),
|
||||||
},
|
expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
|
||||||
(vt) => ({
|
});
|
||||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
|
||||||
|
export const guilds = pgTable("guilds", {
|
||||||
|
id: bigint("id", { mode: "bigint" }).primaryKey(),
|
||||||
|
timezone: text("timezone").notNull().default("Etc/UTC"),
|
||||||
|
tpEnabled: boolean("tp_enabled").notNull().default(false),
|
||||||
|
tpChannelId: bigint("tp_channel_id", { mode: "bigint" }),
|
||||||
|
tpInterval: smallint("target_interval").notNull().default(64),
|
||||||
|
tpRolesEnabled: boolean("tp_roles_enabled").notNull().default(false),
|
||||||
|
isAvailableRoleId: bigint("is_available_role_id", { mode: "bigint" }),
|
||||||
|
wantsToBeNotifieRoledId: bigint("wants_to_be_notified_role_id", {
|
||||||
|
mode: "bigint",
|
||||||
}),
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const guildsRelations = relations(guilds, ({ many }) => ({
|
||||||
|
tpMessages: many(tpMessages),
|
||||||
|
matches: many(matches),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const tpMessages = pgTable(
|
||||||
|
"tp_messages",
|
||||||
|
{
|
||||||
|
messageId: bigint("message_id", { mode: "bigint" }),
|
||||||
|
day: smallint("day").notNull(),
|
||||||
|
guildId: bigint("guild_id", { mode: "bigint" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => guilds.id, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(table) => {
|
||||||
|
return {
|
||||||
|
pk: primaryKey({ columns: [table.guildId, table.day] }),
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const matchPlannings = pgTable("match_planning", {
|
export const tpMessagesRelations = relations(tpMessages, ({ one }) => ({
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
channelId: varchar("channel_id", { length: 20 }).notNull(),
|
|
||||||
matchtype: varchar("match_type", { length: 50 }).notNull(),
|
|
||||||
createrId: varchar("creater_id", { length: 20 }).notNull(),
|
|
||||||
roleId: varchar("role_id", { length: 20 }).notNull(),
|
|
||||||
opponentName: varchar("opponent_name", { length: 100 }).notNull(),
|
|
||||||
messageId: varchar("message_id", { length: 20 }).notNull(),
|
|
||||||
utc_ts: timestamp("utc_ts").notNull(),
|
|
||||||
guildId: varchar("guild_id", { length: 20 })
|
|
||||||
.notNull()
|
|
||||||
.references(() => guilds.id, { onDelete: "cascade" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const matchPlanningsRelations = relations(matchPlannings, ({ one }) => ({
|
|
||||||
guild: one(guilds, {
|
guild: one(guilds, {
|
||||||
fields: [matchPlannings.guildId],
|
fields: [tpMessages.guildId],
|
||||||
references: [guilds.id],
|
references: [guilds.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const guilds = pgTable("guild", {
|
export const matches = pgTable("matches", {
|
||||||
id: varchar("id", { length: 20 }).primaryKey(),
|
|
||||||
timezone: text("timezone").notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const guildsRelations = relations(guilds, ({ one, many }) => ({
|
|
||||||
matches: many(matchPlannings),
|
|
||||||
timePlanning: one(timePlannings, {
|
|
||||||
fields: [guilds.id],
|
|
||||||
references: [timePlannings.guildId],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const timePlannings = pgTable("time_planning", {
|
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
guildId: varchar("guild_id", { length: 20 })
|
channelId: bigint("channel_id", { mode: "bigint" }).notNull(),
|
||||||
|
matchType: varchar("match_type", { length: 50 }).notNull(),
|
||||||
|
createrId: bigint("creater_id", { mode: "bigint" }).notNull(),
|
||||||
|
roleId: bigint("role_id", { mode: "bigint" }).notNull(),
|
||||||
|
opponentName: varchar("opponent_name", { length: 100 }).notNull(),
|
||||||
|
messageId: bigint("message_id", { mode: "bigint" }).notNull(),
|
||||||
|
utc_ts: timestamp("utc_ts").notNull(),
|
||||||
|
guildId: bigint("guild_id", { mode: "bigint" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.unique()
|
.references(() => guilds.id, { onDelete: "cascade" }),
|
||||||
.references(() => guilds.id, {
|
|
||||||
onDelete: "cascade",
|
|
||||||
}),
|
|
||||||
channelId: varchar("channel_id", { length: 20 }).notNull(),
|
|
||||||
target_interval: smallint("target_interval").notNull(),
|
|
||||||
roles: boolean("roles").notNull(),
|
|
||||||
isAvailableRoleId: varchar("is_available_role_id", { length: 20 }),
|
|
||||||
wantsToBeNotifieRoledId: varchar("wants_to_be_notified_role_id", {
|
|
||||||
length: 20,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const timePlanningsRelations = relations(
|
export const matchPlanningsRelations = relations(matches, ({ one }) => ({
|
||||||
timePlannings,
|
guild: one(guilds, {
|
||||||
({ one, many }) => ({
|
fields: [matches.guildId],
|
||||||
guild: one(guilds, {
|
references: [guilds.id],
|
||||||
fields: [timePlannings.guildId],
|
|
||||||
references: [guilds.id],
|
|
||||||
}),
|
|
||||||
messages: many(tpMessages),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const tpMessages = pgTable("tp_message", {
|
|
||||||
messageId: varchar("message_id", { length: 20 }).primaryKey(),
|
|
||||||
day: smallint("day").notNull(),
|
|
||||||
planId: integer("plan_id")
|
|
||||||
.notNull()
|
|
||||||
.references(() => timePlannings.id, { onDelete: "cascade" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tpMessagesRelations = relations(tpMessages, ({ one }) => ({
|
|
||||||
plan: one(timePlannings, {
|
|
||||||
fields: [tpMessages.planId],
|
|
||||||
references: [timePlannings.id],
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { mount, StartClient } from "@solidjs/start/client";
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
mount(() => <StartClient />, document.getElementById("app"));
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
|
|
43
src/lib/auth.ts
Normal file
43
src/lib/auth.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
||||||
|
import { Discord } from "arctic";
|
||||||
|
import { Lucia } from "lucia";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { sessions, users } from "~/drizzle/schema";
|
||||||
|
|
||||||
|
if (typeof import.meta.env.PROD === "undefined")
|
||||||
|
throw new Error("No env PROD found!");
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_CLIENT_ID found!");
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_CLIENT_SECRET === "undefined")
|
||||||
|
throw new Error("No env PROD found!");
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_AUTH_REDIRECT_URL === "undefined")
|
||||||
|
throw new Error("No env VITE_AUTH_REDIRECT_URL found!");
|
||||||
|
|
||||||
|
const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);
|
||||||
|
|
||||||
|
export const lucia = new Lucia(adapter, {
|
||||||
|
sessionCookie: {
|
||||||
|
attributes: {
|
||||||
|
// set to `true` when using HTTPS
|
||||||
|
secure: import.meta.env.PROD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getUserAttributes: (attributes) => attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const discord = new Discord(
|
||||||
|
import.meta.env.VITE_DISCORD_CLIENT_ID,
|
||||||
|
import.meta.env.VITE_DISCORD_CLIENT_SECRET,
|
||||||
|
import.meta.env.VITE_AUTH_REDIRECT_URL,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unencoded = `${import.meta.env.VITE_DISCORD_CLIENT_ID}:${import.meta.env.VITE_DISCORD_CLIENT_SECRET}`;
|
||||||
|
const encoded = btoa(unencoded);
|
||||||
|
|
||||||
|
export const BasicAuth = {
|
||||||
|
unencoded: `Basic ${unencoded}`,
|
||||||
|
encoded: `Basic ${encoded}`,
|
||||||
|
};
|
74
src/lib/responseBuilders.ts
Normal file
74
src/lib/responseBuilders.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import stringify from "json-stable-stringify";
|
||||||
|
import objectHash from "object-hash";
|
||||||
|
import { guilds, matches, tpMessages } from "~/drizzle/schema";
|
||||||
|
import { ExtractDataTypes, GetColumns } from "~/types/db";
|
||||||
|
import { components } from "~/types/liljudd";
|
||||||
|
|
||||||
|
export const buildMatches = (
|
||||||
|
queryMatches: ExtractDataTypes<GetColumns<typeof matches>>[],
|
||||||
|
): components["schemas"]["match"][] =>
|
||||||
|
queryMatches.map(
|
||||||
|
({
|
||||||
|
channelId,
|
||||||
|
createrId,
|
||||||
|
roleId,
|
||||||
|
messageId,
|
||||||
|
matchType,
|
||||||
|
opponentName,
|
||||||
|
utc_ts,
|
||||||
|
}) => ({
|
||||||
|
channelId: channelId.toString(),
|
||||||
|
createrId: createrId.toString(),
|
||||||
|
roleId: roleId.toString(),
|
||||||
|
messageId: messageId.toString(),
|
||||||
|
matchType,
|
||||||
|
opponentName,
|
||||||
|
utc_ts: utc_ts.toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function buildConfig(
|
||||||
|
guildQuery: ExtractDataTypes<GetColumns<typeof guilds>> & {
|
||||||
|
tpMessages: ExtractDataTypes<GetColumns<typeof tpMessages>>[];
|
||||||
|
matches: ExtractDataTypes<GetColumns<typeof matches>>[];
|
||||||
|
},
|
||||||
|
): components["schemas"]["guildConfig"] {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
timezone,
|
||||||
|
tpEnabled,
|
||||||
|
tpChannelId,
|
||||||
|
tpInterval,
|
||||||
|
tpRolesEnabled: tpRoles,
|
||||||
|
isAvailableRoleId,
|
||||||
|
wantsToBeNotifieRoledId,
|
||||||
|
} = guildQuery;
|
||||||
|
|
||||||
|
const targetMinute = tpInterval & 63;
|
||||||
|
const targetHour = (tpInterval >> 6) & 31;
|
||||||
|
const targetDay = (tpInterval >> 11) & 7;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
guildId: id.toString(),
|
||||||
|
timezone,
|
||||||
|
features: {
|
||||||
|
timePlanning: {
|
||||||
|
enabled: tpEnabled,
|
||||||
|
channelId: tpChannelId?.toString() ?? null,
|
||||||
|
targetMinute,
|
||||||
|
targetHour,
|
||||||
|
targetDay,
|
||||||
|
roles: {
|
||||||
|
enabled: tpRoles,
|
||||||
|
isAvailableRoleId: isAvailableRoleId?.toString() ?? null,
|
||||||
|
wantsToBeNotifieRoledId: wantsToBeNotifieRoledId?.toString() ?? null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
matches: buildMatches(guildQuery.matches),
|
||||||
|
};
|
||||||
|
|
||||||
|
// generate checksum from payload because
|
||||||
|
// from guildQuery results in bigint serialization error
|
||||||
|
return { ...payload, checksum: objectHash(stringify(payload)) };
|
||||||
|
}
|
39
src/lib/responses.ts
Normal file
39
src/lib/responses.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import httpStatus from "http-status";
|
||||||
|
import {
|
||||||
|
APIResponse,
|
||||||
|
Methods,
|
||||||
|
MyPaths,
|
||||||
|
ResponseSchemas,
|
||||||
|
StatusCodes,
|
||||||
|
} from "~/types/backend";
|
||||||
|
|
||||||
|
export function ErrorResponse<
|
||||||
|
P extends MyPaths,
|
||||||
|
M extends Methods<P>,
|
||||||
|
C extends StatusCodes<P, M> = StatusCodes<P, M>,
|
||||||
|
>(code: C, error?: string): APIResponse<P, M> {
|
||||||
|
console.log(code, error);
|
||||||
|
const responseData = {
|
||||||
|
error: error ?? httpStatus[`${httpStatus[code]}_NAME`],
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(responseData), {
|
||||||
|
status: httpStatus[code],
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Res<
|
||||||
|
P extends MyPaths,
|
||||||
|
M extends Methods<P>,
|
||||||
|
C extends StatusCodes<P, M> = StatusCodes<P, M>,
|
||||||
|
>(code: C, payload: ResponseSchemas<P, M, C>): APIResponse<P, M> {
|
||||||
|
return new Response(payload === null ? null : JSON.stringify(payload), {
|
||||||
|
status: httpStatus[code],
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
42
src/lib/zod.ts
Normal file
42
src/lib/zod.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const zodId = z
|
||||||
|
.string()
|
||||||
|
.refine((value) => /^\d{7,20}$/.test(value), "Invalid ID supplied");
|
||||||
|
export const zodBigIntId = zodId.transform((value) => BigInt(value));
|
||||||
|
|
||||||
|
export const zodTpMessages = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
channelId: zodId.nullable(),
|
||||||
|
rolesEnabled: z.boolean(),
|
||||||
|
isAvailableRoleId: zodId.nullable(),
|
||||||
|
wantsToBeNotifieRoledId: zodId.nullable(),
|
||||||
|
messageIds: z.object({
|
||||||
|
"0": zodId.nullable(),
|
||||||
|
"1": zodId.nullable(),
|
||||||
|
"2": zodId.nullable(),
|
||||||
|
"3": zodId.nullable(),
|
||||||
|
"4": zodId.nullable(),
|
||||||
|
"5": zodId.nullable(),
|
||||||
|
"6": zodId.nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zodMatch = z.object({
|
||||||
|
match: z.object({
|
||||||
|
channelId: zodId,
|
||||||
|
createrId: zodId,
|
||||||
|
messageId: zodId,
|
||||||
|
roleId: zodId,
|
||||||
|
matchType: z.string(),
|
||||||
|
opponentName: z.string(),
|
||||||
|
utc_ts: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
timezone: z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) => moment.tz.names().includes(value),
|
||||||
|
"Unknown timezone supplied",
|
||||||
|
),
|
||||||
|
});
|
100
src/middleware.ts
Normal file
100
src/middleware.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { createMiddleware } from "@solidjs/start/middleware";
|
||||||
|
import colors from "colors";
|
||||||
|
import fs from "fs";
|
||||||
|
import { verifyRequestOrigin } from "lucia";
|
||||||
|
import { appendHeader, getCookie, getHeader } from "vinxi/http";
|
||||||
|
import { lucia } from "./lib/auth";
|
||||||
|
|
||||||
|
colors.enable();
|
||||||
|
|
||||||
|
let started: boolean = false;
|
||||||
|
|
||||||
|
export default createMiddleware({
|
||||||
|
onRequest: async (event) => {
|
||||||
|
if (event.nativeEvent.node.req.method !== "GET") {
|
||||||
|
const originHeader = getHeader(event, "Origin") ?? null;
|
||||||
|
const hostHeader = getHeader(event, "Host") ?? null;
|
||||||
|
if (
|
||||||
|
!originHeader ||
|
||||||
|
!hostHeader ||
|
||||||
|
!verifyRequestOrigin(originHeader, [hostHeader])
|
||||||
|
) {
|
||||||
|
event.nativeEvent.node.res.writeHead(403).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = getCookie(event, lucia.sessionCookieName) ?? null;
|
||||||
|
if (!sessionId) {
|
||||||
|
event.nativeEvent.context.session = null;
|
||||||
|
event.nativeEvent.context.user = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session, user } = await lucia.validateSession(sessionId);
|
||||||
|
if (session && session.fresh) {
|
||||||
|
appendHeader(
|
||||||
|
event,
|
||||||
|
"Set-Cookie",
|
||||||
|
lucia.createSessionCookie(session.id).serialize(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!session) {
|
||||||
|
appendHeader(
|
||||||
|
event,
|
||||||
|
"Set-Cookie",
|
||||||
|
lucia.createBlankSessionCookie().serialize(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
event.nativeEvent.context.session = session;
|
||||||
|
event.nativeEvent.context.user = user;
|
||||||
|
},
|
||||||
|
onBeforeResponse: async (event, response) => {
|
||||||
|
let consoleLog = "",
|
||||||
|
fileLog = "";
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
try {
|
||||||
|
await fs.promises.mkdir("log");
|
||||||
|
console.log("Created 'log' Folder.");
|
||||||
|
} catch {}
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(currentDate.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
|
||||||
|
const seconds = String(currentDate.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
|
// Create a short and numeric representation
|
||||||
|
const date = `[${year}-${month}-${day}_${hours}:${minutes}:${seconds}]`;
|
||||||
|
const xForwardedFor = event.request.headers.get("x-forwarded-for");
|
||||||
|
const ip = (xForwardedFor || "127.0.0.1, 192.168.178.1").split(",");
|
||||||
|
const route = event.request.url;
|
||||||
|
const frontend = !new URL(event.request.url).pathname.startsWith("/api");
|
||||||
|
const method = frontend ? "Frontend" : event.request.method;
|
||||||
|
const code =
|
||||||
|
(response.body as Response | undefined)?.status ?? event.response.status;
|
||||||
|
consoleLog += [
|
||||||
|
date,
|
||||||
|
ip[0].yellow,
|
||||||
|
method,
|
||||||
|
code,
|
||||||
|
route?.green,
|
||||||
|
event.nativeEvent.context.user?.discord_id.rainbow,
|
||||||
|
].join(" ");
|
||||||
|
fileLog += [
|
||||||
|
date,
|
||||||
|
ip[0],
|
||||||
|
method,
|
||||||
|
code,
|
||||||
|
route,
|
||||||
|
event.nativeEvent.context.user?.discord_id,
|
||||||
|
].join(" ");
|
||||||
|
await fs.promises.appendFile("log/log.txt", fileLog + "\n");
|
||||||
|
console.log(consoleLog);
|
||||||
|
},
|
||||||
|
});
|
|
@ -2,47 +2,100 @@ import { APIEvent } from "@solidjs/start/server/types";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import db from "~/drizzle";
|
import db from "~/drizzle";
|
||||||
import { guilds } from "~/drizzle/schema";
|
import { guilds } from "~/drizzle/schema";
|
||||||
|
import { BasicAuth } from "~/lib/auth";
|
||||||
|
import { buildConfig } from "~/lib/responseBuilders";
|
||||||
|
import { ErrorResponse, Res } from "~/lib/responses";
|
||||||
|
import { zodBigIntId } from "~/lib/zod";
|
||||||
|
import { APIResponse } from "~/types/backend";
|
||||||
|
|
||||||
export const GET = async ({ params }: APIEvent) => {
|
type Path = "/api/{guildId}/config";
|
||||||
const guild = await db.query.guilds
|
|
||||||
.findFirst({
|
|
||||||
where: eq(guilds.id, params.guildId),
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!guild)
|
export const GET = async (
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
event: APIEvent,
|
||||||
status: 404,
|
): Promise<APIResponse<Path, "get">> => {
|
||||||
});
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
return guild;
|
default:
|
||||||
};
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
export const DELETE = async ({ params }: APIEvent) => {
|
|
||||||
const guildQuery = await db.query.guilds
|
const guildQuery = await db.query.guilds
|
||||||
.findFirst({
|
.findFirst({
|
||||||
where: eq(guilds.id, params.guildId),
|
where: eq(guilds.id, guildId),
|
||||||
with: {
|
with: { tpMessages: true, matches: true },
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (!guildQuery)
|
if (!guildQuery) return ErrorResponse("NOT_FOUND");
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
const guild = await db
|
return Res("OK", buildConfig(guildQuery));
|
||||||
.delete(guilds)
|
};
|
||||||
.where(eq(guilds.id, params.guildId))
|
|
||||||
.returning()
|
export const POST = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "post">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildQuery = await db.insert(guilds).values({ id: guildId }).execute();
|
||||||
|
|
||||||
|
if (!guildQuery) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
return Res("NO_CONTENT", null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "delete">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildQuery = await db.query.guilds
|
||||||
|
.findFirst({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: { tpMessages: true, matches: true },
|
||||||
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return guild;
|
if (!guildQuery) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
await db.delete(guilds).where(eq(guilds.id, guildId)).execute();
|
||||||
|
|
||||||
|
return Res("NO_CONTENT", null);
|
||||||
};
|
};
|
||||||
|
|
108
src/routes/api/[guildId]/matches.ts
Normal file
108
src/routes/api/[guildId]/matches.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { guilds, matches } from "~/drizzle/schema";
|
||||||
|
import { BasicAuth } from "~/lib/auth";
|
||||||
|
import { buildMatches } from "~/lib/responseBuilders";
|
||||||
|
import { ErrorResponse, Res } from "~/lib/responses";
|
||||||
|
import { zodBigIntId, zodMatch } from "~/lib/zod";
|
||||||
|
import { APIResponse, RequestBody } from "~/types/backend";
|
||||||
|
|
||||||
|
type Path = "/api/{guildId}/matches";
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "get">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await db.query.guilds
|
||||||
|
.findFirst({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: {
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!guild) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
if (guild.matches.length < 1) return Res("NO_CONTENT", null);
|
||||||
|
|
||||||
|
return Res("OK", {
|
||||||
|
matches: buildMatches(guild.matches),
|
||||||
|
timezone: guild.timezone,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "post">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await db.query.guilds
|
||||||
|
.findFirst({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: {
|
||||||
|
matches: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!guild) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
const unparsedBody = await new Response(event.request.body).json();
|
||||||
|
|
||||||
|
let body: RequestBody<Path, "post">;
|
||||||
|
try {
|
||||||
|
body = zodMatch.parse(unparsedBody);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.timezone !== guild.timezone)
|
||||||
|
return ErrorResponse(
|
||||||
|
"BAD_REQUEST",
|
||||||
|
"Match's timezone is different from guild's timezone",
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.insert(matches).values({
|
||||||
|
guildId: guild.id,
|
||||||
|
channelId: BigInt(body.match.channelId),
|
||||||
|
roleId: BigInt(body.match.roleId),
|
||||||
|
createrId: BigInt(body.match.createrId),
|
||||||
|
messageId: BigInt(body.match.messageId),
|
||||||
|
matchType: body.match.matchType,
|
||||||
|
opponentName: body.match.opponentName,
|
||||||
|
utc_ts: new Date(body.match.utc_ts),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Res("NO_CONTENT", null);
|
||||||
|
};
|
|
@ -1,24 +0,0 @@
|
||||||
import { APIEvent } from "@solidjs/start/server/types";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import db from "~/drizzle";
|
|
||||||
import { guilds } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const PUT = async ({ params }: APIEvent) => {
|
|
||||||
const guild = await db.query.guilds
|
|
||||||
.findFirst({
|
|
||||||
where: eq(guilds.id, params.guildId),
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!guild)
|
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
return "TODO";
|
|
||||||
// return guild.timePlanning;
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { APIEvent } from "@solidjs/start/server/types";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import db from "~/drizzle";
|
|
||||||
import { guilds } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const POST = async ({ params }: APIEvent) => {
|
|
||||||
const guild = await db.query.guilds
|
|
||||||
.findFirst({
|
|
||||||
where: eq(guilds.id, params.guildId),
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!guild)
|
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
return "TODO";
|
|
||||||
// return guild.timePlanning;
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { APIEvent } from "@solidjs/start/server/types";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import db from "~/drizzle";
|
|
||||||
import { guilds } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const GET = async ({ params }: APIEvent) => {
|
|
||||||
const guild = await db.query.guilds
|
|
||||||
.findFirst({
|
|
||||||
where: eq(guilds.id, params.guildId),
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!guild)
|
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
return "TODO";
|
|
||||||
// return guild.timePlanning;
|
|
||||||
};
|
|
145
src/routes/api/[guildId]/timePlanning.ts
Normal file
145
src/routes/api/[guildId]/timePlanning.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { guilds, tpMessages } from "~/drizzle/schema";
|
||||||
|
import { BasicAuth } from "~/lib/auth";
|
||||||
|
import { ErrorResponse, Res } from "~/lib/responses";
|
||||||
|
import { zodBigIntId, zodTpMessages } from "~/lib/zod";
|
||||||
|
import { APIResponse, RequestBody } from "~/types/backend";
|
||||||
|
|
||||||
|
type Path = "/api/{guildId}/timePlanning";
|
||||||
|
|
||||||
|
const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
|
||||||
|
type DayKeys = (typeof DayKeys)[number];
|
||||||
|
type Messages = Record<DayKeys, string | null>;
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "get">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await db.query.guilds.findFirst({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: {
|
||||||
|
tpMessages: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!guild) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
const tpMessages = guild.tpMessages.reduce(
|
||||||
|
(acc, message) => {
|
||||||
|
const day = message.day.toString() as DayKeys;
|
||||||
|
if (!/^[0-6]$/.test(day)) return acc;
|
||||||
|
acc[day] = message.messageId?.toString() ?? null;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"0": null,
|
||||||
|
"1": null,
|
||||||
|
"2": null,
|
||||||
|
"3": null,
|
||||||
|
"4": null,
|
||||||
|
"5": null,
|
||||||
|
"6": null,
|
||||||
|
} as Messages,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Res("OK", {
|
||||||
|
enabled: guild.tpEnabled,
|
||||||
|
channelId: guild.tpChannelId?.toString() ?? null,
|
||||||
|
rolesEnabled: guild.tpRolesEnabled,
|
||||||
|
isAvailableRoleId: guild.isAvailableRoleId?.toString() ?? null,
|
||||||
|
wantsToBeNotifieRoledId: guild.wantsToBeNotifieRoledId?.toString() ?? null,
|
||||||
|
messageIds: tpMessages,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUT = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "put">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await db.query.guilds
|
||||||
|
.findFirst({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: { tpMessages: true },
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!guild) return ErrorResponse("NOT_FOUND");
|
||||||
|
|
||||||
|
const unparsedBody = await new Response(event.request.body).json();
|
||||||
|
|
||||||
|
let body: RequestBody<Path, "put">;
|
||||||
|
try {
|
||||||
|
body = zodTpMessages.parse(unparsedBody);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
enabled,
|
||||||
|
channelId,
|
||||||
|
rolesEnabled,
|
||||||
|
isAvailableRoleId,
|
||||||
|
wantsToBeNotifieRoledId,
|
||||||
|
messageIds,
|
||||||
|
} = body;
|
||||||
|
if (guild.tpChannelId !== channelId)
|
||||||
|
await db
|
||||||
|
.update(guilds)
|
||||||
|
.set({
|
||||||
|
tpEnabled: enabled,
|
||||||
|
tpChannelId: channelId ? BigInt(channelId) : null,
|
||||||
|
tpRolesEnabled: rolesEnabled,
|
||||||
|
isAvailableRoleId: isAvailableRoleId ? BigInt(isAvailableRoleId) : null,
|
||||||
|
wantsToBeNotifieRoledId: wantsToBeNotifieRoledId
|
||||||
|
? BigInt(wantsToBeNotifieRoledId)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
.where(eq(guilds.id, guild.id))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
DayKeys.map(async (dayStr) => {
|
||||||
|
const day = parseInt(dayStr);
|
||||||
|
const messageId = messageIds[dayStr];
|
||||||
|
await db
|
||||||
|
.update(tpMessages)
|
||||||
|
.set({ messageId: messageId ? BigInt(messageId) : null })
|
||||||
|
.where(and(eq(tpMessages.guildId, guild.id), eq(tpMessages.day, day)))
|
||||||
|
.execute();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Res("NO_CONTENT", null);
|
||||||
|
};
|
|
@ -1,28 +0,0 @@
|
||||||
import { APIEvent } from "@solidjs/start/server/types";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import db from "~/drizzle";
|
|
||||||
import { guilds } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const GET = async ({ params }: APIEvent) => {
|
|
||||||
const guild = await db.query.guilds
|
|
||||||
.findFirst({
|
|
||||||
where: eq(guilds.id, params.guildId),
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (!guild)
|
|
||||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
|
||||||
status: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
return "TODO";
|
|
||||||
// return guild.timePlanning;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PUT = async () => {
|
|
||||||
return "TODO";
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { SolidAuth } from "@auth/solid-start"
|
|
||||||
import { authOptions } from "~/server/auth"
|
|
||||||
|
|
||||||
export const { GET, POST } = SolidAuth(authOptions)
|
|
130
src/routes/api/auth/callback/discord.ts
Normal file
130
src/routes/api/auth/callback/discord.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import { OAuth2RequestError } from "arctic";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import httpStatus from "http-status";
|
||||||
|
import createClient from "openapi-fetch";
|
||||||
|
import { getCookie, setCookie } from "vinxi/http";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { discordTokens, users } from "~/drizzle/schema";
|
||||||
|
import { discord, lucia } from "~/lib/auth";
|
||||||
|
import { paths } from "~/types/discord";
|
||||||
|
|
||||||
|
export async function GET(event: APIEvent): Promise<Response> {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
const state = url.searchParams.get("state");
|
||||||
|
const error = url.searchParams.get("error");
|
||||||
|
const error_description = url.searchParams.get("error_description");
|
||||||
|
if (error)
|
||||||
|
switch (error) {
|
||||||
|
case "access_denied":
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: { Location: "/" },
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
console.log("Discord oauth error:", error_description);
|
||||||
|
return new Response(decodeURI(error_description ?? ""), {
|
||||||
|
status: httpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedState = getCookie("discord_oauth_state") ?? null;
|
||||||
|
if (!code || !state || !storedState || state !== storedState) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokens = await discord.validateAuthorizationCode(code);
|
||||||
|
const { GET } = createClient<paths>({
|
||||||
|
baseUrl: "https://discord.com/api/v10",
|
||||||
|
});
|
||||||
|
const discordUserResponse = await GET("/users/@me", {
|
||||||
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||||
|
});
|
||||||
|
if (discordUserResponse.error) throw discordUserResponse.error;
|
||||||
|
const discordUser = discordUserResponse.data;
|
||||||
|
const existingUser = await db.query.users
|
||||||
|
.findFirst({
|
||||||
|
where: eq(users.discord_id, discordUser.id),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
const session = await lucia.createSession(
|
||||||
|
existingUser.id,
|
||||||
|
{},
|
||||||
|
{ sessionId: createId() },
|
||||||
|
);
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
setCookie(
|
||||||
|
sessionCookie.name,
|
||||||
|
sessionCookie.value,
|
||||||
|
sessionCookie.attributes,
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
name: discordUser.global_name,
|
||||||
|
image: discordUser.avatar,
|
||||||
|
})
|
||||||
|
.where(eq(users.discord_id, discordUser.id))
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: { Location: "/config" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = createId();
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
discord_id: discordUser.id,
|
||||||
|
name: discordUser.global_name,
|
||||||
|
image: discordUser.avatar,
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.insert(discordTokens)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
accessToken: tokens.accessToken,
|
||||||
|
expiresAt: tokens.accessTokenExpiresAt,
|
||||||
|
refreshToken: tokens.refreshToken,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
const session = await lucia.createSession(
|
||||||
|
userId,
|
||||||
|
{},
|
||||||
|
{ sessionId: createId() },
|
||||||
|
);
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
setCookie(
|
||||||
|
sessionCookie.name,
|
||||||
|
sessionCookie.value,
|
||||||
|
sessionCookie.attributes,
|
||||||
|
);
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: { Location: "/config" },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// the specific error message depends on the provider
|
||||||
|
if (e instanceof OAuth2RequestError) {
|
||||||
|
// invalid code
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error("Unknown error on callback.");
|
||||||
|
console.error(e);
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
src/routes/api/auth/login.ts
Normal file
28
src/routes/api/auth/login.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import { generateState } from "arctic";
|
||||||
|
import httpStatus from "http-status";
|
||||||
|
import { setCookie } from "vinxi/http";
|
||||||
|
import { discord } from "~/lib/auth";
|
||||||
|
|
||||||
|
if (typeof import.meta.env.PROD === "undefined")
|
||||||
|
throw new Error("No env PROD found!");
|
||||||
|
|
||||||
|
export async function GET(event: APIEvent) {
|
||||||
|
const state = generateState();
|
||||||
|
const url = await discord.createAuthorizationURL(state, {
|
||||||
|
scopes: ["identify", "guilds", "guilds.members.read"],
|
||||||
|
});
|
||||||
|
|
||||||
|
setCookie(event, "discord_oauth_state", state, {
|
||||||
|
path: "/",
|
||||||
|
secure: import.meta.env.PROD,
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 60 * 10,
|
||||||
|
sameSite: "lax",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: { Location: url.toString() },
|
||||||
|
});
|
||||||
|
}
|
20
src/routes/api/auth/logout.ts
Normal file
20
src/routes/api/auth/logout.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import httpStatus from "http-status";
|
||||||
|
import { appendHeader } from "vinxi/http";
|
||||||
|
import { lucia } from "~/lib/auth";
|
||||||
|
|
||||||
|
export const GET = async (event: APIEvent) => {
|
||||||
|
if (!event.nativeEvent.context.session) {
|
||||||
|
return new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
await lucia.invalidateSession(event.nativeEvent.context.session.id);
|
||||||
|
appendHeader(
|
||||||
|
event,
|
||||||
|
"Set-Cookie",
|
||||||
|
lucia.createBlankSessionCookie().serialize(),
|
||||||
|
);
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: { Location: "/" },
|
||||||
|
});
|
||||||
|
};
|
43
src/routes/api/boot.ts
Normal file
43
src/routes/api/boot.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server/types";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { guilds } from "~/drizzle/schema";
|
||||||
|
import { BasicAuth } from "~/lib/auth";
|
||||||
|
import { buildConfig } from "~/lib/responseBuilders";
|
||||||
|
import { ErrorResponse, Res } from "~/lib/responses";
|
||||||
|
import { zodBigIntId } from "~/lib/zod";
|
||||||
|
import { APIResponse } from "~/types/backend";
|
||||||
|
|
||||||
|
type Path = "/api/boot";
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
event: APIEvent,
|
||||||
|
): Promise<APIResponse<Path, "get">> => {
|
||||||
|
switch (event.request.headers.get("authorization")) {
|
||||||
|
case BasicAuth.unencoded:
|
||||||
|
case BasicAuth.encoded:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ErrorResponse("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(event.params.guildId);
|
||||||
|
} catch (e) {
|
||||||
|
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildQuery = await db.query.guilds
|
||||||
|
.findMany({
|
||||||
|
where: eq(guilds.id, guildId),
|
||||||
|
with: { tpMessages: true, matches: true },
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return Res(
|
||||||
|
"OK",
|
||||||
|
guildQuery.map((e) => buildConfig(e)),
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +0,0 @@
|
||||||
import db from "~/drizzle";
|
|
||||||
|
|
||||||
export const GET = async () => {
|
|
||||||
const guilds = await db.query.guilds
|
|
||||||
.findMany({
|
|
||||||
with: {
|
|
||||||
timePlanning: { with: { messages: true } },
|
|
||||||
matches: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return { guilds };
|
|
||||||
};
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
@ -16,11 +15,13 @@ import { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
import db from "~/drizzle";
|
import db from "~/drizzle";
|
||||||
import { accounts } from "~/drizzle/schema";
|
import { discordTokens } from "~/drizzle/schema";
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
import "../../styles/pages/config.scss";
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_BOT_TOKEN === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_BOT_TOKEN found!");
|
||||||
|
|
||||||
const guessTZ = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const guessTZ = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
const initialValue = (params: ReturnType<typeof useParams>) => ({
|
const initialValue = (params: ReturnType<typeof useParams>) => ({
|
||||||
|
@ -47,26 +48,21 @@ const getPayload = async (
|
||||||
if (!event) return { success: false, message: "No request event!" };
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
|
|
||||||
const pathname = new URL(event.request.url).pathname;
|
const pathname = new URL(event.request.url).pathname;
|
||||||
const session = await getSession(event.request, authOptions);
|
const { user } = event.nativeEvent.context;
|
||||||
if (!session?.user?.id)
|
if (!user) return { success: false, message: "User not logged in!" };
|
||||||
return { success: false, message: "No user with id!" };
|
|
||||||
|
|
||||||
const { DISCORD_ACCESS_TOKEN } = (
|
const tokens = await db.query.discordTokens
|
||||||
await db
|
.findFirst({
|
||||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
where: eq(discordTokens.userId, user.id),
|
||||||
.from(accounts)
|
})
|
||||||
.where(eq(accounts.userId, session.user?.id))
|
.execute();
|
||||||
.limit(1)
|
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
if (!DISCORD_ACCESS_TOKEN)
|
|
||||||
return { success: false, message: "No discord access token!" };
|
|
||||||
|
|
||||||
const { GET } = createClient<paths>({
|
const { GET } = createClient<paths>({
|
||||||
baseUrl: "https://discord.com/api/v10",
|
baseUrl: "https://discord.com/api/v10",
|
||||||
});
|
});
|
||||||
const guildsRequest = await GET("/users/@me/guilds", {
|
const guildsRequest = await GET("/users/@me/guilds", {
|
||||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||||
});
|
});
|
||||||
const channelsRequest = await GET("/guilds/{guild_id}/channels", {
|
const channelsRequest = await GET("/guilds/{guild_id}/channels", {
|
||||||
params: {
|
params: {
|
||||||
|
@ -98,7 +94,7 @@ const getPayload = async (
|
||||||
"User is no MANAGE_GUILD permissions on this guild with requested id!",
|
"User is no MANAGE_GUILD permissions on this guild with requested id!",
|
||||||
};
|
};
|
||||||
|
|
||||||
let channels: ReturnType<typeof initialValue>["guild"]["channels"] = [];
|
const channels: ReturnType<typeof initialValue>["guild"]["channels"] = [];
|
||||||
channelsRequest.data?.forEach((channel) => {
|
channelsRequest.data?.forEach((channel) => {
|
||||||
if (channel.type !== 0) return;
|
if (channel.type !== 0) return;
|
||||||
channels.push({
|
channels.push({
|
||||||
|
@ -115,7 +111,6 @@ const getPayload = async (
|
||||||
id: guild.id,
|
id: guild.id,
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
icon: guild.icon,
|
icon: guild.icon,
|
||||||
// channel: "1162917335275950180",
|
|
||||||
channel: "",
|
channel: "",
|
||||||
channels,
|
channels,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import {
|
import {
|
||||||
faBadgeCheck,
|
faBadgeCheck,
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
|
@ -12,11 +11,16 @@ import { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
import db from "~/drizzle";
|
import db from "~/drizzle";
|
||||||
import { accounts } from "~/drizzle/schema";
|
import { discordTokens } from "~/drizzle/schema";
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
import "../../styles/pages/config.scss";
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_CLIENT_ID found!");
|
||||||
|
|
||||||
|
if (typeof import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS === "undefined")
|
||||||
|
throw new Error("No env VITE_DISCORD_OAUTH2_PERMISSIONS found!");
|
||||||
|
|
||||||
const initialValue = () => ({
|
const initialValue = () => ({
|
||||||
success: null as boolean | null,
|
success: null as boolean | null,
|
||||||
guilds: [] as {
|
guilds: [] as {
|
||||||
|
@ -36,26 +40,21 @@ const getPayload = async (): Promise<
|
||||||
if (!event) return { success: false, message: "No request event!" };
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
|
|
||||||
const pathname = new URL(event.request.url).pathname;
|
const pathname = new URL(event.request.url).pathname;
|
||||||
const session = await getSession(event.request, authOptions);
|
const { user } = event.nativeEvent.context;
|
||||||
if (!session?.user?.id)
|
if (!user) return { success: false, message: "User not logged in!" };
|
||||||
return { success: false, message: "No user with id!" };
|
|
||||||
|
|
||||||
const { DISCORD_ACCESS_TOKEN } = (
|
const tokens = await db.query.discordTokens
|
||||||
await db
|
.findFirst({
|
||||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
where: eq(discordTokens.userId, user.id),
|
||||||
.from(accounts)
|
})
|
||||||
.where(eq(accounts.userId, session.user?.id))
|
.execute();
|
||||||
.limit(1)
|
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
if (!DISCORD_ACCESS_TOKEN)
|
|
||||||
return { success: false, message: "No discord access token!" };
|
|
||||||
|
|
||||||
const { GET } = createClient<paths>({
|
const { GET } = createClient<paths>({
|
||||||
baseUrl: "https://discord.com/api/v10",
|
baseUrl: "https://discord.com/api/v10",
|
||||||
});
|
});
|
||||||
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
||||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -112,7 +111,7 @@ function index() {
|
||||||
href={
|
href={
|
||||||
i() % 3 === 0
|
i() % 3 === 0
|
||||||
? `/config/${guild.id}`
|
? `/config/${guild.id}`
|
||||||
: `https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_BOT_PERMISSIONS}&scope=bot&guild_id=${guild.id}`
|
: `https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS}&scope=bot&guild_id=${guild.id}`
|
||||||
}
|
}
|
||||||
class="flex-row centered"
|
class="flex-row centered"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import Discord from "@auth/core/providers/discord";
|
|
||||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
|
||||||
import { type SolidAuthConfig } from "@auth/solid-start";
|
|
||||||
import db from "~/drizzle";
|
|
||||||
|
|
||||||
export const authOptions: SolidAuthConfig = {
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
...Discord({
|
|
||||||
clientId: import.meta.env.VITE_DISCORD_CLIENT_ID,
|
|
||||||
clientSecret: import.meta.env.VITE_DISCORD_CLIENT_SECRET,
|
|
||||||
}),
|
|
||||||
authorization:
|
|
||||||
"https://discord.com/api/oauth2/authorize?scope=identify+email+guilds+guilds.members.read",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
adapter: DrizzleAdapter(db),
|
|
||||||
secret: import.meta.env.VITE_AUTH_SECRET,
|
|
||||||
callbacks: {
|
|
||||||
// @ts-ignore
|
|
||||||
session: ({ session, user }) => {
|
|
||||||
if (session?.user) {
|
|
||||||
session.user.id = user.id;
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pages: {
|
|
||||||
// signIn: "/signin",
|
|
||||||
// signOut: "/signout",
|
|
||||||
// error: '/auth/error', // Error code passed in query string as ?error=
|
|
||||||
// verifyRequest: '/auth/verify-request', // (used for check email message)
|
|
||||||
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
|
|
||||||
},
|
|
||||||
redirectProxyUrl: import.meta.env.DEV
|
|
||||||
? import.meta.env.VITE_AUTH_REDIRECT_PROXY_URL
|
|
||||||
: undefined,
|
|
||||||
};
|
|
15
src/types/authjs.d.ts
vendored
15
src/types/authjs.d.ts
vendored
|
@ -1,15 +0,0 @@
|
||||||
import { DefaultSession as DSession } from "@auth/core/types"
|
|
||||||
|
|
||||||
declare module "@auth/core/types" {
|
|
||||||
/**
|
|
||||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
|
||||||
*/
|
|
||||||
interface Session extends DSession {
|
|
||||||
user?: {
|
|
||||||
id: string
|
|
||||||
name?: string | null
|
|
||||||
email?: string | null
|
|
||||||
image?: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
70
src/types/backend.d.ts
vendored
Normal file
70
src/types/backend.d.ts
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { HttpStatus } from "http-status";
|
||||||
|
import { paths } from "./liljudd";
|
||||||
|
|
||||||
|
export type MyPaths = keyof paths;
|
||||||
|
|
||||||
|
export type Methods<Path extends MyPaths> = keyof paths[Path];
|
||||||
|
|
||||||
|
export type Responses<
|
||||||
|
Path extends MyPaths,
|
||||||
|
Method extends Methods<Path>,
|
||||||
|
> = "responses" extends keyof paths[Path][Method]
|
||||||
|
? paths[Path][Method]["responses"]
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type StatusCodes<P extends MyPaths, M extends Methods<P>> = {
|
||||||
|
[CodeName in keyof HttpStatus]: HttpStatus[CodeName] extends number
|
||||||
|
? HttpStatus[CodeName] extends keyof Responses<P, M>
|
||||||
|
? CodeName
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
}[keyof HttpStatus];
|
||||||
|
|
||||||
|
export type ResponseSchemas<
|
||||||
|
Path extends MyPaths,
|
||||||
|
Method extends Methods<Path>,
|
||||||
|
Code extends StatusCodes<Path, Method>,
|
||||||
|
> = Code extends keyof HttpStatus
|
||||||
|
? HttpStatus[Code] extends keyof Responses<Path, Method>
|
||||||
|
? "content" extends keyof Responses<Path, Method>[HttpStatus[Code]]
|
||||||
|
? "application/json" extends keyof Responses<
|
||||||
|
Path,
|
||||||
|
Method
|
||||||
|
>[HttpStatus[Code]]["content"]
|
||||||
|
? Responses<
|
||||||
|
Path,
|
||||||
|
Method
|
||||||
|
>[HttpStatus[Code]]["content"]["application/json"] extends never
|
||||||
|
? null
|
||||||
|
: Responses<
|
||||||
|
Path,
|
||||||
|
Method
|
||||||
|
>[HttpStatus[Code]]["content"]["application/json"]
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type Parameters<
|
||||||
|
Path extends MyPaths,
|
||||||
|
Method extends Methods<Path>,
|
||||||
|
> = "parameters" extends keyof paths[Path][Method]
|
||||||
|
? "path" extends keyof paths[Path][Method]["parameters"]
|
||||||
|
? paths[Path][Method]["parameters"]["path"]
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type RequestBody<
|
||||||
|
Path extends MyPaths,
|
||||||
|
Method extends Methods<Path>,
|
||||||
|
> = "requestBody" extends keyof paths[Path][Method]
|
||||||
|
? "content" extends keyof paths[Path][Method]["requestBody"]
|
||||||
|
? "application/json" extends keyof paths[Path][Method]["requestBody"]["content"]
|
||||||
|
? paths[Path][Method]["requestBody"]["content"]["application/json"]
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export interface APIResponse<Path extends MyPaths, Method extends Methods<Path>>
|
||||||
|
extends Response {}
|
13
src/types/db.d.ts
vendored
Normal file
13
src/types/db.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { PgColumn, PgTableWithColumns } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export type GetColumns<T> =
|
||||||
|
T extends PgTableWithColumns<infer Table> ? Table["columns"] : never;
|
||||||
|
|
||||||
|
export type ExtractDataTypes<T> = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[K in keyof T]: T[K] extends PgColumn<infer ColumnConfig, any, any>
|
||||||
|
? ColumnConfig["notNull"] extends true
|
||||||
|
? ColumnConfig["data"]
|
||||||
|
: ColumnConfig["data"] | null
|
||||||
|
: unknown;
|
||||||
|
};
|
3
src/types/env.d.ts
vendored
3
src/types/env.d.ts
vendored
|
@ -4,7 +4,7 @@ interface ImportMetaEnv {
|
||||||
readonly VITE_DISCORD_CLIENT_ID: string;
|
readonly VITE_DISCORD_CLIENT_ID: string;
|
||||||
readonly VITE_DISCORD_CLIENT_SECRET: string;
|
readonly VITE_DISCORD_CLIENT_SECRET: string;
|
||||||
readonly VITE_DISCORD_BOT_TOKEN: string;
|
readonly VITE_DISCORD_BOT_TOKEN: string;
|
||||||
readonly VITE_DISCORD_BOT_PERMISSIONS: string;
|
readonly VITE_DISCORD_OAUTH2_PERMISSIONS: string;
|
||||||
|
|
||||||
readonly VITE_AUTH_SECRET: string;
|
readonly VITE_AUTH_SECRET: string;
|
||||||
readonly VITE_AUTH_REDIRECT_PROXY_URL: string | undefined;
|
readonly VITE_AUTH_REDIRECT_PROXY_URL: string | undefined;
|
||||||
|
@ -12,7 +12,6 @@ interface ImportMetaEnv {
|
||||||
readonly VITE_DATABASE_URL: string;
|
readonly VITE_DATABASE_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
0
src/global.d.ts → src/types/global.d.ts
vendored
0
src/global.d.ts → src/types/global.d.ts
vendored
411
src/types/liljudd.d.ts
vendored
411
src/types/liljudd.d.ts
vendored
|
@ -5,24 +5,53 @@
|
||||||
|
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/api/config/{guildId}": {
|
"/api/boot": {
|
||||||
/**
|
/**
|
||||||
* Find guild config by ID
|
* Retrieve all guild's configs
|
||||||
* @description Returns a single guild config
|
* @description Returns all guild's configs.
|
||||||
|
*/
|
||||||
|
get: operations["getGuildsForBoot"];
|
||||||
|
};
|
||||||
|
"/api/{guildId}/config": {
|
||||||
|
/**
|
||||||
|
* Find a guild's config by ID
|
||||||
|
* @description Returns a single guild's config.
|
||||||
*/
|
*/
|
||||||
get: operations["getGuildById"];
|
get: operations["getGuildById"];
|
||||||
/**
|
/**
|
||||||
* Deletes a guild config by ID
|
* Creates a guild's config by ID
|
||||||
* @description Delete a guild's config
|
* @description Create a guild's config when the bot is has joined a new guild.
|
||||||
|
*/
|
||||||
|
post: operations["postGuildById"];
|
||||||
|
/**
|
||||||
|
* Deletes a guild's config by ID
|
||||||
|
* @description Delete a guild's config when the bot is removed from the guild.
|
||||||
*/
|
*/
|
||||||
delete: operations["deleteGuildById"];
|
delete: operations["deleteGuildById"];
|
||||||
};
|
};
|
||||||
"/api/tp_messages/{guildId}": {
|
"/api/{guildId}/timePlanning": {
|
||||||
/**
|
/**
|
||||||
* Find guild by ID for it's tp_messages
|
* Find the timePlanning of guild by ID
|
||||||
* @description Returns tp_messages for a guild
|
* @description Returns timePlanning for a guild
|
||||||
*/
|
*/
|
||||||
get: operations["getTp_messagesOfGuildById"];
|
get: operations["gettimePlanningOfGuildById"];
|
||||||
|
/**
|
||||||
|
* Put new message IDs for timePlanning of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
put: operations["puttimePlanningOfGuildById"];
|
||||||
|
};
|
||||||
|
"/api/{guildId}/matches": {
|
||||||
|
/**
|
||||||
|
* Find all matches of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
get: operations["getMatchesOfGuildById"];
|
||||||
|
/**
|
||||||
|
* Save a new created match of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
post: operations["postMatchOfGuildById"];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,67 +60,72 @@ export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
guildConfig: {
|
guildConfig: {
|
||||||
|
guildId: components["schemas"]["id"];
|
||||||
/**
|
/**
|
||||||
* Format: varchar(19)
|
* Format: text
|
||||||
* @example 1234567890123456789
|
* @example Europe/Berlin
|
||||||
*/
|
*/
|
||||||
guildID?: string;
|
timezone: string;
|
||||||
features?: {
|
features: {
|
||||||
time_planning?: {
|
timePlanning: {
|
||||||
/**
|
enabled: boolean;
|
||||||
* Format: varchar(19)
|
channelId: components["schemas"]["idOrNull"];
|
||||||
* @example 1234567890123456789
|
/** @example 0 */
|
||||||
*/
|
targetMinute: number;
|
||||||
channelID?: string;
|
/** @example 1 */
|
||||||
/** @example 0 0 1 * * * 60o 1w */
|
targetHour: number;
|
||||||
cron?: string;
|
/** @example 1 */
|
||||||
/**
|
targetDay: number;
|
||||||
* Format: varchar(19)
|
roles: {
|
||||||
* @example 1234567890123456789
|
enabled: boolean;
|
||||||
*/
|
isAvailableRoleId: components["schemas"]["idOrNull"];
|
||||||
isAvailableRoleId?: string;
|
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
|
||||||
/**
|
};
|
||||||
* Format: varchar(19)
|
|
||||||
* @example 1234567890123456789
|
|
||||||
*/
|
|
||||||
wantsToBeNotifieRoledId?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
matches?: components["schemas"]["match"][];
|
matches: components["schemas"]["match"][];
|
||||||
|
checksum: string;
|
||||||
};
|
};
|
||||||
match: {
|
match: {
|
||||||
/**
|
channelId: components["schemas"]["id"];
|
||||||
* Format: varchar(19)
|
createrId: components["schemas"]["id"];
|
||||||
* @example 1234567890123456789
|
roleId: components["schemas"]["id"];
|
||||||
*/
|
messageId: components["schemas"]["id"];
|
||||||
channelID?: string;
|
|
||||||
/**
|
/**
|
||||||
* Format: varchar(50)
|
* Format: varchar(50)
|
||||||
* @example Scrim
|
* @example Scrim
|
||||||
*/
|
*/
|
||||||
matchType?: string;
|
matchType: string;
|
||||||
/**
|
|
||||||
* Format: varchar(19)
|
|
||||||
* @example 1234567890123456789
|
|
||||||
*/
|
|
||||||
createrId?: string;
|
|
||||||
/**
|
|
||||||
* Format: varchar(19)
|
|
||||||
* @example 1234567890123456789
|
|
||||||
*/
|
|
||||||
roleId?: string;
|
|
||||||
/**
|
/**
|
||||||
* Format: varchar(100)
|
* Format: varchar(100)
|
||||||
* @example ?
|
* @example ?
|
||||||
*/
|
*/
|
||||||
opponentName?: string;
|
opponentName: string;
|
||||||
/**
|
/** @example 2020-01-01T00:00:00Z */
|
||||||
* Format: varchar(19)
|
utc_ts: string;
|
||||||
* @example 1234567890123456789
|
};
|
||||||
*/
|
timePlanning: {
|
||||||
messsageId?: string;
|
enabled: boolean;
|
||||||
/** @example 0 0 1 5 2 2023 60o */
|
channelId: components["schemas"]["idOrNull"];
|
||||||
cron?: string;
|
rolesEnabled: boolean;
|
||||||
|
isAvailableRoleId: components["schemas"]["idOrNull"];
|
||||||
|
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
|
||||||
|
messageIds: {
|
||||||
|
0: components["schemas"]["idOrNull"];
|
||||||
|
1: components["schemas"]["idOrNull"];
|
||||||
|
2: components["schemas"]["idOrNull"];
|
||||||
|
3: components["schemas"]["idOrNull"];
|
||||||
|
4: components["schemas"]["idOrNull"];
|
||||||
|
5: components["schemas"]["idOrNull"];
|
||||||
|
6: components["schemas"]["idOrNull"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @example 1234567890123456789 */
|
||||||
|
id: string;
|
||||||
|
/** @example 1234567890123456789 */
|
||||||
|
idOrNull: string | null;
|
||||||
|
error: {
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
|
@ -108,8 +142,40 @@ export type external = Record<string, never>;
|
||||||
export interface operations {
|
export interface operations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find guild config by ID
|
* Retrieve all guild's configs
|
||||||
* @description Returns a single guild config
|
* @description Returns all guild's configs.
|
||||||
|
*/
|
||||||
|
getGuildsForBoot: {
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["guildConfig"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Invalid ID supplied */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Guild not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Find a guild's config by ID
|
||||||
|
* @description Returns a single guild's config.
|
||||||
*/
|
*/
|
||||||
getGuildById: {
|
getGuildById: {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
@ -127,22 +193,32 @@ export interface operations {
|
||||||
};
|
};
|
||||||
/** @description Invalid ID supplied */
|
/** @description Invalid ID supplied */
|
||||||
400: {
|
400: {
|
||||||
content: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
/** @description Guild not found */
|
/** @description Guild not found */
|
||||||
404: {
|
404: {
|
||||||
content: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Deletes a guild config by ID
|
* Creates a guild's config by ID
|
||||||
* @description Delete a guild's config
|
* @description Create a guild's config when the bot is has joined a new guild.
|
||||||
*/
|
*/
|
||||||
deleteGuildById: {
|
postGuildById: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
/** @description ID of guild config to delete */
|
/** @description ID of guild's config to create */
|
||||||
guildId: string;
|
guildId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -153,22 +229,68 @@ export interface operations {
|
||||||
};
|
};
|
||||||
/** @description Invalid ID supplied */
|
/** @description Invalid ID supplied */
|
||||||
400: {
|
400: {
|
||||||
content: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
/** @description Guild not found */
|
/** @description Guild not found */
|
||||||
404: {
|
404: {
|
||||||
content: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Find guild by ID for it's tp_messages
|
* Deletes a guild's config by ID
|
||||||
* @description Returns tp_messages for a guild
|
* @description Delete a guild's config when the bot is removed from the guild.
|
||||||
*/
|
*/
|
||||||
getTp_messagesOfGuildById: {
|
deleteGuildById: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
/** @description ID of guild's tp_messages to return */
|
/** @description ID of guild's config to delete */
|
||||||
|
guildId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Invalid ID supplied */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Guild not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Find the timePlanning of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
gettimePlanningOfGuildById: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
/** @description ID of guild's timePlanning to return */
|
||||||
guildId: string;
|
guildId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -176,21 +298,164 @@ export interface operations {
|
||||||
/** @description successful operation */
|
/** @description successful operation */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["guildConfig"];
|
"application/json": components["schemas"]["timePlanning"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Time planning not enabled for this guild */
|
/** @description Invalid ID supplied */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Guild not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Put new message IDs for timePlanning of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
puttimePlanningOfGuildById: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
/** @description ID of guild's timePlanning to return */
|
||||||
|
guildId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Put new message IDs for timePlanning in channel */
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["timePlanning"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
204: {
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
/** @description Invalid ID supplied */
|
/** @description Invalid ID supplied */
|
||||||
400: {
|
400: {
|
||||||
content: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
/** @description Guild not found */
|
/** @description Guild not found */
|
||||||
404: {
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Find all matches of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
getMatchesOfGuildById: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
/** @description ID of guild's timePlanning to return */
|
||||||
|
guildId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
matches: components["schemas"]["match"][];
|
||||||
|
/**
|
||||||
|
* Format: text
|
||||||
|
* @example Europe/Berlin
|
||||||
|
*/
|
||||||
|
timezone: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Invalid ID supplied */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Guild not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Save a new created match of guild by ID
|
||||||
|
* @description Returns timePlanning for a guild
|
||||||
|
*/
|
||||||
|
postMatchOfGuildById: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
/** @description ID of match's guild to set */
|
||||||
|
guildId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Save a new created match in channel */
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
match: components["schemas"]["match"];
|
||||||
|
/**
|
||||||
|
* Format: text
|
||||||
|
* @description Has to match guild tz
|
||||||
|
* @example Europe/Berlin
|
||||||
|
*/
|
||||||
|
timezone: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
|
204: {
|
||||||
content: never;
|
content: never;
|
||||||
};
|
};
|
||||||
|
/** @description Invalid ID supplied */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Guild not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["error"];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
13
src/types/lucia-auth.d.ts
vendored
Normal file
13
src/types/lucia-auth.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { users } from "~/drizzle/schema";
|
||||||
|
import { lucia } from "~/lib/auth";
|
||||||
|
import { ExtractDataTypes, GetColumns } from "./db";
|
||||||
|
|
||||||
|
declare module "lucia" {
|
||||||
|
interface Register {
|
||||||
|
Lucia: typeof lucia;
|
||||||
|
DatabaseUserAttributes: DatabaseUserAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatabaseUserAttributes
|
||||||
|
extends ExtractDataTypes<GetColumns<typeof users>> {}
|
12
src/types/vinxi.d.ts
vendored
Normal file
12
src/types/vinxi.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { H3EventContext as EventContext } from "h3/dist";
|
||||||
|
import { Session, User } from "lucia";
|
||||||
|
|
||||||
|
declare module "vinxi/http" {
|
||||||
|
interface H3EventContext extends EventContext {
|
||||||
|
user: User | null;
|
||||||
|
session: Session | null;
|
||||||
|
}
|
||||||
|
class H3Eventt {
|
||||||
|
context: H3EventContext;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
import { defineConfig } from "@solidjs/start/config";
|
|
||||||
|
|
||||||
export default defineConfig({});
|
|
Loading…
Reference in a new issue