First commit
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
});
|
||||
Executable
+165
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Executable
+59
@@ -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;
|
||||
Executable
+33
@@ -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";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Executable
+18
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
Executable
+13
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user