This commit is contained in:
10
src/util/action.ts
Normal file
10
src/util/action.ts
Normal 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
14
src/util/minecraft.ts
Normal 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
80
src/util/permissions.ts
Normal 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
5
src/util/random.ts
Normal 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
77
src/util/session.ts
Normal 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
50
src/util/settings.ts
Normal 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
7
src/util/sleep.ts
Normal 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
33
src/util/webhook.ts
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user