Fix: Using bigint and added backend testing

This commit is contained in:
Aron Malcher 2024-03-10 17:12:50 +01:00
parent ed6195e1e2
commit 89507f8412
Signed by: aronmal
GPG key ID: 816B7707426FC612
20 changed files with 830 additions and 292 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
src/drizzle/migrations src/drizzle/migrations
log
dist dist
.vinxi .vinxi

View file

@ -1,30 +0,0 @@
GET https://discord.com/api/users/@me
Authorization: Bearer {{$dotenv DISCORD_ACCESS_TOKEN}}
###
GET https://discord.com/api/users/@me
Authorization: Bot {{$dotenv DISCORD_BOT_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

View file

@ -1,14 +1,18 @@
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { expect, test, type BrowserContext, type Page } from "@playwright/test"; 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 { Lucia, type Cookie } from "lucia";
import createClient from "openapi-fetch"; import createClient from "openapi-fetch";
import * as schema from "~/drizzle/schema";
import { paths } from "~/types/discord";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import "dotenv/config";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; 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 queryClient = postgres(process.env.DATABASE_URL!);
const db = drizzle(queryClient, { const db = drizzle(queryClient, {
@ -25,7 +29,22 @@ let page: Page;
let sessionCookie: Cookie | undefined; let sessionCookie: Cookie | undefined;
let userId = createId();
let guildId: bigint;
test.describe.serial("User auth process", () => { 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 }) => { test.beforeAll(async ({ browser }) => {
context = await browser.newContext(); context = await browser.newContext();
page = await context.newPage(); page = await context.newPage();
@ -59,6 +78,14 @@ test.describe.serial("User auth process", () => {
]); ]);
}); });
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 () => { test.afterAll(async () => {
await context.close(); await context.close();
}); });
@ -75,8 +102,8 @@ test.describe.serial("User auth process", () => {
await page.waitForURL("/"); await page.waitForURL("/");
}); });
test("Generate auth session for further tests", async () => { test("Generate auth session for further tests", async ({ browser }) => {
const { GET } = createClient<paths>({ const { GET } = createClient<discord.paths>({
baseUrl: "https://discord.com/api/v10", baseUrl: "https://discord.com/api/v10",
}); });
const discordUserResponse = await GET("/users/@me", { const discordUserResponse = await GET("/users/@me", {
@ -86,7 +113,22 @@ test.describe.serial("User auth process", () => {
}); });
if (discordUserResponse.error) throw discordUserResponse.error; if (discordUserResponse.error) throw discordUserResponse.error;
const discordUser = discordUserResponse.data; const discordUser = discordUserResponse.data;
const userId = createId();
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({ await db.insert(schema.users).values({
id: userId, id: userId,
discord_id: discordUser.id, discord_id: discordUser.id,
@ -119,4 +161,110 @@ test.describe.serial("User auth process", () => {
"landing_page_logged_in.png", "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,
);
}
});
}); });

View file

@ -28,6 +28,7 @@
"@solidjs/router": "^0.12.4", "@solidjs/router": "^0.12.4",
"@solidjs/start": "^0.6.0", "@solidjs/start": "^0.6.0",
"arctic": "^1.2.1", "arctic": "^1.2.1",
"colors": "^1.4.0",
"drizzle-orm": "^0.29.4", "drizzle-orm": "^0.29.4",
"http-status": "^1.7.4", "http-status": "^1.7.4",
"json-stable-stringify": "^1.1.1", "json-stable-stringify": "^1.1.1",

View file

@ -44,6 +44,9 @@ dependencies:
arctic: arctic:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
colors:
specifier: ^1.4.0
version: 1.4.0
drizzle-orm: drizzle-orm:
specifier: ^0.29.4 specifier: ^0.29.4
version: 0.29.4(pg@8.11.3)(postgres@3.4.3) version: 0.29.4(pg@8.11.3)(postgres@3.4.3)
@ -2761,6 +2764,11 @@ packages:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
dev: false dev: false
/colors@1.4.0:
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
engines: {node: '>=0.1.90'}
dev: false
/commander@2.20.3: /commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: false dev: false
@ -3370,6 +3378,7 @@ packages:
/eslint-config-prettier@9.1.0(eslint@8.57.0): /eslint-config-prettier@9.1.0(eslint@8.57.0):
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
hasBin: true
peerDependencies: peerDependencies:
eslint: '>=7.0.0' eslint: '>=7.0.0'
dependencies: dependencies:
@ -4671,6 +4680,7 @@ packages:
/nitropack@2.8.1: /nitropack@2.8.1:
resolution: {integrity: sha512-pODv2kEEzZSDQR+1UMXbGyNgMedUDq/qUomtiAnQKQvLy52VGlecXO1xDfH3i0kP1yKEcKTnWsx1TAF5gHM7xQ==} resolution: {integrity: sha512-pODv2kEEzZSDQR+1UMXbGyNgMedUDq/qUomtiAnQKQvLy52VGlecXO1xDfH3i0kP1yKEcKTnWsx1TAF5gHM7xQ==}
engines: {node: ^16.11.0 || >=17.0.0} engines: {node: ^16.11.0 || >=17.0.0}
hasBin: true
peerDependencies: peerDependencies:
xml2js: ^0.6.2 xml2js: ^0.6.2
peerDependenciesMeta: peerDependenciesMeta:
@ -5344,6 +5354,7 @@ packages:
/rollup-plugin-visualizer@5.12.0(rollup@4.12.0): /rollup-plugin-visualizer@5.12.0(rollup@4.12.0):
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true
peerDependencies: peerDependencies:
rollup: 2.x || 3.x || 4.x rollup: 2.x || 3.x || 4.x
peerDependenciesMeta: peerDependenciesMeta:
@ -6048,6 +6059,7 @@ packages:
/update-browserslist-db@1.0.13(browserslist@4.23.0): /update-browserslist-db@1.0.13(browserslist@4.23.0):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
dependencies: dependencies:
@ -6265,6 +6277,7 @@ packages:
/vite@5.1.1(@types/node@20.11.22)(sass@1.71.1): /vite@5.1.1(@types/node@20.11.22)(sass@1.71.1):
resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==} resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies: peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0 '@types/node': ^18.0.0 || >=20.0.0
less: '*' less: '*'

View file

@ -31,13 +31,34 @@
} }
}, },
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -77,13 +98,92 @@
} }
}, },
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}
},
"security": [
{
"basicAuth": []
}
]
},
"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's config to create",
"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": [ "security": [
@ -101,7 +201,7 @@
{ {
"name": "guildId", "name": "guildId",
"in": "path", "in": "path",
"description": "ID of guild config to delete", "description": "ID of guild's config to delete",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
@ -114,13 +214,34 @@
"description": "successful operation" "description": "successful operation"
}, },
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -130,17 +251,17 @@
] ]
} }
}, },
"/api/{guildId}/tp_messages": { "/api/{guildId}/timePlanning": {
"get": { "get": {
"tags": ["Time planning messages"], "tags": ["Time planning messages"],
"summary": "Find the tp_messages of guild by ID", "summary": "Find the timePlanning of guild by ID",
"description": "Returns tp_messages for a guild", "description": "Returns timePlanning for a guild",
"operationId": "getTp_messagesOfGuildById", "operationId": "gettimePlanningOfGuildById",
"parameters": [ "parameters": [
{ {
"name": "guildId", "name": "guildId",
"in": "path", "in": "path",
"description": "ID of guild's tp_messages to return", "description": "ID of guild's timePlanning to return",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
@ -154,22 +275,40 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/tp_messages" "$ref": "#/components/schemas/timePlanning"
} }
} }
} }
}, },
"204": {
"description": "Time planning not enabled for this guild"
},
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -180,14 +319,14 @@
}, },
"put": { "put": {
"tags": ["Time planning messages"], "tags": ["Time planning messages"],
"summary": "Put new message IDs for tp_messages of guild by ID", "summary": "Put new message IDs for timePlanning of guild by ID",
"description": "Returns tp_messages for a guild", "description": "Returns timePlanning for a guild",
"operationId": "putTp_messagesOfGuildById", "operationId": "puttimePlanningOfGuildById",
"parameters": [ "parameters": [
{ {
"name": "guildId", "name": "guildId",
"in": "path", "in": "path",
"description": "ID of guild's tp_messages to return", "description": "ID of guild's timePlanning to return",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
@ -196,11 +335,11 @@
} }
], ],
"requestBody": { "requestBody": {
"description": "Put new message IDs for tp_messages in channel", "description": "Put new message IDs for timePlanning in channel",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/tp_messages" "$ref": "#/components/schemas/timePlanning"
} }
} }
}, },
@ -211,16 +350,34 @@
"description": "successful operation" "description": "successful operation"
}, },
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
}, "content": {
"403": { "application/json": {
"description": "Time planning not enabled for this guild" "schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -234,13 +391,13 @@
"get": { "get": {
"tags": ["Matches"], "tags": ["Matches"],
"summary": "Find all matches of guild by ID", "summary": "Find all matches of guild by ID",
"description": "Returns tp_messages for a guild", "description": "Returns timePlanning for a guild",
"operationId": "getMatchesOfGuildById", "operationId": "getMatchesOfGuildById",
"parameters": [ "parameters": [
{ {
"name": "guildId", "name": "guildId",
"in": "path", "in": "path",
"description": "ID of guild's tp_messages to return", "description": "ID of guild's timePlanning to return",
"required": true, "required": true,
"schema": { "schema": {
"type": "string", "type": "string",
@ -273,17 +430,35 @@
} }
} }
}, },
"204": {
"description": "Time planning not enabled for this guild"
},
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -295,7 +470,7 @@
"post": { "post": {
"tags": ["Matches"], "tags": ["Matches"],
"summary": "Save a new created match of guild by ID", "summary": "Save a new created match of guild by ID",
"description": "Returns tp_messages for a guild", "description": "Returns timePlanning for a guild",
"operationId": "postMatchOfGuildById", "operationId": "postMatchOfGuildById",
"parameters": [ "parameters": [
{ {
@ -337,13 +512,34 @@
"description": "successful operation" "description": "successful operation"
}, },
"400": { "400": {
"description": "Invalid ID supplied" "description": "Invalid ID supplied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"401": { "401": {
"description": "Unauthorized" "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
}, },
"404": { "404": {
"description": "Guild not found" "description": "Guild not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error"
}
}
}
} }
}, },
"security": [ "security": [
@ -361,8 +557,7 @@
"required": ["guildId", "timezone", "features", "matches", "checksum"], "required": ["guildId", "timezone", "features", "matches", "checksum"],
"properties": { "properties": {
"guildId": { "guildId": {
"type": "number", "$ref": "#/components/schemas/id"
"example": 1234567890123456789
}, },
"timezone": { "timezone": {
"type": "string", "type": "string",
@ -388,9 +583,7 @@
"type": "boolean" "type": "boolean"
}, },
"channelId": { "channelId": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"targetMinute": { "targetMinute": {
"type": "number", "type": "number",
@ -416,14 +609,10 @@
"type": "boolean" "type": "boolean"
}, },
"isAvailableRoleId": { "isAvailableRoleId": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"wantsToBeNotifieRoledId": { "wantsToBeNotifieRoledId": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
} }
} }
} }
@ -455,20 +644,16 @@
], ],
"properties": { "properties": {
"channelId": { "channelId": {
"type": "number", "$ref": "#/components/schemas/id"
"example": 1234567890123456789
}, },
"createrId": { "createrId": {
"type": "number", "$ref": "#/components/schemas/id"
"example": 1234567890123456789
}, },
"roleId": { "roleId": {
"type": "number", "$ref": "#/components/schemas/id"
"example": 1234567890123456789
}, },
"messageId": { "messageId": {
"type": "number", "$ref": "#/components/schemas/id"
"example": 1234567890123456789
}, },
"matchType": { "matchType": {
"type": "string", "type": "string",
@ -486,56 +671,80 @@
} }
} }
}, },
"tp_messages": { "timePlanning": {
"type": "object", "type": "object",
"required": ["channelId", "messageIds"], "required": [
"enabled",
"channelId",
"rolesEnabled",
"isAvailableRoleId",
"wantsToBeNotifieRoledId",
"messageIds"
],
"properties": { "properties": {
"enabled": {
"type": "boolean"
},
"channelId": { "channelId": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789 },
"rolesEnabled": {
"type": "boolean"
},
"isAvailableRoleId": {
"$ref": "#/components/schemas/idOrNull"
},
"wantsToBeNotifieRoledId": {
"$ref": "#/components/schemas/idOrNull"
}, },
"messageIds": { "messageIds": {
"type": "object", "type": "object",
"required": ["0", "1", "2", "3", "4", "5", "6"], "required": ["0", "1", "2", "3", "4", "5", "6"],
"properties": { "properties": {
"0": { "0": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"1": { "1": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"2": { "2": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"3": { "3": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"4": { "4": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"5": { "5": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
}, },
"6": { "6": {
"type": "number", "$ref": "#/components/schemas/idOrNull"
"example": 1234567890123456789,
"nullable": true
} }
} }
} }
} }
},
"id": {
"type": "string",
"pattern": "^\\d{7,20}$",
"example": "1234567890123456789"
},
"idOrNull": {
"type": "string",
"pattern": "^\\d{7,20}$",
"example": "1234567890123456789",
"nullable": true
},
"error": {
"type": "object",
"required": "error",
"properties": {
"error": {
"type": "string"
}
}
} }
}, },
"securitySchemes": { "securitySchemes": {

View file

@ -1,7 +1,7 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint,
boolean, boolean,
integer,
pgTable, pgTable,
primaryKey, primaryKey,
serial, serial,
@ -39,14 +39,16 @@ export const discordTokens = pgTable("tokens", {
}); });
export const guilds = pgTable("guilds", { export const guilds = pgTable("guilds", {
id: integer("id").primaryKey(), id: bigint("id", { mode: "bigint" }).primaryKey(),
timezone: text("timezone").notNull().default("Etc/UTC"), timezone: text("timezone").notNull().default("Etc/UTC"),
tpEnabled: boolean("tp_enabled").notNull().default(false), tpEnabled: boolean("tp_enabled").notNull().default(false),
tpChannelId: integer("tp_channel_id"), tpChannelId: bigint("tp_channel_id", { mode: "bigint" }),
tpInterval: smallint("target_interval").notNull(), tpInterval: smallint("target_interval").notNull().default(64),
tpRoles: boolean("tp_roles").notNull(), tpRolesEnabled: boolean("tp_roles_enabled").notNull().default(false),
isAvailableRoleId: integer("is_available_role_id"), isAvailableRoleId: bigint("is_available_role_id", { mode: "bigint" }),
wantsToBeNotifieRoledId: integer("wants_to_be_notified_role_id"), wantsToBeNotifieRoledId: bigint("wants_to_be_notified_role_id", {
mode: "bigint",
}),
}); });
export const guildsRelations = relations(guilds, ({ many }) => ({ export const guildsRelations = relations(guilds, ({ many }) => ({
@ -57,9 +59,9 @@ export const guildsRelations = relations(guilds, ({ many }) => ({
export const tpMessages = pgTable( export const tpMessages = pgTable(
"tp_messages", "tp_messages",
{ {
messageId: integer("message_id"), messageId: bigint("message_id", { mode: "bigint" }),
day: smallint("day").notNull(), day: smallint("day").notNull(),
guildId: integer("guild_id") guildId: bigint("guild_id", { mode: "bigint" })
.notNull() .notNull()
.references(() => guilds.id, { onDelete: "cascade" }), .references(() => guilds.id, { onDelete: "cascade" }),
}, },
@ -79,14 +81,14 @@ export const tpMessagesRelations = relations(tpMessages, ({ one }) => ({
export const matches = pgTable("matches", { export const matches = pgTable("matches", {
id: serial("id").primaryKey(), id: serial("id").primaryKey(),
channelId: integer("channel_id").notNull(), channelId: bigint("channel_id", { mode: "bigint" }).notNull(),
matchType: varchar("match_type", { length: 50 }).notNull(), matchType: varchar("match_type", { length: 50 }).notNull(),
createrId: integer("creater_id").notNull(), createrId: bigint("creater_id", { mode: "bigint" }).notNull(),
roleId: integer("role_id").notNull(), roleId: bigint("role_id", { mode: "bigint" }).notNull(),
opponentName: varchar("opponent_name", { length: 100 }).notNull(), opponentName: varchar("opponent_name", { length: 100 }).notNull(),
messageId: integer("message_id").notNull(), messageId: bigint("message_id", { mode: "bigint" }).notNull(),
utc_ts: timestamp("utc_ts").notNull(), utc_ts: timestamp("utc_ts").notNull(),
guildId: integer("guild_id") guildId: bigint("guild_id", { mode: "bigint" })
.notNull() .notNull()
.references(() => guilds.id, { onDelete: "cascade" }), .references(() => guilds.id, { onDelete: "cascade" }),
}); });

View file

@ -2,14 +2,27 @@ import stringify from "json-stable-stringify";
import objectHash from "object-hash"; import objectHash from "object-hash";
import { guilds, matches, tpMessages } from "~/drizzle/schema"; import { guilds, matches, tpMessages } from "~/drizzle/schema";
import { ExtractDataTypes, GetColumns } from "~/types/db"; import { ExtractDataTypes, GetColumns } from "~/types/db";
import { components } from "~/types/liljudd";
export const buildMatches = ( export const buildMatches = (
queryMatches: ExtractDataTypes<GetColumns<typeof matches>>[], queryMatches: ExtractDataTypes<GetColumns<typeof matches>>[],
) => ): components["schemas"]["match"][] =>
queryMatches.map( queryMatches.map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars ({
({ id, guildId, utc_ts, ...match }) => ({ channelId,
...match, 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(), utc_ts: utc_ts.toISOString(),
}), }),
); );
@ -19,14 +32,14 @@ export function buildConfig(
tpMessages: ExtractDataTypes<GetColumns<typeof tpMessages>>[]; tpMessages: ExtractDataTypes<GetColumns<typeof tpMessages>>[];
matches: ExtractDataTypes<GetColumns<typeof matches>>[]; matches: ExtractDataTypes<GetColumns<typeof matches>>[];
}, },
) { ): components["schemas"]["guildConfig"] {
const { const {
id, id,
timezone, timezone,
tpEnabled, tpEnabled,
tpChannelId, tpChannelId,
tpInterval, tpInterval,
tpRoles, tpRolesEnabled: tpRoles,
isAvailableRoleId, isAvailableRoleId,
wantsToBeNotifieRoledId, wantsToBeNotifieRoledId,
} = guildQuery; } = guildQuery;
@ -36,20 +49,26 @@ export function buildConfig(
const targetDay = (tpInterval >> 11) & 7; const targetDay = (tpInterval >> 11) & 7;
const payload = { const payload = {
guildId: id, guildId: id.toString(),
timezone, timezone,
features: { features: {
timePlanning: { timePlanning: {
enabled: tpEnabled, enabled: tpEnabled,
channelId: tpChannelId, channelId: tpChannelId?.toString() ?? null,
targetMinute, targetMinute,
targetHour, targetHour,
targetDay, targetDay,
roles: { enabled: tpRoles, isAvailableRoleId, wantsToBeNotifieRoledId }, roles: {
enabled: tpRoles,
isAvailableRoleId: isAvailableRoleId?.toString() ?? null,
wantsToBeNotifieRoledId: wantsToBeNotifieRoledId?.toString() ?? null,
},
}, },
}, },
matches: buildMatches(guildQuery.matches), matches: buildMatches(guildQuery.matches),
checksum: objectHash(stringify(guildQuery)),
}; };
return payload;
// generate checksum from payload because
// from guildQuery results in bigint serialization error
return { ...payload, checksum: objectHash(stringify(payload)) };
} }

View file

@ -12,6 +12,7 @@ export function ErrorResponse<
M extends Methods<P>, M extends Methods<P>,
C extends StatusCodes<P, M> = StatusCodes<P, M>, C extends StatusCodes<P, M> = StatusCodes<P, M>,
>(code: C, error?: string): APIResponse<P, M> { >(code: C, error?: string): APIResponse<P, M> {
console.log(code, error);
const responseData = { const responseData = {
error: error ?? httpStatus[`${httpStatus[code]}_NAME`], error: error ?? httpStatus[`${httpStatus[code]}_NAME`],
}; };

View file

@ -1,13 +1,17 @@
import moment from "moment-timezone"; import moment from "moment-timezone";
import { z } from "zod"; import { z } from "zod";
export const zodId = z const zodId = z
.string() .string()
.refine((value) => /^\d{7,20}$/.test(value), "Invalid ID supplied") .refine((value) => /^\d{7,20}$/.test(value), "Invalid ID supplied");
.transform((value) => parseInt(value)); export const zodBigIntId = zodId.transform((value) => BigInt(value));
export const zodTpMessages = z.object({ export const zodTpMessages = z.object({
channelId: zodId, enabled: z.boolean(),
channelId: zodId.nullable(),
rolesEnabled: z.boolean(),
isAvailableRoleId: zodId.nullable(),
wantsToBeNotifieRoledId: zodId.nullable(),
messageIds: z.object({ messageIds: z.object({
"0": zodId.nullable(), "0": zodId.nullable(),
"1": zodId.nullable(), "1": zodId.nullable(),

View file

@ -1,8 +1,14 @@
import { createMiddleware } from "@solidjs/start/middleware"; import { createMiddleware } from "@solidjs/start/middleware";
import { Session, User, verifyRequestOrigin } from "lucia"; import colors from "colors";
import fs from "fs";
import { verifyRequestOrigin } from "lucia";
import { appendHeader, getCookie, getHeader } from "vinxi/http"; import { appendHeader, getCookie, getHeader } from "vinxi/http";
import { lucia } from "./lib/auth"; import { lucia } from "./lib/auth";
colors.enable();
let started: boolean = false;
export default createMiddleware({ export default createMiddleware({
onRequest: async (event) => { onRequest: async (event) => {
if (event.nativeEvent.node.req.method !== "GET") { if (event.nativeEvent.node.req.method !== "GET") {
@ -43,12 +49,52 @@ export default createMiddleware({
event.nativeEvent.context.session = session; event.nativeEvent.context.session = session;
event.nativeEvent.context.user = user; event.nativeEvent.context.user = user;
}, },
}); onBeforeResponse: async (event, response) => {
let consoleLog = "",
fileLog = "";
declare module "h3" { if (!started) {
// eslint-disable-next-line no-unused-vars try {
interface H3EventContext { await fs.promises.mkdir("log");
user: User | null; console.log("Created 'log' Folder.");
session: Session | null; } 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);
},
});

View file

@ -5,7 +5,7 @@ import { guilds } from "~/drizzle/schema";
import { BasicAuth } from "~/lib/auth"; import { BasicAuth } from "~/lib/auth";
import { buildConfig } from "~/lib/responseBuilders"; import { buildConfig } from "~/lib/responseBuilders";
import { ErrorResponse, Res } from "~/lib/responses"; import { ErrorResponse, Res } from "~/lib/responses";
import { zodId } from "~/lib/zod"; import { zodBigIntId } from "~/lib/zod";
import { APIResponse } from "~/types/backend"; import { APIResponse } from "~/types/backend";
type Path = "/api/{guildId}/config"; type Path = "/api/{guildId}/config";
@ -22,9 +22,9 @@ export const GET = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
@ -41,6 +41,32 @@ export const GET = async (
return Res("OK", buildConfig(guildQuery)); 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 ( export const DELETE = async (
event: APIEvent, event: APIEvent,
): Promise<APIResponse<Path, "delete">> => { ): Promise<APIResponse<Path, "delete">> => {
@ -53,9 +79,9 @@ export const DELETE = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }

View file

@ -5,7 +5,7 @@ import { guilds, matches } from "~/drizzle/schema";
import { BasicAuth } from "~/lib/auth"; import { BasicAuth } from "~/lib/auth";
import { buildMatches } from "~/lib/responseBuilders"; import { buildMatches } from "~/lib/responseBuilders";
import { ErrorResponse, Res } from "~/lib/responses"; import { ErrorResponse, Res } from "~/lib/responses";
import { zodId, zodMatch } from "~/lib/zod"; import { zodBigIntId, zodMatch } from "~/lib/zod";
import { APIResponse, RequestBody } from "~/types/backend"; import { APIResponse, RequestBody } from "~/types/backend";
type Path = "/api/{guildId}/matches"; type Path = "/api/{guildId}/matches";
@ -22,9 +22,9 @@ export const GET = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
@ -60,9 +60,9 @@ export const POST = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
@ -94,8 +94,13 @@ export const POST = async (
); );
await db.insert(matches).values({ await db.insert(matches).values({
...body.match,
guildId: guild.id, 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), utc_ts: new Date(body.match.utc_ts),
}); });

View file

@ -4,14 +4,14 @@ import db from "~/drizzle";
import { guilds, tpMessages } from "~/drizzle/schema"; import { guilds, tpMessages } from "~/drizzle/schema";
import { BasicAuth } from "~/lib/auth"; import { BasicAuth } from "~/lib/auth";
import { ErrorResponse, Res } from "~/lib/responses"; import { ErrorResponse, Res } from "~/lib/responses";
import { zodId, zodTpMessages } from "~/lib/zod"; import { zodBigIntId, zodTpMessages } from "~/lib/zod";
import { APIResponse, RequestBody } from "~/types/backend"; import { APIResponse, RequestBody } from "~/types/backend";
type Path = "/api/{guildId}/tp_messages"; type Path = "/api/{guildId}/timePlanning";
const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const; const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
type DayKeys = (typeof DayKeys)[number]; type DayKeys = (typeof DayKeys)[number];
type Messages = Record<DayKeys, number | null>; type Messages = Record<DayKeys, string | null>;
export const GET = async ( export const GET = async (
event: APIEvent, event: APIEvent,
@ -25,9 +25,9 @@ export const GET = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
@ -41,13 +41,11 @@ export const GET = async (
if (!guild) return ErrorResponse("NOT_FOUND"); if (!guild) return ErrorResponse("NOT_FOUND");
if (!guild.tpEnabled || !guild.tpChannelId) return Res("NO_CONTENT", null);
const tpMessages = guild.tpMessages.reduce( const tpMessages = guild.tpMessages.reduce(
(acc, message) => { (acc, message) => {
const day = message.day.toString() as DayKeys; const day = message.day.toString() as DayKeys;
if (!/^[0-6]$/.test(day)) return acc; if (!/^[0-6]$/.test(day)) return acc;
acc[day] = message.messageId; acc[day] = message.messageId?.toString() ?? null;
return acc; return acc;
}, },
{ {
@ -62,7 +60,11 @@ export const GET = async (
); );
return Res("OK", { return Res("OK", {
channelId: guild.tpChannelId, enabled: guild.tpEnabled,
channelId: guild.tpChannelId?.toString() ?? null,
rolesEnabled: guild.tpRolesEnabled,
isAvailableRoleId: guild.isAvailableRoleId?.toString() ?? null,
wantsToBeNotifieRoledId: guild.wantsToBeNotifieRoledId?.toString() ?? null,
messageIds: tpMessages, messageIds: tpMessages,
}); });
}; };
@ -79,9 +81,9 @@ export const PUT = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
@ -95,8 +97,6 @@ export const PUT = async (
if (!guild) return ErrorResponse("NOT_FOUND"); if (!guild) return ErrorResponse("NOT_FOUND");
if (!guild.tpEnabled) return ErrorResponse("FORBIDDEN");
const unparsedBody = await new Response(event.request.body).json(); const unparsedBody = await new Response(event.request.body).json();
let body: RequestBody<Path, "put">; let body: RequestBody<Path, "put">;
@ -106,19 +106,36 @@ export const PUT = async (
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }
if (guild.tpChannelId !== body.channelId) const {
enabled,
channelId,
rolesEnabled,
isAvailableRoleId,
wantsToBeNotifieRoledId,
messageIds,
} = body;
if (guild.tpChannelId !== channelId)
await db await db
.update(guilds) .update(guilds)
.set({ tpChannelId: body.channelId }) .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)) .where(eq(guilds.id, guild.id))
.execute(); .execute();
await Promise.all( await Promise.all(
DayKeys.map(async (dayStr) => { DayKeys.map(async (dayStr) => {
const day = parseInt(dayStr); const day = parseInt(dayStr);
const messageId = messageIds[dayStr];
await db await db
.update(tpMessages) .update(tpMessages)
.set({ messageId: body.messageIds[dayStr] }) .set({ messageId: messageId ? BigInt(messageId) : null })
.where(and(eq(tpMessages.guildId, guild.id), eq(tpMessages.day, day))) .where(and(eq(tpMessages.guildId, guild.id), eq(tpMessages.day, day)))
.execute(); .execute();
}), }),

View file

@ -5,7 +5,7 @@ import { guilds } from "~/drizzle/schema";
import { BasicAuth } from "~/lib/auth"; import { BasicAuth } from "~/lib/auth";
import { buildConfig } from "~/lib/responseBuilders"; import { buildConfig } from "~/lib/responseBuilders";
import { ErrorResponse, Res } from "~/lib/responses"; import { ErrorResponse, Res } from "~/lib/responses";
import { zodId } from "~/lib/zod"; import { zodBigIntId } from "~/lib/zod";
import { APIResponse } from "~/types/backend"; import { APIResponse } from "~/types/backend";
type Path = "/api/boot"; type Path = "/api/boot";
@ -22,9 +22,9 @@ export const GET = async (
return ErrorResponse("UNAUTHORIZED"); return ErrorResponse("UNAUTHORIZED");
} }
let guildId: number; let guildId: bigint;
try { try {
guildId = zodId.parse(event.params.guildId); guildId = zodBigIntId.parse(event.params.guildId);
} catch (e) { } catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e)); return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
} }

4
src/types/db.d.ts vendored
View file

@ -6,6 +6,8 @@ export type GetColumns<T> =
export type ExtractDataTypes<T> = { export type ExtractDataTypes<T> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof T]: T[K] extends PgColumn<infer ColumnConfig, any, any> [K in keyof T]: T[K] extends PgColumn<infer ColumnConfig, any, any>
? ColumnConfig["data"] ? ColumnConfig["notNull"] extends true
? ColumnConfig["data"]
: ColumnConfig["data"] | null
: unknown; : unknown;
}; };

1
src/types/env.d.ts vendored
View file

@ -12,7 +12,6 @@ interface ImportMetaEnv {
readonly VITE_DATABASE_URL: string; readonly VITE_DATABASE_URL: string;
} }
// eslint-disable-next-line no-unused-vars
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }

244
src/types/liljudd.d.ts vendored
View file

@ -18,33 +18,38 @@ export interface paths {
* @description Returns a single guild's config. * @description Returns a single guild's config.
*/ */
get: operations["getGuildById"]; get: operations["getGuildById"];
/**
* 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 * Deletes a guild's config by ID
* @description Delete a guild's config when the bot is removed from the guild. * @description Delete a guild's config when the bot is removed from the guild.
*/ */
delete: operations["deleteGuildById"]; delete: operations["deleteGuildById"];
}; };
"/api/{guildId}/tp_messages": { "/api/{guildId}/timePlanning": {
/** /**
* Find the tp_messages of guild by ID * Find the timePlanning of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
get: operations["getTp_messagesOfGuildById"]; get: operations["gettimePlanningOfGuildById"];
/** /**
* Put new message IDs for tp_messages of guild by ID * Put new message IDs for timePlanning of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
put: operations["putTp_messagesOfGuildById"]; put: operations["puttimePlanningOfGuildById"];
}; };
"/api/{guildId}/matches": { "/api/{guildId}/matches": {
/** /**
* Find all matches of guild by ID * Find all matches of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
get: operations["getMatchesOfGuildById"]; get: operations["getMatchesOfGuildById"];
/** /**
* Save a new created match of guild by ID * Save a new created match of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
post: operations["postMatchOfGuildById"]; post: operations["postMatchOfGuildById"];
}; };
@ -55,8 +60,7 @@ export type webhooks = Record<string, never>;
export interface components { export interface components {
schemas: { schemas: {
guildConfig: { guildConfig: {
/** @example 1234567890123456800 */ guildId: components["schemas"]["id"];
guildId: number;
/** /**
* Format: text * Format: text
* @example Europe/Berlin * @example Europe/Berlin
@ -65,8 +69,7 @@ export interface components {
features: { features: {
timePlanning: { timePlanning: {
enabled: boolean; enabled: boolean;
/** @example 1234567890123456800 */ channelId: components["schemas"]["idOrNull"];
channelId: number | null;
/** @example 0 */ /** @example 0 */
targetMinute: number; targetMinute: number;
/** @example 1 */ /** @example 1 */
@ -75,10 +78,8 @@ export interface components {
targetDay: number; targetDay: number;
roles: { roles: {
enabled: boolean; enabled: boolean;
/** @example 1234567890123456800 */ isAvailableRoleId: components["schemas"]["idOrNull"];
isAvailableRoleId: number | null; wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
/** @example 1234567890123456800 */
wantsToBeNotifieRoledId: number | null;
}; };
}; };
}; };
@ -86,14 +87,10 @@ export interface components {
checksum: string; checksum: string;
}; };
match: { match: {
/** @example 1234567890123456800 */ channelId: components["schemas"]["id"];
channelId: number; createrId: components["schemas"]["id"];
/** @example 1234567890123456800 */ roleId: components["schemas"]["id"];
createrId: number; messageId: components["schemas"]["id"];
/** @example 1234567890123456800 */
roleId: number;
/** @example 1234567890123456800 */
messageId: number;
/** /**
* Format: varchar(50) * Format: varchar(50)
* @example Scrim * @example Scrim
@ -107,26 +104,29 @@ export interface components {
/** @example 2020-01-01T00:00:00Z */ /** @example 2020-01-01T00:00:00Z */
utc_ts: string; utc_ts: string;
}; };
tp_messages: { timePlanning: {
/** @example 1234567890123456800 */ enabled: boolean;
channelId: number; channelId: components["schemas"]["idOrNull"];
rolesEnabled: boolean;
isAvailableRoleId: components["schemas"]["idOrNull"];
wantsToBeNotifieRoledId: components["schemas"]["idOrNull"];
messageIds: { messageIds: {
/** @example 1234567890123456800 */ 0: components["schemas"]["idOrNull"];
0: number | null; 1: components["schemas"]["idOrNull"];
/** @example 1234567890123456800 */ 2: components["schemas"]["idOrNull"];
1: number | null; 3: components["schemas"]["idOrNull"];
/** @example 1234567890123456800 */ 4: components["schemas"]["idOrNull"];
2: number | null; 5: components["schemas"]["idOrNull"];
/** @example 1234567890123456800 */ 6: components["schemas"]["idOrNull"];
3: number | null;
/** @example 1234567890123456800 */
4: number | null;
/** @example 1234567890123456800 */
5: number | null;
/** @example 1234567890123456800 */
6: number | null;
}; };
}; };
/** @example 1234567890123456789 */
id: string;
/** @example 1234567890123456789 */
idOrNull: string | null;
error: {
error?: string;
};
}; };
responses: never; responses: never;
parameters: never; parameters: never;
@ -155,15 +155,21 @@ export interface operations {
}; };
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };
@ -187,16 +193,58 @@ export interface operations {
}; };
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: {
"application/json": components["schemas"]["error"];
};
};
};
};
/**
* Creates a guild's config by ID
* @description Create a guild's config when the bot is has joined a new guild.
*/
postGuildById: {
parameters: {
path: {
/** @description ID of guild's config to create */
guildId: string;
};
};
responses: {
/** @description successful operation */
204: {
content: never; content: never;
}; };
/** @description Invalid ID supplied */
400: {
content: {
"application/json": components["schemas"]["error"];
};
};
/** @description Unauthorized */
401: {
content: {
"application/json": components["schemas"]["error"];
};
};
/** @description Guild not found */
404: {
content: {
"application/json": components["schemas"]["error"];
};
};
}; };
}; };
/** /**
@ -206,7 +254,7 @@ export interface operations {
deleteGuildById: { deleteGuildById: {
parameters: { parameters: {
path: { path: {
/** @description ID of guild config to delete */ /** @description ID of guild's config to delete */
guildId: string; guildId: string;
}; };
}; };
@ -217,26 +265,32 @@ export interface operations {
}; };
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };
/** /**
* Find the tp_messages of guild by ID * Find the timePlanning of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
getTp_messagesOfGuildById: { gettimePlanningOfGuildById: {
parameters: { parameters: {
path: { path: {
/** @description ID of guild's tp_messages to return */ /** @description ID of guild's timePlanning to return */
guildId: string; guildId: string;
}; };
}; };
@ -244,42 +298,44 @@ export interface operations {
/** @description successful operation */ /** @description successful operation */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["tp_messages"]; "application/json": components["schemas"]["timePlanning"];
}; };
}; };
/** @description Time planning not enabled for this guild */
204: {
content: never;
};
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };
/** /**
* Put new message IDs for tp_messages of guild by ID * Put new message IDs for timePlanning of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
putTp_messagesOfGuildById: { puttimePlanningOfGuildById: {
parameters: { parameters: {
path: { path: {
/** @description ID of guild's tp_messages to return */ /** @description ID of guild's timePlanning to return */
guildId: string; guildId: string;
}; };
}; };
/** @description Put new message IDs for tp_messages in channel */ /** @description Put new message IDs for timePlanning in channel */
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["tp_messages"]; "application/json": components["schemas"]["timePlanning"];
}; };
}; };
responses: { responses: {
@ -289,30 +345,32 @@ export interface operations {
}; };
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
}; "application/json": components["schemas"]["error"];
/** @description Time planning not enabled for this guild */ };
403: {
content: never;
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };
/** /**
* Find all matches of guild by ID * Find all matches of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
getMatchesOfGuildById: { getMatchesOfGuildById: {
parameters: { parameters: {
path: { path: {
/** @description ID of guild's tp_messages to return */ /** @description ID of guild's timePlanning to return */
guildId: string; guildId: string;
}; };
}; };
@ -330,27 +388,29 @@ export interface operations {
}; };
}; };
}; };
/** @description Time planning not enabled for this guild */
204: {
content: never;
};
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };
/** /**
* Save a new created match of guild by ID * Save a new created match of guild by ID
* @description Returns tp_messages for a guild * @description Returns timePlanning for a guild
*/ */
postMatchOfGuildById: { postMatchOfGuildById: {
parameters: { parameters: {
@ -380,15 +440,21 @@ export interface operations {
}; };
/** @description Invalid ID supplied */ /** @description Invalid ID supplied */
400: { 400: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Unauthorized */ /** @description Unauthorized */
401: { 401: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
/** @description Guild not found */ /** @description Guild not found */
404: { 404: {
content: never; content: {
"application/json": components["schemas"]["error"];
};
}; };
}; };
}; };

View file

@ -3,7 +3,6 @@ import { lucia } from "~/lib/auth";
import { ExtractDataTypes, GetColumns } from "./db"; import { ExtractDataTypes, GetColumns } from "./db";
declare module "lucia" { declare module "lucia" {
// eslint-disable-next-line no-unused-vars
interface Register { interface Register {
Lucia: typeof lucia; Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes; DatabaseUserAttributes: DatabaseUserAttributes;
@ -11,6 +10,4 @@ declare module "lucia" {
} }
interface DatabaseUserAttributes interface DatabaseUserAttributes
extends ExtractDataTypes<GetColumns<typeof users>> { extends ExtractDataTypes<GetColumns<typeof users>> {}
warst: string;
}

12
src/types/vinxi.d.ts vendored Normal file
View 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;
}
}