rewrite website

This commit is contained in:
2025-10-13 17:22:49 +02:00
parent a6d910f56a
commit 32f28e5324
263 changed files with 17904 additions and 14451 deletions

82
src/db/schema/admin.ts Normal file
View File

@@ -0,0 +1,82 @@
import { int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
import { Permissions } from '@util/permissions.ts';
import * as bcrypt from 'bcrypt';
type Database = MySql2Database<{ admin: typeof admin }>;
export const admin = mysqlTable('admin', {
id: int('id').primaryKey().autoincrement(),
username: varchar('username', { length: 255 }).notNull(),
password: varchar('password', { length: 255 }).notNull(),
permissions: int('permissions').notNull()
});
export type AddAdminReq = Omit<typeof admin.$inferInsert, 'id'>;
export type EditAdminReq = {
id: number;
username: string;
password: string | null;
permissions: number;
};
export type DeleteAdminReq = {
id: number;
};
export type GetAdminReq = {};
export type GetAdminRes = Omit<typeof admin.$inferSelect, 'password'>[];
export type ExistsAdminReq = {
username: string;
password: string;
};
export type ExistsAdminRes = {
id: number;
username: string;
permissions: Permissions;
} | null;
export async function addAdmin(db: Database, values: AddAdminReq) {
values.password = bcrypt.hashSync(values.password, 10);
const adminIds = await db.insert(admin).values(values).$returningId();
return adminIds[0];
}
export async function editAdmin(db: Database, values: EditAdminReq) {
return db
.update(admin)
.set({
id: values.id,
username: values.username,
password: values.password != null ? bcrypt.hashSync(values.password, 10) : undefined,
permissions: values.permissions
})
.where(eq(admin.id, values.id));
}
export async function deleteAdmin(db: Database, values: DeleteAdminReq) {
return db.delete(admin).where(eq(admin.id, values.id));
}
export async function getAdmins(db: Database, _values: GetAdminReq): Promise<GetAdminRes> {
return db.select({ id: admin.id, username: admin.username, permissions: admin.permissions }).from(admin);
}
export async function existsAdmin(db: Database, values: ExistsAdminReq): Promise<ExistsAdminRes> {
const a = await db.query.admin.findFirst({
where: eq(admin.username, values.username)
});
if (!a || !bcrypt.compareSync(values.password, a.password)) return null;
return {
id: a.id,
username: a.username,
permissions: new Permissions(a.permissions)
};
}

View File

@@ -0,0 +1,58 @@
import { int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
type Database = MySql2Database<{ blockedUser: typeof blockedUser }>;
export const blockedUser = mysqlTable('blocked_user', {
id: int('id').primaryKey().autoincrement(),
uuid: varchar('uuid', { length: 255 }).unique().notNull(),
comment: varchar('comment', { length: 255 })
});
export type AddBlockedUserReq = {
uuid: string;
comment?: string | null;
};
export type EditBlockedUserReq = {
id: number;
uuid: string;
comment?: string | null;
};
export type DeleteBlockedUserReq = {
id: number;
};
export type GetBlockedUserByUuidReq = {
uuid: string;
};
export type GetBlockedUsersReq = {};
export async function addBlockedUser(db: Database, values: AddBlockedUserReq) {
const bu = await db.insert(blockedUser).values(values).$returningId();
return bu[0];
}
export async function editBlockedUser(db: Database, values: EditBlockedUserReq) {
await db.update(blockedUser).set(values).where(eq(blockedUser.id, values.id));
}
export async function deleteBlockedUser(db: Database, values: DeleteBlockedUserReq) {
return db.delete(blockedUser).where(eq(blockedUser.id, values.id));
}
export async function getBlockedUserByUuid(db: Database, values: GetBlockedUserByUuidReq) {
const bu = await db.query.blockedUser.findFirst({
where: eq(blockedUser.uuid, values.uuid)
});
return bu ?? null;
}
export async function getBlockedUsers(db: Database, _values: GetBlockedUsersReq) {
return db.select().from(blockedUser);
}

98
src/db/schema/feedback.ts Normal file
View File

@@ -0,0 +1,98 @@
import { int, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core';
import { user } from './user.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq, inArray } from 'drizzle-orm';
import { generateRandomString } from '@util/random.ts';
type Database = MySql2Database<{ feedback: typeof feedback }>;
export const feedback = mysqlTable('feedback', {
id: int('id').primaryKey().autoincrement(),
event: varchar('event', { length: 255 }).notNull(),
title: varchar('title', { length: 255 }),
content: text('content'),
urlHash: varchar('url_hash', { length: 255 }).unique().notNull(),
lastChanged: timestamp('last_changed', { mode: 'date' }).notNull().defaultNow().onUpdateNow(),
userId: int('user_id').references(() => user.id)
});
export type AddFeedbackReq = {
event: string;
content: string;
};
export type AddUserFeedbacksReq = {
event: string;
title: string;
uuids: string[];
};
export type SubmitFeedbackReq = {
urlHash: string;
content: string;
};
export type GetFeedbacksReq = {};
export type GetFeedbackByUrlHash = {
urlHash: string;
};
export async function addFeedback(db: Database, values: AddFeedbackReq) {
return db.insert(feedback).values({
event: values.event,
content: values.content,
urlHash: generateRandomString(16)
});
}
export async function addUserFeedbacks(db: Database, values: AddUserFeedbacksReq) {
const users = await db.select({ id: user.id, uuid: user.uuid }).from(user).where(inArray(user.uuid, values.uuids));
const userFeedbacks = users.map((user) => ({
id: user.id,
uuid: user.uuid!,
urlHash: generateRandomString(16)
}));
await db.insert(feedback).values(
userFeedbacks.map((feedback) => ({
event: values.event,
title: values.title,
urlHash: feedback.urlHash,
userId: feedback.id
}))
);
return userFeedbacks;
}
export async function submitFeedback(db: Database, values: SubmitFeedbackReq) {
return db
.update(feedback)
.set({
content: values.content
})
.where(eq(feedback.urlHash, values.urlHash));
}
export async function getFeedbacks(db: Database, _values: GetFeedbacksReq) {
return db
.select({
id: feedback.id,
event: feedback.event,
title: feedback.title,
content: feedback.content,
urlHash: feedback.urlHash,
lastChanged: feedback.lastChanged,
username: user.username
})
.from(feedback)
.leftJoin(user, eq(feedback.userId, user.id));
}
export async function getFeedbackByUrlHash(db: Database, values: GetFeedbackByUrlHash) {
return db.query.feedback.findFirst({
where: eq(feedback.urlHash, values.urlHash)
});
}

218
src/db/schema/report.ts Normal file
View File

@@ -0,0 +1,218 @@
import { alias, int, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { and, eq, isNotNull } from 'drizzle-orm';
import { reportStatus } from './reportStatus.ts';
import { generateRandomString } from '@util/random.ts';
import { BASE_PATH } from 'astro:env/server';
import { strikeReason } from '@db/schema/strikeReason.ts';
import { strike } from '@db/schema/strike.ts';
import { user } from '@db/schema/user.ts';
type Database = MySql2Database<{ report: typeof report }>;
export const report = mysqlTable('report', {
id: int('id').primaryKey().autoincrement(),
reason: varchar('reason', { length: 255 }).notNull(),
body: text('body'),
urlHash: varchar('url_hash', { length: 255 }).notNull(),
createdAt: timestamp('created_at', { mode: 'date' }),
reporterId: int('reporter_id').references(() => user.id),
reportedId: int('reported_id').references(() => user.id)
});
export type AddReportReq = {
reason: string;
body: string | null;
createdAt?: Date | null;
reporterId?: number;
reportedId?: number | null;
};
export type EditReportReq = {
id: number;
reportedId: number | null;
};
export type SubmitReportReq = {
urlHash: string;
reportedId: number | null;
reason: string;
body: string;
};
export type GetReportsReq = {
reporter?: string | null;
reported?: string | null;
includeDrafts?: boolean | null;
};
export type GetReportById = {
id: number;
};
export type GetReportByUrlHash = {
urlHash: string;
};
export async function addReport(db: Database, values: AddReportReq) {
const urlHash = generateRandomString(16);
const r = await db
.insert(report)
.values({
reason: values.reason,
body: values.body,
urlHash: urlHash,
createdAt: values.createdAt,
reporterId: values.reporterId,
reportedId: values.reportedId
})
.$returningId();
return Object.assign(r[0], { url: `${BASE_PATH}/report/${urlHash}` });
}
export async function editReport(db: Database, values: EditReportReq) {
return db.update(report).set({
reportedId: values.reportedId
});
}
export async function submitReport(db: Database, values: SubmitReportReq) {
return db
.update(report)
.set({
reportedId: values.reportedId,
reason: values.reason,
body: values.body,
createdAt: new Date()
})
.where(eq(report.urlHash, values.urlHash));
}
export async function getReports(db: Database, values: GetReportsReq) {
const reporter = alias(user, 'reporter');
const reported = alias(user, 'reported');
let reporterIdSubquery;
if (values.reporter != null) {
reporterIdSubquery = db
.select({ id: reporter.id })
.from(reporter)
.where(eq(reporter.username, values.reporter))
.as('reporter_id_subquery');
}
let reportedIdSubquery;
if (values.reported != null) {
reportedIdSubquery = db
.select({ id: reported.id })
.from(reported)
.where(eq(reported.username, values.reported))
.as('reported_id_subquery');
}
return db
.select({
id: report.id,
reason: report.reason,
body: report.body,
urlHash: report.urlHash,
createdAt: report.createdAt,
reporter: {
id: reporter.id,
username: reporter.username
},
reported: {
id: reported.id,
username: reported.username
},
status: {
status: reportStatus.status,
notice: reportStatus.notice,
statement: reportStatus.statement
},
strike: {
strikeReasonId: strikeReason.id
}
})
.from(report)
.innerJoin(reporter, eq(report.reporterId, reporter.id))
.leftJoin(reported, eq(report.reportedId, reported.id))
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
.leftJoin(strike, eq(report.id, strike.reportId))
.leftJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id))
.where(
and(
values.reporter != null ? eq(report.reporterId, reporterIdSubquery!.id) : undefined,
values.reported != null ? eq(report.reportedId, reportedIdSubquery!.id) : undefined,
values.includeDrafts == false ? isNotNull(report.createdAt) : undefined
)
);
}
export async function getReportById(db: Database, values: GetReportById) {
const reporter = alias(user, 'reporter');
const reported = alias(user, 'reported');
const reports = await db
.select({
id: report.id,
reason: report.reason,
body: report.body,
createdAt: report.createdAt,
reporter: {
id: reporter.id,
username: reporter.username
},
reported: {
id: reported.id,
username: reported.username
},
status: {
status: reportStatus.status,
notice: reportStatus.notice,
statement: reportStatus.statement
}
})
.from(report)
.innerJoin(reporter, eq(report.reporterId, reporter.id))
.leftJoin(reported, eq(report.reportedId, reported.id))
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
.where(eq(report.id, values.id));
return reports[0] ?? null;
}
export async function getReportByUrlHash(db: Database, values: GetReportByUrlHash) {
const reporter = alias(user, 'reporter');
const reported = alias(user, 'reported');
const reports = await db
.select({
id: report.id,
reason: report.reason,
body: report.body,
createdAt: report.createdAt,
urlHash: report.urlHash,
reporter: {
id: reporter.id,
username: reporter.username
},
reported: {
id: reported.id,
username: reported.username
},
status: {
status: reportStatus.status,
notice: reportStatus.notice,
statement: reportStatus.statement
}
})
.from(report)
.innerJoin(reporter, eq(report.reporterId, reporter.id))
.leftJoin(reported, eq(report.reportedId, reported.id))
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
.where(eq(report.urlHash, values.urlHash));
return reports[0] ?? null;
}

View File

@@ -0,0 +1,38 @@
import { char, int, mysqlEnum, mysqlTable } from 'drizzle-orm/mysql-core';
import { report } from '@db/schema/report.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
type Database = MySql2Database<{ reportAttachment: typeof reportAttachment }>;
export const reportAttachment = mysqlTable('report_attachment', {
type: mysqlEnum('type', ['image', 'video']),
hash: char('hash', { length: 32 }),
reportId: int('report_id')
.notNull()
.references(() => report.id)
});
export type AddReportAttachmentReq = {
type: 'image' | 'video';
hash: string;
reportId: number;
};
export type GetReportAttachmentsReq = {
reportId: number;
};
export async function addReportAttachment(db: Database, values: AddReportAttachmentReq) {
await db.insert(reportAttachment).values(values);
}
export async function getReportAttachments(db: Database, values: GetReportAttachmentsReq) {
return db
.select({
type: reportAttachment.type,
hash: reportAttachment.hash
})
.from(reportAttachment)
.where(eq(reportAttachment.reportId, values.reportId));
}

View File

@@ -0,0 +1,50 @@
import { int, mysqlEnum, mysqlTable, text } from 'drizzle-orm/mysql-core';
import { admin } from './admin.ts';
import { report } from './report.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
type Database = MySql2Database<{ reportStatus: typeof reportStatus }>;
export const reportStatus = mysqlTable('report_status', {
status: mysqlEnum('status', ['open', 'closed']),
notice: text('notice'),
statement: text('statement'),
reportId: int('report_id')
.notNull()
.unique()
.references(() => report.id),
reviewerId: int('reviewer_id').references(() => admin.id)
});
export type GetReportStatusReq = {
reportId: number;
};
export type EditReportStatusReq = {
reportId: number;
status: 'open' | 'closed' | null;
notice: string | null;
statement: string | null;
};
export async function getReportStatus(db: Database, values: GetReportStatusReq) {
const rs = await db.query.reportStatus.findFirst({
where: eq(reportStatus.reportId, values.reportId)
});
return rs ?? null;
}
export async function editReportStatus(db: Database, values: EditReportStatusReq) {
return db
.insert(reportStatus)
.values(values)
.onDuplicateKeyUpdate({
set: {
status: values.status,
notice: values.notice,
statement: values.statement
}
});
}

52
src/db/schema/settings.ts Normal file
View File

@@ -0,0 +1,52 @@
import { mysqlTable, text, varchar } from 'drizzle-orm/mysql-core';
import { eq, inArray } from 'drizzle-orm';
import type { MySql2Database } from 'drizzle-orm/mysql2';
type Database = MySql2Database<{ settings: typeof settings }>;
export const settings = mysqlTable('settings', {
name: varchar('name', { length: 255 }).unique().notNull(),
value: text()
});
export type GetSettingsReq = {
names?: string[];
};
export type SetSettingsReq = {
settings: {
name: string;
value: string | null;
}[];
};
export type GetSettingReq = {
name: string;
};
export async function getSettings(db: Database, values: GetSettingsReq) {
return db
.select({ name: settings.name, value: settings.value })
.from(settings)
.where(values.names ? inArray(settings.name, values.names) : undefined);
}
export async function setSettings(db: Database, values: SetSettingsReq) {
return db.transaction(async (tx) => {
for (const setting of values.settings) {
await tx
.insert(settings)
.values(setting)
.onDuplicateKeyUpdate({
set: {
value: setting.value
}
});
}
});
}
export async function getSetting(db: Database, values: GetSettingReq): Promise<string | null> {
const value = await db.select({ value: settings.value }).from(settings).where(eq(settings.name, values.name));
return value.length > 0 ? value[0]?.value : null;
}

81
src/db/schema/strike.ts Normal file
View File

@@ -0,0 +1,81 @@
import { int, mysqlTable, timestamp } from 'drizzle-orm/mysql-core';
import { strikeReason } from '@db/schema/strikeReason.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
import { report } from '@db/schema/report.ts';
type Database = MySql2Database<{ strike: typeof strike }>;
export const strike = mysqlTable('strike', {
at: timestamp('at', { mode: 'date' }).notNull(),
reportId: int('report_id')
.notNull()
.references(() => report.id),
strikeReasonId: int('strike_reason_id')
.notNull()
.references(() => strikeReason.id)
});
export type EditStrikeReq = {
reportId: number;
at?: Date;
strikeReasonId: number;
};
export type DeleteStrikeReq = {
reportId: number;
};
export type GetStrikeByReportIdReq = {
reportId: number;
};
export type GetStrikesByUserIdReq = {
userId: number;
};
export async function editStrike(db: Database, values: EditStrikeReq) {
return db
.insert(strike)
.values({
at: values.at ?? new Date(),
reportId: values.reportId,
strikeReasonId: values.strikeReasonId
})
.onDuplicateKeyUpdate({
set: {
at: values.at ?? new Date(),
strikeReasonId: values.strikeReasonId
}
});
}
export async function deleteStrike(db: Database, values: DeleteStrikeReq) {
return db.delete(strike).where(eq(strike.reportId, values.reportId)).limit(1);
}
export async function getStrikeByReportId(db: Database, values: GetStrikeByReportIdReq) {
const strikes = await db
.select({
strike,
strikeReason
})
.from(strike)
.where(eq(strike.reportId, values.reportId))
.leftJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id));
return strikes[0] ?? null;
}
export async function getStrikesByUserId(db: Database, values: GetStrikesByUserIdReq) {
return db
.select({
at: strike.at,
report: report,
reason: strikeReason
})
.from(strike)
.innerJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id))
.innerJoin(report, eq(strike.reportId, report.id))
.where(eq(report.reportedId, values.userId));
}

View File

@@ -0,0 +1,42 @@
import { int, mysqlTable, tinyint, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { asc, eq } from 'drizzle-orm';
type Database = MySql2Database<{ strikeReason: typeof strikeReason }>;
export const strikeReason = mysqlTable('strike_reason', {
id: int('id').primaryKey().autoincrement(),
name: varchar('name', { length: 255 }).notNull(),
weight: tinyint('weight').notNull()
});
export type AddStrikeReasonReq = {
name: string;
weight: number;
};
export type EditStrikeReasonReq = typeof strikeReason.$inferSelect;
export type DeleteStrikeReasonReq = {
id: number;
};
export type GetStrikeReasonsReq = {};
export async function addStrikeReason(db: Database, values: AddStrikeReasonReq) {
const sr = await db.insert(strikeReason).values(values).$returningId();
return sr[0];
}
export async function editStrikeReason(db: Database, values: EditStrikeReasonReq) {
await db.update(strikeReason).set(values).where(eq(strikeReason.id, values.id));
}
export async function deleteStrikeReason(db: Database, values: DeleteStrikeReasonReq) {
await db.delete(strikeReason).where(eq(strikeReason.id, values.id));
}
export async function getStrikeReasons(db: Database, _values: GetStrikeReasonsReq) {
return db.select().from(strikeReason).orderBy(asc(strikeReason.weight));
}

106
src/db/schema/user.ts Normal file
View File

@@ -0,0 +1,106 @@
import { date, int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { eq, like, or } from 'drizzle-orm';
import { mysqlEnum } from 'drizzle-orm/mysql-core/columns/enum';
type Database = MySql2Database<{ user: typeof user }>;
export const user = mysqlTable('user', {
id: int('id').primaryKey().autoincrement(),
firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(),
birthday: date('birthday', { mode: 'string' }).notNull(),
telephone: varchar('telephone', { length: 255 }),
username: varchar('username', { length: 255 }).notNull(),
edition: mysqlEnum(['java', 'bedrock']).notNull(),
uuid: varchar('uuid', { length: 36 })
});
export type AddUserReq = Omit<typeof user.$inferInsert, 'id'>;
export type EditUserReq = Partial<Omit<typeof user.$inferInsert, 'id'>> & { id: number };
export type DeleteUserReq = {
id: number;
};
export type ExistsUserReq = {
username?: string;
uuid?: string;
};
export type GetUsersReq = {
username?: string | null;
limit?: number;
};
export type GetUsersRes = (typeof user.$inferSelect)[];
export type GetUserByIdReq = {
id: number;
};
export type GetUserByIdRes = typeof user.$inferSelect | null;
export type GetUserByUsernameReq = {
username: string;
};
export type GetUserByUsernameRes = typeof user.$inferSelect | null;
export type GetUserByUuidReq = {
uuid: string;
};
export async function addUser(db: Database, values: AddUserReq) {
const userIds = await db.insert(user).values(values).$returningId();
return userIds[0];
}
export async function editUser(db: Database, values: EditUserReq) {
await db.update(user).set(values).where(eq(user.id, values.id));
}
export async function deleteUser(db: Database, values: DeleteUserReq) {
await db.delete(user).where(eq(user.id, values.id));
}
export async function existsUser(db: Database, values: ExistsUserReq) {
const u = db.query.user.findFirst({
where: or(
values.username != null ? eq(user.username, values.username) : undefined,
values.uuid != null ? eq(user.uuid, values.uuid) : undefined
)
});
return u ?? null;
}
export async function getUsers(db: Database, values: GetUsersReq): Promise<GetUsersRes> {
return db.query.user.findMany({
where: values.username != null ? like(user.username, `%${values.username}%`) : undefined,
limit: values.limit
});
}
export async function getUserById(db: Database, values: GetUserByIdReq): Promise<GetUserByIdRes> {
const u = await db.query.user.findFirst({
where: eq(user.id, values.id)
});
return u ?? null;
}
export async function getUserByUsername(db: Database, values: GetUserByUsernameReq): Promise<GetUserByUsernameRes> {
const u = await db.query.user.findFirst({
where: eq(user.username, values.username)
});
return u ?? null;
}
export async function getUserByUuid(db: Database, values: GetUserByUuidReq) {
const u = await db.query.user.findFirst({
where: eq(user.uuid, values.uuid)
});
return u ?? null;
}