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
|
||||
},
|
||||
"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
|
||||
log
|
||||
|
||||
dist
|
||||
.vinxi
|
||||
|
@ -27,3 +28,9 @@ gitignore
|
|||
# System Files
|
||||
.DS_Store
|
||||
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:
|
||||
|
||||
```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_BOT_TOKEN=your_discord_bot_token
|
||||
VITE_DISCORD_BOT_PERMISSIONS=18977581952080
|
||||
VITE_DISCORD_OAUTH2_PERMISSIONS=18977581952080
|
||||
|
||||
VITE_AUTH_SECRET=your_auth_secret
|
||||
|
||||
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).
|
||||
|
||||
#### 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.
|
||||
|
||||
```
|
||||
|
|
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",
|
||||
driver: "pg",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL ?? "",
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
},
|
||||
} 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",
|
||||
"liljudd-openapi-gen": "openapi-typescript ./public/api/specs/liljudd.json -o ./src/types/liljudd.d.ts",
|
||||
"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": {
|
||||
"@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/pro-duotone-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-thin-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/router": "^0.12.0",
|
||||
"@solidjs/start": "^0.5.4",
|
||||
"drizzle-orm": "^0.29.3",
|
||||
"@solidjs/router": "^0.12.4",
|
||||
"@solidjs/start": "^0.6.0",
|
||||
"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",
|
||||
"openapi-fetch": "^0.8.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"openapi-fetch": "^0.9.2",
|
||||
"postgres": "^3.4.3",
|
||||
"solid-js": "^1.8.14",
|
||||
"vinxi": "^0.2.1"
|
||||
"solid-js": "^1.8.15",
|
||||
"vinxi": "^0.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"dotenv": "^16.4.2",
|
||||
"@playwright/test": "^1.42.0",
|
||||
"@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-zod": "^0.5.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-solid": "^0.13.1",
|
||||
"h3": "^1.11.1",
|
||||
"openapi-typescript": "^6.7.4",
|
||||
"pg": "^8.11.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
"sass": "^1.71.1",
|
||||
"typescript": "^5.3.3",
|
||||
"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"
|
||||
},
|
||||
"paths": {
|
||||
"/api/boot/config": {
|
||||
"/api/boot": {
|
||||
"get": {
|
||||
"tags": ["Guild configs"],
|
||||
"summary": "Find a guild's config by ID",
|
||||
"description": "Returns a single guild's config.",
|
||||
"operationId": "getGuildsFromBoot",
|
||||
"tags": ["Bot bootup"],
|
||||
"summary": "Retrieve all guild's configs",
|
||||
"description": "Returns all guild's configs.",
|
||||
"operationId": "getGuildsForBoot",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bootConfig"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/guildConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/{guildId}/config": {
|
||||
"get": {
|
||||
"tags": ["Guild configs"],
|
||||
"tags": ["Guild config"],
|
||||
"summary": "Find a guild's config by ID",
|
||||
"description": "Returns a single guild's config.",
|
||||
"operationId": "getGuildById",
|
||||
|
@ -71,28 +98,52 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Guild configs"],
|
||||
"summary": "Deletes a guild's config by ID",
|
||||
"description": "Delete a guild's config when the bot is removed from the guild.",
|
||||
"operationId": "deleteGuildById",
|
||||
"post": {
|
||||
"tags": ["Guild config"],
|
||||
"summary": "Creates a guild's config by ID",
|
||||
"description": "Create a guild's config when the bot is has joined a new guild.",
|
||||
"operationId": "postGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild config to delete",
|
||||
"description": "ID of guild's config to create",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
|
@ -105,30 +156,112 @@
|
|||
"description": "successful operation"
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/{guildId}/tp_messages": {
|
||||
"get": {
|
||||
"tags": ["Time planning messages"],
|
||||
"summary": "Find the tp_messages of guild by ID",
|
||||
"description": "Returns tp_messages for a guild",
|
||||
"operationId": "getTp_messagesOfGuildById",
|
||||
"delete": {
|
||||
"tags": ["Guild config"],
|
||||
"summary": "Deletes a guild's config by ID",
|
||||
"description": "Delete a guild's config when the bot is removed from the guild.",
|
||||
"operationId": "deleteGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"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,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
|
@ -142,37 +275,58 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tp_messages"
|
||||
"$ref": "#/components/schemas/timePlanning"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["Time planning messages"],
|
||||
"summary": "Put message IDs for tp_messages of guild by ID",
|
||||
"description": "Returns tp_messages for a guild",
|
||||
"operationId": "putTp_messagesOfGuildById",
|
||||
"summary": "Put new message IDs for timePlanning of guild by ID",
|
||||
"description": "Returns timePlanning for a guild",
|
||||
"operationId": "puttimePlanningOfGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild's tp_messages to return",
|
||||
"description": "ID of guild's timePlanning to return",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
|
@ -180,30 +334,55 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"requestBody": {
|
||||
"description": "Put new message IDs for timePlanning in channel",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tp_messages"
|
||||
}
|
||||
"$ref": "#/components/schemas/timePlanning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
"description": "successful operation"
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -212,13 +391,13 @@
|
|||
"get": {
|
||||
"tags": ["Matches"],
|
||||
"summary": "Find all matches of guild by ID",
|
||||
"description": "Returns tp_messages for a guild",
|
||||
"description": "Returns timePlanning for a guild",
|
||||
"operationId": "getMatchesOfGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "guildId",
|
||||
"in": "path",
|
||||
"description": "ID of guild's tp_messages to return",
|
||||
"description": "ID of guild's timePlanning to return",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
|
@ -232,36 +411,66 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["matches", "timezone"],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/tp_messages"
|
||||
}
|
||||
}
|
||||
}
|
||||
"$ref": "#/components/schemas/match"
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"format": "text",
|
||||
"example": "Europe/Berlin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/{guildId}/matches/{channelId}": {
|
||||
"post": {
|
||||
"tags": ["Matches"],
|
||||
"summary": "Save a new created match in channel of guild by IDs",
|
||||
"description": "Returns tp_messages for a guild",
|
||||
"summary": "Save a new created match of guild by ID",
|
||||
"description": "Returns timePlanning for a guild",
|
||||
"operationId": "postMatchOfGuildById",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -273,108 +482,69 @@
|
|||
"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)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"requestBody": {
|
||||
"description": "Save a new created match in channel",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tp_messages"
|
||||
"type": "object",
|
||||
"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": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
"description": "successful operation"
|
||||
},
|
||||
"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": {
|
||||
"description": "Guild not found"
|
||||
"description": "Guild not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tp_messages"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "Time planning not enabled for this guild"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Guild not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -382,27 +552,12 @@
|
|||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"bootConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guilds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/guildConfig"
|
||||
}
|
||||
},
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"guildConfig": {
|
||||
"type": "object",
|
||||
"required": ["guildId", "timezone", "features", "matches", "checksum"],
|
||||
"properties": {
|
||||
"guildID": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
"guildId": {
|
||||
"$ref": "#/components/schemas/id"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
|
@ -411,14 +566,24 @@
|
|||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"required": ["timePlanning"],
|
||||
"properties": {
|
||||
"time_planning": {
|
||||
"timePlanning": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"channelId",
|
||||
"targetMinute",
|
||||
"targetHour",
|
||||
"targetDay",
|
||||
"roles"
|
||||
],
|
||||
"properties": {
|
||||
"channelID": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"channelId": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"targetMinute": {
|
||||
"type": "number",
|
||||
|
@ -434,21 +599,20 @@
|
|||
},
|
||||
"roles": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"isAvailableRoleId",
|
||||
"wantsToBeNotifieRoledId"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isAvailableRoleId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789",
|
||||
"nullable": true
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"wantsToBeNotifieRoledId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789",
|
||||
"nullable": true
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -461,85 +625,132 @@
|
|||
"items": {
|
||||
"$ref": "#/components/schemas/match"
|
||||
}
|
||||
},
|
||||
"checksum": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"match": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"channelId",
|
||||
"matchType",
|
||||
"createrId",
|
||||
"roleId",
|
||||
"opponentName",
|
||||
"messageId",
|
||||
"utc_ts"
|
||||
],
|
||||
"properties": {
|
||||
"channelID": {
|
||||
"type": "string",
|
||||
"format": "varcharq(20)",
|
||||
"example": "1234567890123456789"
|
||||
"channelId": {
|
||||
"$ref": "#/components/schemas/id"
|
||||
},
|
||||
"createrId": {
|
||||
"$ref": "#/components/schemas/id"
|
||||
},
|
||||
"roleId": {
|
||||
"$ref": "#/components/schemas/id"
|
||||
},
|
||||
"messageId": {
|
||||
"$ref": "#/components/schemas/id"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"format": "varchar(50)",
|
||||
"example": "Scrim"
|
||||
},
|
||||
"createrId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"roleId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"opponentName": {
|
||||
"type": "string",
|
||||
"format": "varchar(100)",
|
||||
"example": "?"
|
||||
},
|
||||
"messsageId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
},
|
||||
"utc_ts": {
|
||||
"type": "string",
|
||||
"example": "1706180188"
|
||||
"example": "2020-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tp_messages": {
|
||||
"timePlanning": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled",
|
||||
"channelId",
|
||||
"rolesEnabled",
|
||||
"isAvailableRoleId",
|
||||
"wantsToBeNotifieRoledId",
|
||||
"messageIds"
|
||||
],
|
||||
"properties": {
|
||||
"guildId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"channelId": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)",
|
||||
"example": "1234567890123456789"
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"rolesEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isAvailableRoleId": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"wantsToBeNotifieRoledId": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"messageIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "varchar(20)"
|
||||
"type": "object",
|
||||
"required": ["0", "1", "2", "3", "4", "5", "6"],
|
||||
"properties": {
|
||||
"0": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"example": [
|
||||
"1234567890123456789",
|
||||
"1234567890123456789",
|
||||
"1234567890123456789",
|
||||
"1234567890123456789",
|
||||
"1234567890123456789",
|
||||
"1234567890123456789",
|
||||
"1234567890123456789"
|
||||
]
|
||||
"1": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"2": {
|
||||
"$ref": "#/components/schemas/idOrNull"
|
||||
},
|
||||
"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": {
|
||||
"api_key": {
|
||||
"type": "apiKey",
|
||||
"name": "api_key",
|
||||
"in": "header"
|
||||
"basicAuth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { JSX, Show, Suspense } from "solid-js";
|
||||
import { JSX, Show } from "solid-js";
|
||||
import "../styles/components/NavBar.scss";
|
||||
import { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||
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: {
|
||||
href: string;
|
||||
action?: () => void;
|
||||
rel?: string;
|
||||
name?: string;
|
||||
children?: JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<li class="navElem flex-row thick">
|
||||
<a
|
||||
class="flex-row"
|
||||
href={props.href}
|
||||
onClick={() => props.action && props.action()}
|
||||
>
|
||||
<a class="flex-row" href={props.href} rel={props.rel}>
|
||||
{props.children ?? <></>}
|
||||
<Show when={props.name}>
|
||||
<span>{props.name}</span>
|
||||
|
@ -40,14 +42,12 @@ function NavBar() {
|
|||
</ul>
|
||||
<ul class="flex-row responsive thick">
|
||||
<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"
|
||||
>
|
||||
<FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
|
||||
</Li>
|
||||
<Suspense>
|
||||
<NavUser />
|
||||
</Suspense>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
@ -1,70 +1,40 @@
|
|||
import { getSession } from "@auth/solid-start";
|
||||
import { signIn, signOut } from "@auth/solid-start/client";
|
||||
import {
|
||||
faArrowRightFromBracket,
|
||||
faArrowRightToBracket,
|
||||
faGear,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Show, createResource } from "solid-js";
|
||||
import { cache, createAsync } from "@solidjs/router";
|
||||
import { Show } from "solid-js";
|
||||
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 { 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() {
|
||||
"use server";
|
||||
|
||||
const event = getRequestEvent();
|
||||
if (!event)
|
||||
return { success: false, message: "No request event!", ...initialUser };
|
||||
|
||||
const session = await getSession(event.request, authOptions);
|
||||
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 };
|
||||
return event?.nativeEvent.context.user;
|
||||
}
|
||||
|
||||
const cachedUser = cache(() => getUser(), "userInfo");
|
||||
|
||||
function NavUser() {
|
||||
const [user] = createResource(async () => {
|
||||
const user = await getUser();
|
||||
const user = createAsync(() => cachedUser());
|
||||
const pfp = () => {
|
||||
const thisUser = user();
|
||||
if (!thisUser?.id) return "";
|
||||
|
||||
if (!user.success) console.error("userInfo", user.message);
|
||||
|
||||
return user;
|
||||
});
|
||||
return thisUser.image
|
||||
? `https://cdn.discordapp.com/avatars/${thisUser.discord_id}/${thisUser.image}.png`
|
||||
: `https://cdn.discordapp.com/embed/avatars/${(parseInt(thisUser.discord_id) >> 22) % 6}.png`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={user()?.id}
|
||||
fallback={
|
||||
<Li
|
||||
href="#"
|
||||
name="Login"
|
||||
action={() => signIn("discord", { callbackUrl: "/config" })}
|
||||
>
|
||||
<Li href="/api/auth/login" name="Login" rel="external">
|
||||
<FontAwesomeIcon
|
||||
class="secondary"
|
||||
icon={faArrowRightToBracket}
|
||||
|
@ -75,11 +45,11 @@ function NavUser() {
|
|||
>
|
||||
<Li href="/config">
|
||||
<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" />
|
||||
</div>
|
||||
</Li>
|
||||
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
|
||||
<Li href="/api/auth/logout" rel="external">
|
||||
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
||||
</Li>
|
||||
</Show>
|
||||
|
|
|
@ -2,7 +2,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|||
import postgres from "postgres";
|
||||
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, {
|
||||
schema,
|
||||
});
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { AdapterAccount } from "@auth/core/adapters";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
serial,
|
||||
|
@ -13,130 +12,90 @@ import {
|
|||
} from "drizzle-orm/pg-core";
|
||||
|
||||
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"),
|
||||
email: text("email").notNull(),
|
||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
||||
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", {
|
||||
sessionToken: text("sessionToken").notNull().primaryKey(),
|
||||
userId: text("userId")
|
||||
id: varchar("id", { length: 24 }).primaryKey(),
|
||||
userId: varchar("user_id", { length: 24 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
expiresAt: timestamp("expires_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}).notNull(),
|
||||
});
|
||||
|
||||
export const verificationTokens = pgTable(
|
||||
"verificationToken",
|
||||
{
|
||||
identifier: text("identifier").notNull(),
|
||||
token: text("token").notNull(),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
},
|
||||
(vt) => ({
|
||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||
export const discordTokens = pgTable("tokens", {
|
||||
userId: varchar("user_id", { length: 24 })
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
refreshToken: text("refresh_token").notNull(),
|
||||
accessToken: text("access_token").notNull(),
|
||||
expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
|
||||
});
|
||||
|
||||
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 }) => ({
|
||||
guild: one(guilds, {
|
||||
fields: [tpMessages.guildId],
|
||||
references: [guilds.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const matches = pgTable("matches", {
|
||||
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(),
|
||||
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: varchar("message_id", { length: 20 }).notNull(),
|
||||
messageId: bigint("message_id", { mode: "bigint" }).notNull(),
|
||||
utc_ts: timestamp("utc_ts").notNull(),
|
||||
guildId: varchar("guild_id", { length: 20 })
|
||||
guildId: bigint("guild_id", { mode: "bigint" })
|
||||
.notNull()
|
||||
.references(() => guilds.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const matchPlanningsRelations = relations(matchPlannings, ({ one }) => ({
|
||||
export const matchPlanningsRelations = relations(matches, ({ one }) => ({
|
||||
guild: one(guilds, {
|
||||
fields: [matchPlannings.guildId],
|
||||
fields: [matches.guildId],
|
||||
references: [guilds.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const guilds = pgTable("guild", {
|
||||
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(),
|
||||
guildId: varchar("guild_id", { length: 20 })
|
||||
.notNull()
|
||||
.unique()
|
||||
.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(
|
||||
timePlannings,
|
||||
({ one, many }) => ({
|
||||
guild: one(guilds, {
|
||||
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";
|
||||
|
||||
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 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";
|
||||
|
||||
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();
|
||||
type Path = "/api/{guildId}/config";
|
||||
|
||||
if (!guild)
|
||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
||||
status: 404,
|
||||
});
|
||||
export const GET = async (
|
||||
event: APIEvent,
|
||||
): 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
|
||||
.findFirst({
|
||||
where: eq(guilds.id, params.guildId),
|
||||
with: {
|
||||
timePlanning: { with: { messages: true } },
|
||||
matches: true,
|
||||
},
|
||||
where: eq(guilds.id, guildId),
|
||||
with: { tpMessages: true, matches: true },
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!guildQuery)
|
||||
return new Response(JSON.stringify({ error: "No such guild found." }), {
|
||||
status: 404,
|
||||
});
|
||||
if (!guildQuery) return ErrorResponse("NOT_FOUND");
|
||||
|
||||
const guild = await db
|
||||
.delete(guilds)
|
||||
.where(eq(guilds.id, params.guildId))
|
||||
.returning()
|
||||
return Res("OK", buildConfig(guildQuery));
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
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 { useLocation, useNavigate, useParams } from "@solidjs/router";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
@ -16,11 +15,13 @@ import { getRequestEvent } from "solid-js/web";
|
|||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||
import Layout from "~/components/Layout";
|
||||
import db from "~/drizzle";
|
||||
import { accounts } from "~/drizzle/schema";
|
||||
import { authOptions } from "~/server/auth";
|
||||
import { discordTokens } from "~/drizzle/schema";
|
||||
import { paths } from "~/types/discord";
|
||||
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 initialValue = (params: ReturnType<typeof useParams>) => ({
|
||||
|
@ -47,26 +48,21 @@ const getPayload = async (
|
|||
if (!event) return { success: false, message: "No request event!" };
|
||||
|
||||
const pathname = new URL(event.request.url).pathname;
|
||||
const session = await getSession(event.request, authOptions);
|
||||
if (!session?.user?.id)
|
||||
return { success: false, message: "No user with id!" };
|
||||
const { user } = event.nativeEvent.context;
|
||||
if (!user) return { success: false, message: "User not logged in!" };
|
||||
|
||||
const { DISCORD_ACCESS_TOKEN } = (
|
||||
await db
|
||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
||||
.from(accounts)
|
||||
.where(eq(accounts.userId, session.user?.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
)[0];
|
||||
if (!DISCORD_ACCESS_TOKEN)
|
||||
return { success: false, message: "No discord access token!" };
|
||||
const tokens = await db.query.discordTokens
|
||||
.findFirst({
|
||||
where: eq(discordTokens.userId, user.id),
|
||||
})
|
||||
.execute();
|
||||
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||
|
||||
const { GET } = createClient<paths>({
|
||||
baseUrl: "https://discord.com/api/v10",
|
||||
});
|
||||
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", {
|
||||
params: {
|
||||
|
@ -98,7 +94,7 @@ const getPayload = async (
|
|||
"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) => {
|
||||
if (channel.type !== 0) return;
|
||||
channels.push({
|
||||
|
@ -115,7 +111,6 @@ const getPayload = async (
|
|||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.icon,
|
||||
// channel: "1162917335275950180",
|
||||
channel: "",
|
||||
channels,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { getSession } from "@auth/solid-start";
|
||||
import {
|
||||
faBadgeCheck,
|
||||
faCircleExclamation,
|
||||
|
@ -12,11 +11,16 @@ import { getRequestEvent } from "solid-js/web";
|
|||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||
import Layout from "~/components/Layout";
|
||||
import db from "~/drizzle";
|
||||
import { accounts } from "~/drizzle/schema";
|
||||
import { authOptions } from "~/server/auth";
|
||||
import { discordTokens } from "~/drizzle/schema";
|
||||
import { paths } from "~/types/discord";
|
||||
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 = () => ({
|
||||
success: null as boolean | null,
|
||||
guilds: [] as {
|
||||
|
@ -36,26 +40,21 @@ const getPayload = async (): Promise<
|
|||
if (!event) return { success: false, message: "No request event!" };
|
||||
|
||||
const pathname = new URL(event.request.url).pathname;
|
||||
const session = await getSession(event.request, authOptions);
|
||||
if (!session?.user?.id)
|
||||
return { success: false, message: "No user with id!" };
|
||||
const { user } = event.nativeEvent.context;
|
||||
if (!user) return { success: false, message: "User not logged in!" };
|
||||
|
||||
const { DISCORD_ACCESS_TOKEN } = (
|
||||
await db
|
||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
||||
.from(accounts)
|
||||
.where(eq(accounts.userId, session.user?.id))
|
||||
.limit(1)
|
||||
.execute()
|
||||
)[0];
|
||||
if (!DISCORD_ACCESS_TOKEN)
|
||||
return { success: false, message: "No discord access token!" };
|
||||
const tokens = await db.query.discordTokens
|
||||
.findFirst({
|
||||
where: eq(discordTokens.userId, user.id),
|
||||
})
|
||||
.execute();
|
||||
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||
|
||||
const { GET } = createClient<paths>({
|
||||
baseUrl: "https://discord.com/api/v10",
|
||||
});
|
||||
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
@ -112,7 +111,7 @@ function index() {
|
|||
href={
|
||||
i() % 3 === 0
|
||||
? `/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"
|
||||
>
|
||||
|
|
|
@ -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_SECRET: 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_REDIRECT_PROXY_URL: string | undefined;
|
||||
|
@ -12,7 +12,6 @@ interface ImportMetaEnv {
|
|||
readonly VITE_DATABASE_URL: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
interface ImportMeta {
|
||||
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 {
|
||||
"/api/config/{guildId}": {
|
||||
"/api/boot": {
|
||||
/**
|
||||
* Find guild config by ID
|
||||
* @description Returns a single guild config
|
||||
* Retrieve all guild's configs
|
||||
* @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"];
|
||||
/**
|
||||
* Deletes a guild config by ID
|
||||
* @description Delete a guild's config
|
||||
* Creates a guild's config by ID
|
||||
* @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"];
|
||||
};
|
||||
"/api/tp_messages/{guildId}": {
|
||||
"/api/{guildId}/timePlanning": {
|
||||
/**
|
||||
* Find guild by ID for it's tp_messages
|
||||
* @description Returns tp_messages for a guild
|
||||
* Find the timePlanning of guild by ID
|
||||
* @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 {
|
||||
schemas: {
|
||||
guildConfig: {
|
||||
guildId: components["schemas"]["id"];
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
* Format: text
|
||||
* @example Europe/Berlin
|
||||
*/
|
||||
guildID?: string;
|
||||
features?: {
|
||||
time_planning?: {
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
channelID?: string;
|
||||
/** @example 0 0 1 * * * 60o 1w */
|
||||
cron?: string;
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
isAvailableRoleId?: string;
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
wantsToBeNotifieRoledId?: string;
|
||||
timezone: string;
|
||||
features: {
|
||||
timePlanning: {
|
||||
enabled: boolean;
|
||||
channelId: components["schemas"]["idOrNull"];
|
||||
/** @example 0 */
|
||||
targetMinute: number;
|
||||
/** @example 1 */
|
||||
targetHour: number;
|
||||
/** @example 1 */
|
||||
targetDay: number;
|
||||
roles: {
|
||||
enabled: boolean;
|
||||
isAvailableRoleId: components["schemas"]["idOrNull"];
|
||||
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
|
||||
};
|
||||
};
|
||||
matches?: components["schemas"]["match"][];
|
||||
};
|
||||
matches: components["schemas"]["match"][];
|
||||
checksum: string;
|
||||
};
|
||||
match: {
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
channelID?: string;
|
||||
channelId: components["schemas"]["id"];
|
||||
createrId: components["schemas"]["id"];
|
||||
roleId: components["schemas"]["id"];
|
||||
messageId: components["schemas"]["id"];
|
||||
/**
|
||||
* Format: varchar(50)
|
||||
* @example Scrim
|
||||
*/
|
||||
matchType?: string;
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
createrId?: string;
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
roleId?: string;
|
||||
matchType: string;
|
||||
/**
|
||||
* Format: varchar(100)
|
||||
* @example ?
|
||||
*/
|
||||
opponentName?: string;
|
||||
/**
|
||||
* Format: varchar(19)
|
||||
* @example 1234567890123456789
|
||||
*/
|
||||
messsageId?: string;
|
||||
/** @example 0 0 1 5 2 2023 60o */
|
||||
cron?: string;
|
||||
opponentName: string;
|
||||
/** @example 2020-01-01T00:00:00Z */
|
||||
utc_ts: string;
|
||||
};
|
||||
timePlanning: {
|
||||
enabled: boolean;
|
||||
channelId: components["schemas"]["idOrNull"];
|
||||
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;
|
||||
|
@ -108,8 +142,40 @@ export type external = Record<string, never>;
|
|||
export interface operations {
|
||||
|
||||
/**
|
||||
* Find guild config by ID
|
||||
* @description Returns a single guild config
|
||||
* Retrieve all guild's configs
|
||||
* @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: {
|
||||
parameters: {
|
||||
|
@ -127,22 +193,32 @@ export interface operations {
|
|||
};
|
||||
/** @description Invalid ID supplied */
|
||||
400: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
/** @description Guild not found */
|
||||
404: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Deletes a guild config by ID
|
||||
* @description Delete a guild's config
|
||||
* Creates a guild's config by ID
|
||||
* @description Create a guild's config when the bot is has joined a new guild.
|
||||
*/
|
||||
deleteGuildById: {
|
||||
postGuildById: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description ID of guild config to delete */
|
||||
/** @description ID of guild's config to create */
|
||||
guildId: string;
|
||||
};
|
||||
};
|
||||
|
@ -153,22 +229,68 @@ export interface operations {
|
|||
};
|
||||
/** @description Invalid ID supplied */
|
||||
400: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
/** @description Guild not found */
|
||||
404: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": components["schemas"]["error"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Find guild by ID for it's tp_messages
|
||||
* @description Returns tp_messages for a guild
|
||||
* Deletes a guild's config by ID
|
||||
* @description Delete a guild's config when the bot is removed from the guild.
|
||||
*/
|
||||
getTp_messagesOfGuildById: {
|
||||
deleteGuildById: {
|
||||
parameters: {
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
@ -176,21 +298,164 @@ export interface operations {
|
|||
/** @description successful operation */
|
||||
200: {
|
||||
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: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Invalid ID supplied */
|
||||
400: {
|
||||
content: never;
|
||||
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 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;
|
||||
};
|
||||
/** @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