initial commit
Some checks failed
deploy / build-and-deploy (push) Failing after 21s

This commit is contained in:
2025-05-18 13:16:20 +02:00
commit 60f3f8a096
148 changed files with 17900 additions and 0 deletions

10
src/util/action.ts Normal file
View File

@ -0,0 +1,10 @@
import type { ActionError } from 'astro:actions';
import { popupState } from '@components/popup/Popup.ts';
export function actionErrorPopup<E extends Record<string, any>>(error: ActionError<E>) {
popupState.set({
type: 'error',
title: `Fehler (${error.status})`,
message: error.message
});
}

14
src/util/minecraft.ts Normal file
View File

@ -0,0 +1,14 @@
export async function getJavaUuid(username: string) {
const response = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
if (!response.ok) {
// rate limit
if (response.status == 429) return null;
// user doesn't exist
else if (response.status < 500) throw new Error();
return null;
}
const json = await response.json();
const id: string = json['id'];
// prettier-ignore
return `${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`;
}

80
src/util/permissions.ts Normal file
View File

@ -0,0 +1,80 @@
export class Permissions {
static readonly Admin = new Permissions(2 << 0);
static readonly Users = new Permissions(2 << 1);
static readonly Reports = new Permissions(2 << 2);
static readonly Feedback = new Permissions(2 << 3);
static readonly Settings = new Permissions(2 << 4);
static readonly Tools = new Permissions(2 << 5);
readonly value: number;
constructor(value: number | number[] | Permissions[] | null) {
if (value == null) {
this.value = 0;
} else if (typeof value == 'number') {
this.value = value;
} else if (Array.isArray(value)) {
let finalValue = 0;
for (const v of value) {
if (typeof v == 'number') {
finalValue |= v;
} else {
finalValue |= v.value;
}
}
this.value = finalValue;
} else {
throw 'Invalid arguments';
}
}
toJSON() {
return this.value;
}
toNumberArray() {
const array = [];
if (this.admin) array.push(Permissions.Admin.value);
if (this.users) array.push(Permissions.Users.value);
if (this.reports) array.push(Permissions.Reports.value);
if (this.feedback) array.push(Permissions.Feedback.value);
if (this.settings) array.push(Permissions.Settings.value);
if (this.tools) array.push(Permissions.Tools.value);
return array;
}
get admin() {
return (this.value & Permissions.Admin.value) != 0;
}
get users() {
return (this.value & Permissions.Users.value) != 0;
}
get reports() {
return (this.value & Permissions.Reports.value) != 0;
}
get feedback() {
return (this.value & Permissions.Reports.value) != 0;
}
get settings() {
return (this.value & Permissions.Reports.value) != 0;
}
get tools() {
return (this.value & Permissions.Tools.value) != 0;
}
hasPermissions(other: Permissions) {
return (other.value & this.value) == other.value;
}
static allPermissions() {
return [
Permissions.Admin,
Permissions.Users,
Permissions.Reports,
Permissions.Feedback,
Permissions.Settings,
Permissions.Tools
];
}
}

5
src/util/random.ts Normal file
View File

@ -0,0 +1,5 @@
import * as crypto from 'crypto';
export function generateRandomString(length: number) {
return crypto.randomBytes(length).toString('hex');
}

77
src/util/session.ts Normal file
View File

@ -0,0 +1,77 @@
import type { AstroCookies, AstroCookieSetOptions } from 'astro';
import { ActionError } from 'astro:actions';
import crypto from 'node:crypto';
import { Permissions } from './permissions.ts';
export class Session {
static readonly #cookieName = 'muelleel';
static readonly #cookieOptions: AstroCookieSetOptions = {
httpOnly: true,
path: '/',
sameSite: 'lax'
};
static #sessions: Session[] = [];
readonly sessionId: string;
readonly adminId: number;
readonly permissions: Permissions;
private constructor(sessionId: string, adminId: number, permissions: Permissions) {
this.sessionId = sessionId;
this.adminId = adminId;
this.permissions = permissions;
Session.#sessions.push(this);
}
invalidate(cookies?: AstroCookies) {
for (let i = 0; i < Session.#sessions.length; i++) {
if (Session.#sessions[i] == this) {
Session.#sessions = Session.#sessions.splice(i, 1);
if (cookies) cookies.delete(Session.#cookieName, Session.#cookieOptions);
break;
}
}
}
static newSession(adminId: number, permissions: Permissions, cookies: AstroCookies) {
const session = new Session(crypto.randomBytes(16).toString('hex'), adminId, permissions);
Session.#sessions.push(session);
cookies.set(Session.#cookieName, session.sessionId, Session.#cookieOptions);
return session;
}
static sessionFromCookies(cookies: AstroCookies, neededPermissions?: Permissions) {
const sessionId = cookies.get(Session.#cookieName);
if (!sessionId) return null;
for (const session of Session.#sessions) {
if (session.sessionId == sessionId.value) {
if (neededPermissions && !session.permissions.hasPermissions(neededPermissions)) {
break;
}
return session;
}
}
return null;
}
static actionSessionFromCookies(cookies: AstroCookies, neededPermissions?: Permissions) {
const sessionId = cookies.get(Session.#cookieName);
if (!sessionId) throw new ActionError({ code: 'UNAUTHORIZED' });
for (const session of Session.#sessions) {
if (session.sessionId == sessionId.value) {
if (neededPermissions && !session.permissions.hasPermissions(neededPermissions)) {
throw new ActionError({ code: 'UNAUTHORIZED' });
}
return session;
}
}
throw new ActionError({ code: 'UNAUTHORIZED' });
}
}

50
src/util/settings.ts Normal file
View File

@ -0,0 +1,50 @@
import type { Database } from '@db/database.ts';
export async function setSettings<K extends SettingKey>(db: Database, settings: { [k in K]: SettingKeyValueType<K> }) {
const dbSettings = Object.entries(settings).map(([name, value]) => ({ name, value: JSON.stringify(value) }));
await db.setSettings({ settings: dbSettings });
}
export async function getSetting<K extends SettingKey>(db: Database, key: K): Promise<SettingKeyValueType<K> | null>;
export async function getSetting<K extends SettingKey, D extends SettingKeyValueType<K>>(
db: Database,
key: K,
defaultValue: D
): Promise<SettingKeyValueType<K>>;
export async function getSetting<K extends SettingKey, D extends SettingKeyValueType<K>>(
db: Database,
key: K,
defaultValue?: D
) {
const setting = await db.getSetting({ name: key });
return setting != null ? JSON.parse(setting) : defaultValue != undefined ? defaultValue : null;
}
export async function getSettings<K extends SettingKey>(
db: Database
): Promise<{ [k in K]: SettingKeyValueType<K> | null }>;
export async function getSettings<K extends SettingKey[]>(
db: Database,
keys: K
): Promise<{ [k in K[number]]: SettingKeyValueType<k> | null }>;
export async function getSettings<K extends SettingKey[]>(db: Database, keys?: K) {
const settings = await db.getSettings({ names: keys });
return settings.reduce(
(prev, curr) => Object.assign(prev, { [curr.name]: curr.value != null ? JSON.parse(curr.value) : null }),
{} as { [k in K[number]]: SettingKeyValueType<k> | null }
);
}
export enum SettingKey {
SignupEnabled = 'signup.enabled',
SignupDisabledMessage = 'signup.disabledMessage',
SignupDisabledSubMessage = 'signup.disabledSubMessage'
}
export type SettingKeyValueType<K extends SettingKey> = {
[SettingKey.SignupEnabled]: boolean;
[SettingKey.SignupDisabledMessage]: string;
[SettingKey.SignupDisabledSubMessage]: string;
}[K];

7
src/util/sleep.ts Normal file
View File

@ -0,0 +1,7 @@
export async function sleepMilliseconds(milliseconds: number) {
await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
export async function sleepSeconds(seconds: number) {
await sleepMilliseconds(seconds * 1000);
}

33
src/util/webhook.ts Normal file
View File

@ -0,0 +1,33 @@
import { sleepSeconds } from './sleep.ts';
import { WEBHOOK_ENDPOINT } from 'astro:env/client';
export enum WebhookAction {
Strike = 'strike'
}
export type WebhookActionType<T extends WebhookAction> = {
[WebhookAction.Strike]: {
users: string[];
totalWeight: number;
};
}[T];
export async function sendWebhook<T extends WebhookAction>(action: T, data: WebhookActionType<T>) {
if (!WEBHOOK_ENDPOINT || !/^https?:\/\/.+$/.test(WEBHOOK_ENDPOINT)) return;
while (true) {
try {
const response = await fetch(WEBHOOK_ENDPOINT, {
body: JSON.stringify(data),
headers: {
'x-webhook-action': action
},
keepalive: false
});
if (response.status === 200) return;
} catch (_) {}
await sleepSeconds(60);
}
}