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

93
src/db/database.sql Normal file
View File

@@ -0,0 +1,93 @@
-- admins
CREATE TABLE IF NOT EXISTS admin (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
permissions INT NOT NULL
);
-- user
CREATE TABLE IF NOT EXISTS user (
id INT AUTO_INCREMENT PRIMARY KEY,
firstname VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NOT NULL,
birthday DATE NOT NULL,
telephone VARCHAR(255),
username VARCHAR(255) NOT NULL,
edition ENUM('java', 'bedrock'),
uuid VARCHAR(36)
);
-- blocked user
CREATE TABLE IF NOT EXISTS blocked_user (
id INT AUTO_INCREMENT PRIMARY KEY,
uuid VARCHAR(255) UNIQUE NOT NULL,
comment TINYTEXT
);
-- report
CREATE TABLE IF NOT EXISTS report (
id INT AUTO_INCREMENT PRIMARY KEY,
reason VARCHAR(255) NOT NULL,
body TEXT,
url_hash VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP,
reporter_id INT,
reported_id INT,
FOREIGN KEY (reporter_id) REFERENCES user(id) ON DELETE CASCADE,
FOREIGN KEY (reported_id) REFERENCES user(id) ON DELETE CASCADE
);
-- report attachment
CREATE TABLE IF NOT EXISTS report_attachment (
id INT AUTO_INCREMENT PRIMARY KEY,
type ENUM('image', 'video') NOT NULL,
hash CHAR(32) NOT NULL,
report_id INT NOT NULL,
FOREIGN KEY (report_id) REFERENCES report(id) ON DELETE CASCADE
);
-- report status
CREATE TABLE IF NOT EXISTS report_status (
status ENUM('open', 'closed'),
notice TEXT,
statement TEXT,
report_id INT NOT NULL UNIQUE,
reviewer_id INT,
FOREIGN KEY (report_id) REFERENCES report(id) ON DELETE CASCADE,
FOREIGN KEY (reviewer_id) REFERENCES admin(id) ON DELETE CASCADE
);
-- strike reason
CREATE TABLE IF NOT EXISTS strike_reason (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
weight TINYINT NOT NULL
);
-- strike
CREATE TABLE IF NOT EXISTS strike (
at TIMESTAMP NOT NULL,
report_id INT NOT NULL UNIQUE,
strike_reason_id INT NOT NULL,
FOREIGN KEY (report_id) REFERENCES report(id) ON DELETE CASCADE,
FOREIGN KEY (strike_reason_id) REFERENCES strike_reason(id) ON DELETE CASCADE
);
-- feedback
CREATE TABLE IF NOT EXISTS feedback (
id INT AUTO_INCREMENT PRIMARY KEY,
event VARCHAR(255) NOT NULL,
title VARCHAR(255),
content TEXT,
url_hash VARCHAR(255) NOT NULL UNIQUE,
last_changed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
user_id INT,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
);
-- settings
CREATE TABLE IF NOT EXISTS settings (
name VARCHAR(255) UNIQUE NOT NULL,
value TEXT NOT NULL
);

235
src/db/database.ts Normal file
View File

@@ -0,0 +1,235 @@
import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import {
existsAdmin,
admin,
type ExistsAdminReq,
type GetAdminReq,
getAdmins,
type AddAdminReq,
addAdmin,
type EditAdminReq,
editAdmin,
type DeleteAdminReq,
deleteAdmin
} from './schema/admin';
import {
type AddUserReq,
type EditUserReq,
type DeleteUserReq,
type GetUsersReq,
type GetUserByUsernameReq,
user,
addUser,
editUser,
deleteUser,
getUsers,
getUserByUsername,
existsUser,
type ExistsUserReq,
type GetUserByUuidReq,
getUserByUuid,
type GetUserByIdReq,
getUserById
} from './schema/user';
import {
type GetSettingReq,
settings,
getSetting,
type GetSettingsReq,
getSettings,
type SetSettingsReq,
setSettings
} from './schema/settings';
import {
addFeedback,
type AddFeedbackReq,
addUserFeedbacks,
type AddUserFeedbacksReq,
feedback,
getFeedbackByUrlHash,
type GetFeedbackByUrlHash,
getFeedbacks,
type GetFeedbacksReq,
submitFeedback,
type SubmitFeedbackReq
} from './schema/feedback.ts';
import {
addReport,
type AddReportReq,
editReport,
type EditReportReq,
getReportById,
type GetReportById,
getReportByUrlHash,
type GetReportByUrlHash,
getReports,
type GetReportsReq,
report,
submitReport,
type SubmitReportReq
} from './schema/report.ts';
import { DATABASE_URI } from 'astro:env/server';
import {
type GetStrikeReasonsReq,
getStrikeReasons,
strikeReason,
type AddStrikeReasonReq,
addStrikeReason,
type EditStrikeReasonReq,
editStrikeReason,
type DeleteStrikeReasonReq,
deleteStrikeReason
} from '@db/schema/strikeReason.ts';
import {
deleteStrike,
type DeleteStrikeReq,
editStrike,
type EditStrikeReq,
getStrikeByReportId,
type GetStrikeByReportIdReq,
getStrikesByUserId,
type GetStrikesByUserIdReq,
strike
} from '@db/schema/strike.ts';
import {
editReportStatus,
type EditReportStatusReq,
getReportStatus,
type GetReportStatusReq,
reportStatus
} from '@db/schema/reportStatus.ts';
import {
addBlockedUser,
type AddBlockedUserReq,
getBlockedUsers,
type GetBlockedUsersReq,
blockedUser,
type GetBlockedUserByUuidReq,
getBlockedUserByUuid,
type EditBlockedUserReq,
editBlockedUser,
type DeleteBlockedUserReq,
deleteBlockedUser
} from '@db/schema/blockedUser.ts';
import {
addReportAttachment,
type AddReportAttachmentReq,
getReportAttachments,
type GetReportAttachmentsReq,
reportAttachment
} from '@db/schema/reportAttachment.ts';
export class Database {
protected readonly db: MySql2Database<{
admin: typeof admin;
user: typeof user;
blockedUser: typeof blockedUser;
report: typeof report;
reportAttachment: typeof reportAttachment;
reportStatus: typeof reportStatus;
strike: typeof strike;
strikeReason: typeof strikeReason;
feedback: typeof feedback;
settings: typeof settings;
}>;
private constructor(db: typeof this.db) {
this.db = db;
}
static async init(databaseUri: string) {
const connectionPool = mysql.createPool({
uri: databaseUri
});
const db = drizzle({
client: connectionPool,
schema: {
admin,
user,
blockedUser,
report,
reportAttachment,
reportStatus,
strike,
strikeReason,
feedback,
settings
},
mode: 'default'
});
return new Database(db);
}
async transaction<T>(fn: (tx: Database & { rollback: () => never }) => Promise<T>): Promise<T> {
return this.db.transaction((tx) => fn(new Database(tx) as Database & { rollback: () => never }));
}
/* admins */
addAdmin = (values: AddAdminReq) => addAdmin(this.db, values);
editAdmin = (values: EditAdminReq) => editAdmin(this.db, values);
deleteAdmin = (values: DeleteAdminReq) => deleteAdmin(this.db, values);
getAdmins = (values: GetAdminReq) => getAdmins(this.db, values);
existsAdmin = (values: ExistsAdminReq) => existsAdmin(this.db, values);
/* user */
addUser = (values: AddUserReq) => addUser(this.db, values);
editUser = (values: EditUserReq) => editUser(this.db, values);
deleteUser = (values: DeleteUserReq) => deleteUser(this.db, values);
existsUser = (values: ExistsUserReq) => existsUser(this.db, values);
getUsers = (values: GetUsersReq) => getUsers(this.db, values);
getUserById = (values: GetUserByIdReq) => getUserById(this.db, values);
getUserByUsername = (values: GetUserByUsernameReq) => getUserByUsername(this.db, values);
getUserByUuid = (values: GetUserByUuidReq) => getUserByUuid(this.db, values);
/* user blocks */
addBlockedUser = (values: AddBlockedUserReq) => addBlockedUser(this.db, values);
editBlockedUser = (values: EditBlockedUserReq) => editBlockedUser(this.db, values);
deleteBlockedUser = (values: DeleteBlockedUserReq) => deleteBlockedUser(this.db, values);
getBlockedUserByUuid = (values: GetBlockedUserByUuidReq) => getBlockedUserByUuid(this.db, values);
getBlockedUsers = (values: GetBlockedUsersReq) => getBlockedUsers(this.db, values);
/* report */
addReport = (values: AddReportReq) => addReport(this.db, values);
editReport = (values: EditReportReq) => editReport(this.db, values);
submitReport = (values: SubmitReportReq) => submitReport(this.db, values);
getReports = (values: GetReportsReq) => getReports(this.db, values);
getReportById = (values: GetReportById) => getReportById(this.db, values);
getReportByUrlHash = (values: GetReportByUrlHash) => getReportByUrlHash(this.db, values);
/* report attachment */
addReportAttachment = (values: AddReportAttachmentReq) => addReportAttachment(this.db, values);
getReportAttachments = (values: GetReportAttachmentsReq) => getReportAttachments(this.db, values);
/* report status */
getReportStatus = (values: GetReportStatusReq) => getReportStatus(this.db, values);
editReportStatus = (values: EditReportStatusReq) => editReportStatus(this.db, values);
/* strike reason */
addStrikeReason = (values: AddStrikeReasonReq) => addStrikeReason(this.db, values);
editStrikeReason = (values: EditStrikeReasonReq) => editStrikeReason(this.db, values);
deleteStrikeReason = (values: DeleteStrikeReasonReq) => deleteStrikeReason(this.db, values);
getStrikeReasons = (values: GetStrikeReasonsReq) => getStrikeReasons(this.db, values);
/* strikes */
editStrike = (values: EditStrikeReq) => editStrike(this.db, values);
deleteStrike = (values: DeleteStrikeReq) => deleteStrike(this.db, values);
getStrikeByReportId = (values: GetStrikeByReportIdReq) => getStrikeByReportId(this.db, values);
getStrikesByUserId = (values: GetStrikesByUserIdReq) => getStrikesByUserId(this.db, values);
/* feedback */
addFeedback = (values: AddFeedbackReq) => addFeedback(this.db, values);
addUserFeedbacks = (values: AddUserFeedbacksReq) => addUserFeedbacks(this.db, values);
submitFeedback = (values: SubmitFeedbackReq) => submitFeedback(this.db, values);
getFeedbacks = (values: GetFeedbacksReq) => getFeedbacks(this.db, values);
getFeedbackByUrlHash = (values: GetFeedbackByUrlHash) => getFeedbackByUrlHash(this.db, values);
/* settings */
getSettings = (values: GetSettingsReq) => getSettings(this.db, values);
setSettings = (values: SetSettingsReq) => setSettings(this.db, values);
getSetting = (values: GetSettingReq) => getSetting(this.db, values);
}
export const db = await Database.init(DATABASE_URI);

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