Fix: Finished Backend

This commit is contained in:
Aron Malcher 2024-02-26 21:46:33 +01:00
parent 6b388729d9
commit ffaf8d989e
Signed by: aronmal
GPG key ID: 816B7707426FC612
30 changed files with 1478 additions and 873 deletions

View file

@ -4,5 +4,8 @@
"node": true
},
"plugins": ["solid"],
"extends": ["eslint:recommended", "plugin:solid/typescript"]
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:solid/typescript"
]
}

View file

@ -23,31 +23,36 @@
"@lucia-auth/adapter-drizzle": "^1.0.2",
"@paralleldrive/cuid2": "^2.2.2",
"@solidjs/meta": "^0.29.3",
"@solidjs/router": "^0.12.3",
"@solidjs/start": "^0.5.9",
"arctic": "^1.1.6",
"drizzle-orm": "^0.29.3",
"@solidjs/router": "^0.12.4",
"@solidjs/start": "^0.5.10",
"arctic": "^1.2.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.9.1",
"object-hash": "^3.0.0",
"openapi-fetch": "^0.9.2",
"postgres": "^3.4.3",
"solid-js": "^1.8.15",
"vinxi": "^0.3.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.1",
"dotenv": "^16.4.4",
"@types/json-stable-stringify": "^1.0.36",
"@types/object-hash": "^3.0.6",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"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.10.1",
"h3": "^1.10.2",
"openapi-typescript": "^6.7.4",
"pg": "^8.11.3",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"sass": "^1.71.0",
"sass": "^1.71.1",
"typescript": "^5.3.3",
"zod": "3.22.4"
},

File diff suppressed because it is too large Load diff

View file

@ -10,19 +10,22 @@
"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"
}
}
}
}
@ -30,20 +33,23 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"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",
@ -73,18 +79,21 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"basicAuth": []
}
]
},
"delete": {
"tags": ["Guild configs"],
"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",
@ -107,13 +116,16 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"basicAuth": []
}
]
}
@ -153,19 +165,22 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"basicAuth": []
}
]
},
"put": {
"tags": ["Time planning messages"],
"summary": "Put message IDs for tp_messages of guild by ID",
"summary": "Put new message IDs for tp_messages of guild by ID",
"description": "Returns tp_messages for a guild",
"operationId": "putTp_messagesOfGuildById",
"parameters": [
@ -180,30 +195,37 @@
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tp_messages"
}
"requestBody": {
"description": "Put new message IDs for tp_messages in channel",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tp_messages"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "Time planning not enabled for this guild"
"description": "successful operation"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Time planning not enabled for this guild"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"basicAuth": []
}
]
}
@ -232,9 +254,20 @@
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/tp_messages"
"type": "object",
"required": ["matches", "timezone"],
"properties": {
"matches": {
"type": "array",
"items": {
"$ref": "#/components/schemas/match"
}
},
"timezone": {
"type": "string",
"format": "text",
"example": "Europe/Berlin"
}
}
}
}
@ -246,21 +279,22 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
"basicAuth": []
}
]
}
},
"/api/{guildId}/matches/{channelId}": {
},
"post": {
"tags": ["Matches"],
"summary": "Save a new created match in channel of guild by IDs",
"summary": "Save a new created match of guild by ID",
"description": "Returns tp_messages for a guild",
"operationId": "postMatchOfGuildById",
"parameters": [
@ -273,108 +307,48 @@
"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",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/tp_messages"
"requestBody": {
"description": "Save a new created match in channel",
"content": {
"application/json": {
"schema": {
"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"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Guild not found"
}
},
"security": [
{
"bot_token": []
}
]
}
},
"/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": [
{
"bot_token": []
"basicAuth": []
}
]
}
@ -382,21 +356,11 @@
},
"components": {
"schemas": {
"bootConfig": {
"type": "object",
"properties": {
"guilds": {
"type": "array",
"items": {
"$ref": "#/components/schemas/guildConfig"
}
}
}
},
"guildConfig": {
"type": "object",
"required": ["guildId", "timezone", "features", "matches", "checksum"],
"properties": {
"guildID": {
"guildId": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789"
@ -408,14 +372,27 @@
},
"features": {
"type": "object",
"required": ["timePlanning"],
"properties": {
"time_planning": {
"timePlanning": {
"type": "object",
"required": [
"enabled",
"channelId",
"targetMinute",
"targetHour",
"targetDay",
"roles"
],
"properties": {
"channelID": {
"enabled": {
"type": "boolean"
},
"channelId": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789"
"example": "1234567890123456789",
"nullable": true
},
"targetMinute": {
"type": "number",
@ -431,6 +408,11 @@
},
"roles": {
"type": "object",
"required": [
"enabled",
"isAvailableRoleId",
"wantsToBeNotifieRoledId"
],
"properties": {
"enabled": {
"type": "boolean"
@ -458,13 +440,25 @@
"items": {
"$ref": "#/components/schemas/match"
}
},
"checksum": {
"type": "string"
}
}
},
"match": {
"type": "object",
"required": [
"channelId",
"matchType",
"createrId",
"roleId",
"opponentName",
"messageId",
"utc_ts"
],
"properties": {
"channelID": {
"channelId": {
"type": "string",
"format": "varcharq(20)",
"example": "1234567890123456789"
@ -489,54 +483,81 @@
"format": "varchar(100)",
"example": "?"
},
"messsageId": {
"messageId": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789"
},
"utc_ts": {
"type": "string",
"example": "1706180188"
"example": "2020-01-01T00:00:00Z"
}
}
},
"tp_messages": {
"type": "object",
"required": ["channelId", "messageIds"],
"properties": {
"guildId": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789"
},
"channelId": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789"
},
"messageIds": {
"type": "array",
"items": {
"type": "string",
"format": "varchar(20)"
},
"example": [
"1234567890123456789",
"1234567890123456789",
"1234567890123456789",
"1234567890123456789",
"1234567890123456789",
"1234567890123456789",
"1234567890123456789"
]
"type": "object",
"required": ["0", "1", "2", "3", "4", "5", "6"],
"properties": {
"0": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"1": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"2": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"3": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"4": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"5": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
},
"6": {
"type": "string",
"format": "varchar(20)",
"example": "1234567890123456789",
"nullable": true
}
}
}
}
}
},
"securitySchemes": {
"bot_token": {
"basicAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
"scheme": "basic"
}
}
}

View file

@ -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"
}

View file

@ -1,5 +1,5 @@
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";
@ -41,9 +41,7 @@ function NavBar() {
>
<FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
</Li>
<Suspense>
<NavUser />
</Suspense>
<NavUser />
</ul>
</nav>
);

View file

@ -3,48 +3,27 @@ import {
faArrowRightToBracket,
faGear,
} from "@fortawesome/pro-regular-svg-icons";
import { User } from "lucia";
import { Show, createResource } from "solid-js";
import { cache, createAsync } from "@solidjs/router";
import { Show } from "solid-js";
import { getRequestEvent } from "solid-js/web";
import { FontAwesomeIcon } from "./FontAwesomeIcon";
import { Li } from "./NavBar";
async function getUser(): Promise<
| ({
success: false;
message: string;
// user?: undefined;
} & Partial<User>)
| ({
success: true;
message?: undefined;
} & User)
> {
async function getUser() {
"use server";
const event = getRequestEvent();
if (!event) return { success: false, message: "No request event!" };
const pathname = new URL(event.request.url).pathname;
const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" };
console.log("userInfo", pathname, "success");
return { success: true, ...user };
return event?.nativeEvent.context.user;
}
const cachedUser = cache(() => getUser(), "userInfo");
function NavUser() {
const [user] = createResource(async () => {
const user = await getUser();
if (!user.success) console.error("userInfo", user.message);
return user;
});
const user = createAsync(() => cachedUser());
const pfp = () => {
const thisUser = user();
if (!thisUser?.success) return "";
if (!thisUser?.id) return "";
return thisUser.image
? `https://cdn.discordapp.com/avatars/${thisUser.discord_id}/${thisUser.image}.png`

View file

@ -1,8 +1,8 @@
import { relations } from "drizzle-orm";
import {
boolean,
integer,
pgTable,
primaryKey,
serial,
smallint,
text,
@ -37,10 +37,51 @@ export const discordTokens = pgTable("tokens", {
expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
});
export const matchPlannings = pgTable("match_planning", {
export const guilds = pgTable("guilds", {
id: varchar("id", { length: 20 }).primaryKey(),
timezone: text("timezone").notNull().default("Etc/UTC"),
tpEnabled: boolean("tp_enabled").notNull().default(false),
tpChannelId: varchar("tp_channel_id", { length: 20 }),
tpInterval: smallint("target_interval").notNull(),
tpRoles: boolean("tp_roles").notNull(),
isAvailableRoleId: varchar("is_available_role_id", { length: 20 }),
wantsToBeNotifieRoledId: varchar("wants_to_be_notified_role_id", {
length: 20,
}),
});
export const guildsRelations = relations(guilds, ({ many }) => ({
tpMessages: many(tpMessages),
matches: many(matches),
}));
export const tpMessages = pgTable(
"tp_messages",
{
messageId: varchar("message_id", { length: 20 }),
day: smallint("day").notNull(),
guildId: varchar("guild_id", { length: 20 })
.notNull()
.references(() => guilds.id, { onDelete: "cascade" }),
},
(table) => {
return {
pk: primaryKey({ columns: [table.guildId, table.day] }),
};
},
);
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(),
matchType: varchar("match_type", { length: 50 }).notNull(),
createrId: varchar("creater_id", { length: 20 }).notNull(),
roleId: varchar("role_id", { length: 20 }).notNull(),
opponentName: varchar("opponent_name", { length: 100 }).notNull(),
@ -51,65 +92,9 @@ export const matchPlannings = pgTable("match_planning", {
.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],
}),
}));

View file

@ -1,6 +1,5 @@
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { Discord } from "arctic";
import { PgColumn, PgTableWithColumns } from "drizzle-orm/pg-core";
import { Lucia } from "lucia";
import db from "~/drizzle";
import { sessions, users } from "~/drizzle/schema";
@ -17,30 +16,16 @@ export const lucia = new Lucia(adapter, {
getUserAttributes: (attributes) => attributes,
});
declare module "lucia" {
// eslint-disable-next-line no-unused-vars
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
type GetColumns<T> =
T extends PgTableWithColumns<infer First> ? First["columns"] : never;
type ExtractDataTypes<T> = {
[K in keyof T]: T[K] extends PgColumn<infer DataType, any, any>
? DataType["data"]
: never;
};
interface DatabaseUserAttributes
extends ExtractDataTypes<GetColumns<typeof users>> {
warst: string;
}
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}`,
};

View file

@ -0,0 +1,83 @@
import stringify from "json-stable-stringify";
import objectHash from "object-hash";
export const buildMatches = (
matches: {
id: number;
messageId: string;
guildId: string;
channelId: string;
matchType: string;
createrId: string;
roleId: string;
opponentName: string;
utc_ts: Date;
}[],
) =>
matches.map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ id, guildId, utc_ts, ...match }) => ({
...match,
utc_ts: utc_ts.toISOString(),
}),
);
export function buildConfig(guildQuery: {
id: string;
timezone: string;
tpEnabled: boolean;
tpChannelId: string | null;
tpInterval: number;
tpRoles: boolean;
isAvailableRoleId: string | null;
wantsToBeNotifieRoledId: string | null;
tpMessages: {
messageId: string | null;
day: number;
guildId: string;
}[];
matches: {
id: number;
messageId: string;
guildId: string;
channelId: string;
matchType: string;
createrId: string;
roleId: string;
opponentName: string;
utc_ts: Date;
}[];
}) {
const {
id,
timezone,
tpEnabled,
tpChannelId,
tpInterval,
tpRoles,
isAvailableRoleId,
wantsToBeNotifieRoledId,
} = guildQuery;
const targetMinute = tpInterval & 63;
const targetHour = (tpInterval >> 6) & 31;
const targetDay = (tpInterval >> 11) & 7;
const payload = {
guildId: id,
timezone,
features: {
timePlanning: {
enabled: tpEnabled,
channelId: tpChannelId,
targetMinute,
targetHour,
targetDay,
roles: { enabled: tpRoles, isAvailableRoleId, wantsToBeNotifieRoledId },
},
},
matches: buildMatches(guildQuery.matches),
checksum: objectHash(stringify(guildQuery)),
};
return payload;
}

39
src/lib/responses.ts Normal file
View 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> {
const responseData = {
error: error ?? httpStatus[`${httpStatus[code]}_NAME`],
};
console.log(responseData);
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",
},
});
}

37
src/lib/zod.ts Normal file
View file

@ -0,0 +1,37 @@
import moment from "moment-timezone";
import { z } from "zod";
export const zodId = z
.string()
.refine((value) => /^\d{7,20}$/.test(value), "Invalid ID supplied");
export const zodTpMessages = z.object({
channelId: zodId,
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",
),
});

View file

@ -0,0 +1,73 @@
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 { zodId } from "~/lib/zod";
import { APIResponse } from "~/types/backend";
type Path = "/api/{guildId}/config";
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");
}
try {
zodId.parse(event.params.guildId);
} catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
}
const guildQuery = await db.query.guilds
.findFirst({
where: eq(guilds.id, event.params.guildId),
with: { tpMessages: true, matches: true },
})
.execute();
if (!guildQuery) return ErrorResponse("NOT_FOUND");
return Res("OK", buildConfig(guildQuery));
};
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");
}
try {
zodId.parse(event.params.guildId);
} catch (e) {
return ErrorResponse("BAD_REQUEST", JSON.stringify(e));
}
const guildQuery = await db.query.guilds
.findFirst({
where: eq(guilds.id, event.params.guildId),
with: { tpMessages: true, matches: true },
})
.execute();
if (!guildQuery) return ErrorResponse("NOT_FOUND");
await db.delete(guilds).where(eq(guilds.id, event.params.guildId)).execute();
return Res("NO_CONTENT", null);
};

View file

@ -0,0 +1,93 @@
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 { 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");
}
const guild = await db.query.guilds
.findFirst({
where: eq(guilds.id, event.params.guildId),
with: {
matches: true,
},
})
.execute();
console.log(event.params.guildId, guild);
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");
}
const guild = await db.query.guilds
.findFirst({
where: eq(guilds.id, event.params.guildId),
with: {
matches: true,
},
})
.execute();
console.log(event.params.guildId, guild);
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({
...body.match,
guildId: guild.id,
utc_ts: new Date(body.match.utc_ts),
});
return Res("NO_CONTENT", null);
};

View file

@ -0,0 +1,114 @@
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 { zodTpMessages } from "~/lib/zod";
import { APIResponse, RequestBody } from "~/types/backend";
type Path = "/api/{guildId}/tp_messages";
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");
}
const guild = await db.query.guilds.findFirst({
where: eq(guilds.id, event.params.guildId),
with: {
tpMessages: true,
},
});
if (!guild) return ErrorResponse("NOT_FOUND");
if (!guild.tpEnabled || !guild.tpChannelId) return Res("NO_CONTENT", null);
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;
return acc;
},
{
"0": null,
"1": null,
"2": null,
"3": null,
"4": null,
"5": null,
"6": null,
} as Messages,
);
return Res("OK", {
channelId: guild.tpChannelId,
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");
}
const guild = await db.query.guilds
.findFirst({
where: eq(guilds.id, event.params.guildId),
with: { tpMessages: true },
})
.execute();
if (!guild) return ErrorResponse("NOT_FOUND");
if (!guild.tpEnabled) return ErrorResponse("FORBIDDEN");
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));
}
if (guild.tpChannelId !== body.channelId)
await db
.update(guilds)
.set({ tpChannelId: body.channelId })
.where(eq(guilds.id, guild.id))
.execute();
await Promise.all(
DayKeys.map(async (dayStr) => {
const day = parseInt(dayStr);
await db
.update(tpMessages)
.set({ messageId: body.messageIds[dayStr] })
.where(and(eq(tpMessages.guildId, guild.id), eq(tpMessages.day, day)))
.execute();
}),
);
return Res("NO_CONTENT", null);
};

View file

@ -2,6 +2,7 @@ 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";
@ -20,20 +21,20 @@ export async function GET(event: APIEvent): Promise<Response> {
switch (error) {
case "access_denied":
return new Response(null, {
status: 302,
status: httpStatus.FOUND,
headers: { Location: "/" },
});
default:
console.log("Discord oauth error:", error_description);
return new Response(decodeURI(error_description ?? ""), {
status: 400,
status: httpStatus.BAD_REQUEST,
});
}
const storedState = getCookie("discord_oauth_state") ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400,
status: httpStatus.BAD_REQUEST,
});
}
@ -77,7 +78,7 @@ export async function GET(event: APIEvent): Promise<Response> {
.execute();
return new Response(null, {
status: 302,
status: httpStatus.FOUND,
headers: { Location: "/config" },
});
}
@ -112,7 +113,7 @@ export async function GET(event: APIEvent): Promise<Response> {
sessionCookie.attributes,
);
return new Response(null, {
status: 302,
status: httpStatus.FOUND,
headers: { Location: "/config" },
});
} catch (e) {
@ -120,13 +121,13 @@ export async function GET(event: APIEvent): Promise<Response> {
if (e instanceof OAuth2RequestError) {
// invalid code
return new Response(null, {
status: 400,
status: httpStatus.BAD_REQUEST,
});
}
console.error("Unknown error on callback.");
console.error(e);
return new Response(null, {
status: 500,
status: httpStatus.INTERNAL_SERVER_ERROR,
});
}
}

View file

@ -1,5 +1,6 @@
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";
@ -18,7 +19,7 @@ export async function GET(event: APIEvent) {
});
return new Response(null, {
status: 302,
status: httpStatus.FOUND,
headers: { Location: url.toString() },
});
}

View file

@ -1,4 +1,5 @@
import { APIEvent } from "@solidjs/start/server/types";
import httpStatus from "http-status";
import { appendHeader } from "vinxi/http";
import { lucia } from "~/lib/auth";
@ -13,7 +14,7 @@ export const GET = async (event: APIEvent) => {
lucia.createBlankSessionCookie().serialize(),
);
return new Response(null, {
status: 302,
status: httpStatus.FOUND,
headers: { Location: "/" },
});
};

35
src/routes/api/boot.ts Normal file
View file

@ -0,0 +1,35 @@
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 { 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");
}
const guildQuery = await db.query.guilds
.findMany({
where: eq(guilds.id, event.params.guildId),
with: { tpMessages: true, matches: true },
})
.execute();
return Res(
"OK",
guildQuery.map((e) => buildConfig(e)),
);
};

View file

@ -1,61 +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) => {
if (params.guildId === "boot") {
const guilds = await db.query.guilds
.findMany({
with: {
timePlanning: { with: { messages: true } },
matches: true,
},
})
.execute();
return { guilds };
}
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 guild;
};
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,
},
})
.execute();
if (!guildQuery)
return new Response(JSON.stringify({ error: "No such guild found." }), {
status: 404,
});
const guild = await db
.delete(guilds)
.where(eq(guilds.id, params.guildId))
.returning()
.execute();
return guild;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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";
};

View file

@ -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 };
};

15
src/types/authjs.d.ts vendored
View file

@ -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
View 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 {}

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

@ -5,24 +5,48 @@
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
* 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}/tp_messages": {
/**
* Find guild by ID for it's tp_messages
* Find the tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
get: operations["getTp_messagesOfGuildById"];
/**
* Put new message IDs for tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
put: operations["putTp_messagesOfGuildById"];
};
"/api/{guildId}/matches": {
/**
* Find all matches of guild by ID
* @description Returns tp_messages for a guild
*/
get: operations["getMatchesOfGuildById"];
/**
* Save a new created match of guild by ID
* @description Returns tp_messages for a guild
*/
post: operations["postMatchOfGuildById"];
};
}
@ -32,66 +56,124 @@ export interface components {
schemas: {
guildConfig: {
/**
* Format: varchar(19)
* Format: varchar(20)
* @example 1234567890123456789
*/
guildID?: string;
features?: {
time_planning?: {
guildId: string;
/**
* Format: text
* @example Europe/Berlin
*/
timezone: string;
features: {
timePlanning: {
enabled: boolean;
/**
* Format: varchar(19)
* Format: varchar(20)
* @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;
channelId: string | null;
/** @example 0 */
targetMinute: number;
/** @example 1 */
targetHour: number;
/** @example 1 */
targetDay: number;
roles: {
enabled: boolean;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
isAvailableRoleId: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
wantsToBeNotifieRoledId: string | null;
};
};
};
matches?: components["schemas"]["match"][];
matches: components["schemas"]["match"][];
checksum: string;
};
match: {
/**
* Format: varchar(19)
* Format: varcharq(20)
* @example 1234567890123456789
*/
channelID?: string;
channelId: string;
/**
* Format: varchar(50)
* @example Scrim
*/
matchType?: string;
matchType: string;
/**
* Format: varchar(19)
* Format: varchar(20)
* @example 1234567890123456789
*/
createrId?: string;
createrId: string;
/**
* Format: varchar(19)
* Format: varchar(20)
* @example 1234567890123456789
*/
roleId?: string;
roleId: string;
/**
* Format: varchar(100)
* @example ?
*/
opponentName?: string;
opponentName: string;
/**
* Format: varchar(19)
* Format: varchar(20)
* @example 1234567890123456789
*/
messsageId?: string;
/** @example 0 0 1 5 2 2023 60o */
cron?: string;
messageId: string;
/** @example 2020-01-01T00:00:00Z */
utc_ts: string;
};
tp_messages: {
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
channelId: string;
messageIds: {
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
0: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
1: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
2: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
3: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
4: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
5: string | null;
/**
* Format: varchar(20)
* @example 1234567890123456789
*/
6: string | null;
};
};
};
responses: never;
@ -108,8 +190,34 @@ 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: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
};
};
};
/**
* Find a guild's config by ID
* @description Returns a single guild's config.
*/
getGuildById: {
parameters: {
@ -129,6 +237,10 @@ export interface operations {
400: {
content: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
@ -136,8 +248,8 @@ export interface operations {
};
};
/**
* Deletes a guild config by ID
* @description Delete a guild's config
* Deletes a guild's config by ID
* @description Delete a guild's config when the bot is removed from the guild.
*/
deleteGuildById: {
parameters: {
@ -155,6 +267,10 @@ export interface operations {
400: {
content: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
@ -162,7 +278,7 @@ export interface operations {
};
};
/**
* Find guild by ID for it's tp_messages
* Find the tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
getTp_messagesOfGuildById: {
@ -176,7 +292,7 @@ export interface operations {
/** @description successful operation */
200: {
content: {
"application/json": components["schemas"]["guildConfig"];
"application/json": components["schemas"]["tp_messages"];
};
};
/** @description Time planning not enabled for this guild */
@ -187,6 +303,137 @@ export interface operations {
400: {
content: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
};
};
};
/**
* Put new message IDs for tp_messages of guild by ID
* @description Returns tp_messages for a guild
*/
putTp_messagesOfGuildById: {
parameters: {
path: {
/** @description ID of guild's tp_messages to return */
guildId: string;
};
};
/** @description Put new message IDs for tp_messages in channel */
requestBody: {
content: {
"application/json": components["schemas"]["tp_messages"];
};
};
responses: {
/** @description successful operation */
204: {
content: never;
};
/** @description Invalid ID supplied */
400: {
content: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Time planning not enabled for this guild */
403: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
};
};
};
/**
* Find all matches of guild by ID
* @description Returns tp_messages for a guild
*/
getMatchesOfGuildById: {
parameters: {
path: {
/** @description ID of guild's tp_messages to return */
guildId: string;
};
};
responses: {
/** @description successful operation */
200: {
content: {
"application/json": {
matches: components["schemas"]["match"][];
/**
* Format: text
* @example Europe/Berlin
*/
timezone: string;
};
};
};
/** @description Time planning not enabled for this guild */
204: {
content: never;
};
/** @description Invalid ID supplied */
400: {
content: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;
};
};
};
/**
* Save a new created match of guild by ID
* @description Returns tp_messages 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: never;
};
/** @description Unauthorized */
401: {
content: never;
};
/** @description Guild not found */
404: {
content: never;

25
src/types/lucia-auth.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
import { PgColumn, PgTableWithColumns } from "drizzle-orm/pg-core";
import { users } from "~/drizzle/schema";
import { lucia } from "~/lib/auth";
declare module "lucia" {
// eslint-disable-next-line no-unused-vars
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
type GetColumns<T> =
T extends PgTableWithColumns<infer First> ? First["columns"] : never;
type ExtractDataTypes<T> = {
[K in keyof T]: T[K] extends PgColumn<infer DataType, never, never>
? DataType["data"]
: never;
};
interface DatabaseUserAttributes
extends ExtractDataTypes<GetColumns<typeof users>> {
warst: string;
}