First commit

This commit is contained in:
2026-03-18 22:42:33 +09:00
commit 50657066a6
64 changed files with 5290 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"name": "backend",
"main": "dist/index.js",
"type": "module",
"scripts": {
"start": "cross-env NODE_ENV=production node .",
"build": "tsc && tsc-alias",
"dev": "tsx watch src/index.ts",
"mikro": "tsx ./node_modules/@mikro-orm/cli/esm.js"
},
"author": {
"name": "Last2014",
"email": "info@last2014.com",
"url": "https://about.last2014.com"
},
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.29.1",
"dependencies": {
"@fastify/static": "^9.0.0",
"@mikro-orm/core": "^6.6.7",
"@mikro-orm/migrations": "^6.6.7",
"@mikro-orm/postgresql": "^6.6.7",
"@mikro-orm/reflection": "^6.6.7",
"@types/node": "^25.2.2",
"argon2": "^0.44.0",
"fastify": "^5.7.4",
"fastify-plugin": "^5.1.0",
"fs": "0.0.1-security",
"os": "^0.1.2",
"tsc-alias": "^1.8.16",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"util": "^0.12.5",
"uuid": "^13.0.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
"devDependencies": {
"cross-env": "^10.1.0"
}
}
+32
View File
@@ -0,0 +1,32 @@
import type { ZodIssue } from "zod/v3";
type ErrorType<R = undefined> = {
bad: "client" | "server";
code: string;
message: string;
reason?: R,
}
export const ErrorBase = <R = undefined>(err: ErrorType<R>) => {return {
success: false,
error: err,
}}
export const DatabaseError = () =>
ErrorBase({
bad: "server",
code: "database_error",
message: "サーバーでデータベースの問題が発生しました。",
});
export const InputError = (issues: ZodIssue[]) =>
ErrorBase({
bad: "client",
code: "input_wrong",
message: "入力に問題があります。",
reason: issues.map(issue => ({
code: issue.code,
path: issue.path,
message: issue.message,
})),
});
+165
View File
@@ -0,0 +1,165 @@
import Fastify from "fastify";
import config from "@/lib/config";
import { accessSync, constants as fsConst } from "node:fs";
import staticStream from "@fastify/static";
import { styleText } from "node:util";
import Routes from "@/routes";
import Database from "@/lib/db";
import logger from "@/lib/logger";
import AccessLog from "@/lib/access";
import Authorization from "@/lib/auth";
import { RequestContext } from "@mikro-orm/core";
import { DatabaseError, ErrorBase } from "@/errors";
import { ConfigEntity } from "@/modules/entities/Config";
process.title = "Chat";
logger.info("Process started...");
if (process.env.NODE_ENV !== "production") {
process.title = "Chat backend";
logger.warn(styleText(["red", "bold", "bgYellow"], "Development environment avaiable!!"));
}
const fastify = Fastify({
logger: false,
trustProxy: config.server.trustProxy,
bodyLimit: 1024 * 100,
});
try {
fastify.setErrorHandler((err, req, res) => {
res.header("Content-Type", "application/json");
if (err instanceof SyntaxError && /JSON/.test(err.message)) {
return res.status(400).send(ErrorBase({
bad: "client",
code: "input_none",
message: "入力がありません。",
}));
}
logger.error("Unknown error: ", err);
return res.status(500).send(ErrorBase({
bad: "server",
code: "unknown_error",
message: "不明なエラーが発生しました。",
}));
});
if (process.env.NODE_ENV === "production") {
try {
accessSync(`${import.meta.dirname}/../../frontend/dist/index.html`, fsConst.R_OK);
await fastify.register(staticStream, {
root: `${import.meta.dirname}/../../frontend/dist`,
index: "/",
});
} catch (err) {
logger.error("Error: It's in production but the frontend dist is not found.");
process.exit(1);
}
}
fastify.setNotFoundHandler((req, res) => {
if (req.url.startsWith("/api")) {
res.header("Content-Type", "application/json");
return res.code(404).send(ErrorBase({
bad: "client",
code: "endpoint_not_found",
message: "エンドポイントが見つかりませんでした。",
}));
}
if (process.env.NODE_ENV === "production") {
return res.sendFile("index.html");
}
return res.code(500).send();
});
await fastify.register(Database);
fastify.addHook("onRequest", (req, res, done) => {
RequestContext.create(fastify.orm.em, done);
});
fastify.addHook("onRequest", async (req, res) => {
if (
req.url.startsWith("/api") &&
!req.url.startsWith("/api/setup")
) {
try {
const configCount = await fastify.orm.em.count(ConfigEntity);
if (configCount === 0) {
return res.code(409).send(ErrorBase({
bad: "client",
code: "yet_initialization",
message: "初期設定が行われていません。",
}));
}
} catch (err) {
logger.error("Database Error: Could not check if already initialization:", err);
return res.code(500).send(DatabaseError());
}
}
});
await fastify.register(AccessLog);
await fastify.register(Authorization);
fastify.removeAllContentTypeParsers();
fastify.addContentTypeParser("application/json", { parseAs: "string" }, (req, body, done) => {
try {
const json = JSON.parse(
typeof body === "string"
? body
: body.toString("utf-8")
);
done(null, json);
} catch (err) {
if (err instanceof Error) {
(err as any).statusCode = 400;
done(err, undefined);
} else {
done(new Error("Invalid JSON"), undefined);
}
}
});
await fastify.register(Routes, {
prefix: "/api",
});
const addr = await fastify.listen({
port: config.server.port,
});
if (process.env.NODE_ENV === "production") {
logger.info(`Listening at ${addr}`);
} else {
logger.info(`Backend listening at ${addr}`);
}
} catch (err) {
logger.error(err);
process.exit(1);
}
const shutdown = async () => {
try {
await fastify.close();
logger.log("Server downed.");
process.exit(0);
} catch (err) {
logger.error("Server down failed", err);
process.exit(1);
}
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
+31
View File
@@ -0,0 +1,31 @@
import type { FastifyPluginCallback } from "fastify";
import logger from "@/lib/logger";
import fp from "fastify-plugin";
declare module "fastify" {
interface FastifyRequest {
startTime: bigint;
}
}
const AccessLog: FastifyPluginCallback = (fastify) => {
fastify.addHook("onRequest", (req, res, done) => {
req.startTime = process.hrtime.bigint();
logger.info(`${req.method} ${req.url} from ${req.ip}`);
done();
});
fastify.addHook("onResponse", (req, res, done) => {
const duration = (Number(
process.hrtime.bigint() - req.startTime
) / 1_000_000).toFixed(2);
logger.info(`${req.method} ${req.url} ${res.statusCode} from ${req.ip} - ${duration}ms`);
done();
});
}
export default fp(AccessLog);
+36
View File
@@ -0,0 +1,36 @@
import type { FastifyPluginCallback } from "fastify";
import fp from "fastify-plugin";
import { TokenEntity } from "@/modules/entities/Token";
import logger from "./logger";
import { DatabaseError, ErrorBase } from "@/errors";
declare module "fastify" {
interface FastifyRequest {
token: TokenEntity | ReturnType<typeof ErrorBase>;
}
}
const Authorization: FastifyPluginCallback = (fastify) => {
fastify.addHook("onRequest", async (req, res) => {
const token = req.headers["authorization"];
if (typeof token !== "string") {
return req.token = ErrorBase({
bad: "client",
code: "token_none",
message: "トークンが設定されていません。",
});
}
try {
const result = await fastify.orm.em.getRepository(TokenEntity).authToken(token);
req.token = result;
} catch (err) {
logger.error("Database Error: Token authorization failed:", err);
return res.code(500).send(DatabaseError());
}
});
}
export default fp(Authorization);
+59
View File
@@ -0,0 +1,59 @@
import z from "zod/v3";
import { readFileSync } from "node:fs";
import { parse as yamlParse } from "yaml";
import { EOL } from "node:os";
import logger from "@/lib/logger";
const schema = z.object({
server: z.object({
port: z.number().min(0).max(65535),
host: z.string().ip(),
trustProxy: z.union([
z.string(),
z.array(z.string()),
z.boolean(),
z.function()
.args(z.string(), z.number())
.returns(z.boolean()),
]),
}),
database: z.object({
user: z.string(),
password: z.string(),
host: z.string(),
port: z.number().min(0).max(65535),
database: z.string(),
}),
});
const config = (() => {
try {
const configFile = readFileSync(
`${import.meta.dirname}/../../../../config/config.yaml`,
"utf-8",
);
const configObj = yamlParse(configFile);
const result = schema.safeParse(configObj);
if (!result.success) {
const issues = result.error.issues.map(i => ` ${i.message}`);
logger.error(`Config file:${EOL}${issues.join(EOL)}`);
process.exit(1);
}
return result.data;
} catch (err) {
if (
err instanceof Error &&
err.message.includes("no such file or directory, open")
) {
logger.error("Config file: Config file is not found.");
} else {
logger.error(`Config file: ${err}`);
}
process.exit(1);
}
})();
export default config;
+33
View File
@@ -0,0 +1,33 @@
import type { FastifyPluginAsync } from "fastify";
import fp from "fastify-plugin";
import { MikroORM } from "@mikro-orm/postgresql";
import config from "@/mikro-orm.config";
import logger from "@/lib/logger";
declare module "fastify" {
interface FastifyInstance {
orm: MikroORM;
}
}
const Database: FastifyPluginAsync = async (fastify) => {
const orm = await MikroORM.init(config);
if (process.env.NODE_ENV !== "production") {
try {
await orm.schema.updateSchema();
logger.info("Database migration completed.");
} catch (err) {
logger.error("Migration failed:", err);
process.exit(1);
}
}
fastify.decorate("orm", orm);
fastify.addHook("onClose", async () => {
await orm.close(true);
});
};
export default fp(Database);
@@ -0,0 +1,121 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"name": {
"name": "name",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"mappedType": "string"
},
"value": {
"name": "value",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"mappedType": "string"
}
},
"name": "config",
"schema": "public",
"indexes": [
{
"keyName": "config_pkey",
"columnNames": [
"name"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{
"columns": {
"userid": {
"name": "userid",
"type": "varchar(20)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 20,
"mappedType": "string"
},
"username": {
"name": "username",
"type": "varchar(30)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 30,
"mappedType": "string"
},
"email": {
"name": "email",
"type": "varchar(254)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 254,
"mappedType": "string"
},
"password": {
"name": "password",
"type": "varchar(60)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 60,
"mappedType": "string"
}
},
"name": "user",
"schema": "public",
"indexes": [
{
"columnNames": [
"email"
],
"composite": false,
"keyName": "user_email_unique",
"constraint": true,
"primary": false,
"unique": true
},
{
"keyName": "user_pkey",
"columnNames": [
"userid"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
}
],
"nativeEnums": {}
}
@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260302212455 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "user" add column "password" varchar(60) not null;`);
}
override async down(): Promise<void> {
this.addSql(`alter table "user" drop column "password";`);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { defineConfig } from "@mikro-orm/postgresql";
import config from "@/lib/config";
import { TsMorphMetadataProvider } from "@mikro-orm/reflection";
import { Migrator } from "@mikro-orm/migrations";
import logger from "@/lib/logger";
export default defineConfig({
entities: ["./dist/modules/entities/**/*.js"],
entitiesTs: ["./src/modules/entities/**/*.ts"],
migrations: {
path: "./dist/migrations",
pathTs: "./src/migrations",
allOrNothing: true,
transactional: true,
disableForeignKeys: false,
},
extensions: [Migrator],
metadataProvider: TsMorphMetadataProvider,
debug: process.env.NODE_ENV !== "production",
logger: (message: string) => {
logger.log(`[MikroORM] ${message}`);
},
dbName: config.database.database,
user: config.database.user,
password: config.database.password,
host: config.database.host,
port: config.database.port,
});
@@ -0,0 +1,10 @@
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
@Entity({ tableName: "config" })
export class ConfigEntity {
@PrimaryKey({ type: "string" })
name!: string;
@Property({ type: "text" })
value!: string;
}
@@ -0,0 +1,31 @@
import { Entity, EntityRepositoryType, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserEntity } from "@/modules/entities/User";
import { TokenRepository } from "@/modules/repositories/Token";
@Entity({
tableName: "token",
repository: () => TokenRepository,
})
export class TokenEntity {
[EntityRepositoryType]?: TokenRepository;
[OptionalProps]?: "createdAt";
@PrimaryKey({ type: "string", length: 64 })
name!: string;
@Property({ type: "text" })
passphrase!: string;
@ManyToOne(() => UserEntity)
@Index()
user!: UserEntity;
@Property()
isNative!: boolean;
@Property({ onCreate: () => new Date() })
createdAt!: Date;
@Property({ nullable: true })
lastUsedAt?: Date;
}
@@ -0,0 +1,36 @@
import { Entity, EntityRepositoryType, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserRepository } from "@/modules/repositories/User";
import { v4 as uuid } from "uuid";
@Entity({
tableName: "user",
repository: () => UserRepository,
})
export class UserEntity {
[EntityRepositoryType]?: UserRepository;
[OptionalProps]?: "uuid" | "isSuspended" | "createdAt";
@PrimaryKey({ length: 36 })
uuid: string = uuid();
@Property({ unique: true, length: 20 })
userid!: string;
@Property({ length: 30 })
username!: string;
@Property({ unique: true, length: 256 })
email!: string;
@Property({ type: "text" })
password!: string;
@Property({ default: false })
isAdmin: boolean = false;
@Property({ default: false })
isSuspended: boolean = false;
@Property({ onCreate: () => new Date() })
createdAt!: Date;
}
@@ -0,0 +1,68 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import type { TokenEntity } from "@/modules/entities/Token";
import { hash, argon2id, verify as argon2Verify } from "argon2";
import { randomBytes } from "node:crypto";
import { UserEntity } from "@/modules/entities/User";
import { ErrorBase } from "@/errors";
export class TokenRepository extends EntityRepository<TokenEntity> {
async createToken(user: UserEntity, isNative: boolean) {
const name = randomBytes(32).toString("hex");
const passphrase = randomBytes(32).toString("hex");
const hashed = await hash(passphrase, {
type: argon2id,
memoryCost: 2 ** 16,
timeCost: 3,
parallelism: 1,
});
const token = this.create({
name,
passphrase: hashed,
isNative,
user,
});
await this.em.persist(token).flush();
return `${name}_${passphrase}`;
}
async authToken(
tokenStr: string
): Promise<TokenEntity | ReturnType<typeof ErrorBase>> {
const tokenArr = tokenStr.split("_");
if (
tokenArr[0]?.length !== 64 ||
tokenArr[1]?.length !== 64
)
return ErrorBase({
bad: "client",
code: "token_length_wrong",
message: "トークンの文字数が不正です。",
});
const token = await this.findOne({ name: tokenArr[0] }, { populate: ["user"] });
if (!token)
return ErrorBase({
bad: "client",
code: "token_invalid",
message: "トークンが不正です。",
});
const isOk = await argon2Verify(token.passphrase, tokenArr[1]);
if (!isOk)
return ErrorBase({
bad: "client",
code: "token_invalid",
message: "トークンが不正です。",
});
token.lastUsedAt = new Date()
await this.em.persist(token).flush();
return token;
}
}
@@ -0,0 +1,55 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { UserEntity } from "@/modules/entities/User";
import { hash, argon2id, verify as argon2Verify } from "argon2";
import z from "zod/v3";
import EmailRegex from "@/regexs/email";
export class UserRepository extends EntityRepository<UserEntity> {
public static schema = z.object({
userid: z.string().trim().min(3).max(20),
username: z.string().trim().min(3).max(30),
email: z.string().min(6).trim().max(254).regex(EmailRegex),
password: z.string().trim().min(8),
isAdmin: z.boolean(),
});
async createUser(data: z.infer<typeof UserRepository.schema>) {
const hashed = await hash(data.password, {
type: argon2id,
memoryCost: 2 ** 16,
timeCost: 3,
parallelism: 1,
});
const user = this.create({
...data,
password: hashed,
});
await this.em.persist(user).flush();
return;
}
async findByUserId(userid: string) {
return this.findOne({ userid });
}
async findByEmail(email: string) {
return this.findOne({ email });
}
async authUser(data: {
userid: string;
password: string;
}) {
const user = await this.findByUserId(data.userid);
if (!user)
return "userid_wrong";
const isOk = await argon2Verify(user.password, data.password);
if (!isOk)
return "password_wrong";
return user;
}
}
+3
View File
@@ -0,0 +1,3 @@
const EmailRegex = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:([0-9]{1,3}\.){3}[0-9]{1,3})\])$/i;
export default EmailRegex;
+18
View File
@@ -0,0 +1,18 @@
import type { FastifyInstance } from "fastify";
import Setup from "./setup";
import Primary from "./primary";
import Me from "./me";
export default async function Routes(fastify: FastifyInstance) {
await fastify.register(Setup, {
prefix: "/setup",
});
await fastify.register(Primary, {
prefix: "/primary",
});
await fastify.register(Me, {
prefix: "/me",
});
}
+11
View File
@@ -0,0 +1,11 @@
import type { FastifyInstance } from "fastify";
export default async function Me(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
if ("error" in req.token)
return res.code(400).send(req.token);
const { password, email, ...safeUser } = req.token.user;
return res.send(safeUser);
});
}
@@ -0,0 +1,13 @@
import type { FastifyInstance } from "fastify";
import SignUp from "./signup";
import SignIn from "./signin";
export default async function Primary(fastify: FastifyInstance) {
await fastify.register(SignUp, {
prefix: "/signup",
});
await fastify.register(SignIn, {
prefix: "/signin",
});
}
@@ -0,0 +1,46 @@
import { DatabaseError, ErrorBase, InputError } from "@/errors";
import logger from "@/lib/logger";
import { TokenEntity } from "@/modules/entities/Token";
import { UserEntity } from "@/modules/entities/User";
import { UserRepository } from "@/modules/repositories/User";
import type { FastifyInstance } from "fastify";
export default function SignIn(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
const schema = UserRepository.schema.pick({
userid: true,
password: true,
});
const result = schema.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const user = await fastify.orm.em.getRepository(UserEntity).authUser(result.data);
if (typeof user === "string") {
return res.code(400).send(ErrorBase({
bad: "client",
code: "auth_input_wrong",
message: "ユーザー名かパスワードが違います。",
}));
}
const token = await fastify.orm.em.getRepository(TokenEntity).createToken(user, true);
return res.send({
success: true,
token: token,
});
} catch (err) {
logger.error("Database Error: User auth or token create failed.", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -0,0 +1,30 @@
import { UserEntity } from "@/modules/entities/User";
import logger from "@/lib/logger";
import type { FastifyInstance } from "fastify";
import { UserRepository } from "@/modules/repositories/User";
import { DatabaseError, InputError } from "@/errors";
export default function SignUp(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
const result = UserRepository.schema.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
await fastify.orm.em.getRepository(UserEntity).createUser(result.data);
return res.code(200).send({
success: true,
});
} catch (err) {
logger.error("Database Error: User create failed.", err);
return res.code(500).send(DatabaseError());
}
});
}
+18
View File
@@ -0,0 +1,18 @@
import { ConfigEntity } from "@/modules/entities/Config";
import { UserEntity } from "@/modules/entities/User";
import type { FastifyInstance } from "fastify";
export default async function ServerInfo(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
const config = fastify.orm.em.getRepository(ConfigEntity);
const user = fastify.orm.em.getRepository(UserEntity);
const configCount = await config.count();
const userCount = await user.count();
return res.send({
isInitialized: configCount > 0,
isFirstAdminExists: userCount > 0,
userCount,
});
});
}
+67
View File
@@ -0,0 +1,67 @@
import { UserEntity } from "@/modules/entities/User";
import logger from "@/lib/logger";
import type { FastifyInstance } from "fastify";
import { UserRepository } from "@/modules/repositories/User";
import { DatabaseError, ErrorBase, InputError } from "@/errors";
import { ConfigEntity } from "@/modules/entities/Config";
export default function CreateAdmin(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
const result = UserRepository.schema.omit({ isAdmin: true }).safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const configCount = await fastify.orm.em.count(ConfigEntity);
if (configCount === 0) {
return res.code(409).send(ErrorBase({
bad: "client",
code: "yet_initialization",
message: "初期設定が行われていません。",
}));
}
} catch (err) {
logger.error("Database Error: Could not check if already initialization:", err);
return res.code(500).send(DatabaseError());
}
try {
const userCount = await fastify.orm.em.getRepository(UserEntity).count();
if (userCount > 0) {
return res.code(409).send(ErrorBase({
bad: "client",
code: "first_admin_already_exists",
message: "最初の管理者ユーザーは既に存在します。",
}));
}
} catch (err) {
logger.error("Database Error: Could not check if administrator exists:", err);
return res.code(500).send(DatabaseError());
}
try {
await fastify.orm.em.getRepository(UserEntity).createUser({
...result.data,
isAdmin: true,
});
logger.warn("First administrator account has been created.")
return res.code(200).send({
success: true,
});
} catch (err) {
logger.error("Database Error: Admin user create failed.", err);
return res.code(500).send(DatabaseError());
}
});
}
+13
View File
@@ -0,0 +1,13 @@
import type { FastifyInstance } from "fastify";
import CreateAdmin from "./create-admin";
import Initialization from "./initialization";
export default async function Setup(fastify: FastifyInstance) {
await fastify.register(Initialization, {
prefix: "/initialization",
});
await fastify.register(CreateAdmin, {
prefix: "/create-admin",
});
}
@@ -0,0 +1,70 @@
import { DatabaseError, ErrorBase, InputError } from "@/errors";
import logger from "@/lib/logger";
import { ConfigEntity } from "@/modules/entities/Config";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
export default function Initialization(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
const bodySchema = z.object({
name: z.string().trim().min(1).max(20),
description: z.string().trim().min(1),
requiredInvitationCode: z.boolean(),
force: z.literal("use_force_initialization").refine(() => process.env.NODE_ENV !== "production").optional(),
});
const result = bodySchema.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
if (result.data.force) {
await fastify.orm.em.nativeDelete(ConfigEntity, {});
fastify.orm.em.clear();
}
const configCount = await fastify.orm.em.count(ConfigEntity);
if (configCount > 0) {
return res.code(409).send(ErrorBase({
bad: "client",
code: "already_initialization",
message: "既に初期設定が行われています。",
}));
}
} catch (err) {
logger.error("Database Error: Could not check if yet initialization:", err);
return res.code(500).send(DatabaseError());
}
try {
const entries = Object.entries(result.data).filter(([key]) => key !== "force");
for (const [key, value] of entries) {
const entity = fastify.orm.em.create(ConfigEntity, {
name: key,
value: typeof value === "string"
? value
: String(value),
});
fastify.orm.em.persist(entity);
}
await fastify.orm.em.flush();
return res.code(200).send({
success: true,
});
} catch (err) {
logger.error("Database Error: Server initialization failed:", err);
return res.code(500).send(DatabaseError());
}
});
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"baseUrl": "./",
"typeRoots": [
"./node_modules/@types",
"./src/types",
],
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
"outDir": "./dist",
"paths": {
"@/*": ["./src/*"],
},
"removeComments": true,
},
"tsc-alias": {
"resolveFullPaths": true,
},
}