Chg: トークンの入力形式をBearerに変更 / Feat: ユニークなIDを生成する関数を作成 / Feat: userテーブルにプロフィールを作成 / Chg: ユーザー作成でのユーザー名を任意に変更 / Fix: meエンドポイントで成功時にもsuccessをレスポンスに含める / Fix: signupエンドポイントで入力が重複したときにデータベースエラーではなく入力エラーとしてレスポンス / Fix: setup/initializationエンドポイントでデータベースエラーが出る問題を修正 / Fix: L.jsの型を実際の値に更新

This commit is contained in:
2026-03-20 17:44:46 +09:00
parent dc04949e36
commit 0e615faa7f
12 changed files with 89 additions and 17 deletions
+13 -1
View File
@@ -14,7 +14,7 @@ const logger = new Logger("Lib | auth.ts");
const Authorization: FastifyPluginCallback = (fastify) => { const Authorization: FastifyPluginCallback = (fastify) => {
fastify.addHook("onRequest", async (req, res) => { fastify.addHook("onRequest", async (req, res) => {
const token = req.headers["authorization"]; let token = req.headers["authorization"];
if (typeof token !== "string") { if (typeof token !== "string") {
return req.token = ErrorBase({ return req.token = ErrorBase({
bad: "client", bad: "client",
@@ -23,6 +23,18 @@ const Authorization: FastifyPluginCallback = (fastify) => {
}); });
} }
if (!token.startsWith("Bearer ")) {
req.token = ErrorBase({
bad: "client",
code: "token_invalid",
message: "トークンが不正です。",
});
return;
}
token = token.replace("Bearer ", "");
try { try {
const result = await fastify.orm.em.getRepository(TokenEntity).authToken(token); const result = await fastify.orm.em.getRepository(TokenEntity).authToken(token);
+13
View File
@@ -0,0 +1,13 @@
import { randomBytes } from "node:crypto";
export default function generateUniqueId(length = 10) {
const guide = Math.ceil(length * 5 / 8);
const bytes = randomBytes(guide);
const bigint = BigInt("0x" + bytes.toString("hex"));
let id = bigint.toString(36);
id = id.padStart(length, "0")
id = id.slice(-length);
return id;
}
+13 -5
View File
@@ -1,6 +1,6 @@
import { Entity, EntityRepositoryType, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; import { Entity, EntityRepositoryType, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserRepository } from "@/modules/repositories/User"; import { UserRepository } from "@/modules/repositories/User";
import { v4 as uuid } from "uuid"; import generateUniqueId from "@/lib/id";
@Entity({ @Entity({
tableName: "user", tableName: "user",
@@ -8,13 +8,14 @@ import { v4 as uuid } from "uuid";
}) })
export class UserEntity { export class UserEntity {
[EntityRepositoryType]?: UserRepository; [EntityRepositoryType]?: UserRepository;
[OptionalProps]?: "uuid" | "isSuspended" | "createdAt"; [OptionalProps]?: "profile" | "isSuspended" | "createdAt";
@PrimaryKey({ @PrimaryKey({
type: "string", type: "string",
length: 36 length: 10,
onCreate: () => generateUniqueId(),
}) })
uuid: string = uuid(); id!: string;
@Property({ @Property({
type: "string", type: "string",
@@ -29,6 +30,13 @@ export class UserEntity {
}) })
username!: string; username!: string;
@Property({
type: "string",
length: 4096,
default: "",
})
profile!: string;
@Property({ @Property({
type: "string", type: "string",
unique: true, unique: true,
@@ -52,7 +60,7 @@ export class UserEntity {
isSuspended: boolean = false; isSuspended: boolean = false;
@Property({ @Property({
type: "date", type: "datetime",
onCreate: () => new Date(), onCreate: () => new Date(),
}) })
createdAt!: Date; createdAt!: Date;
@@ -8,12 +8,13 @@ export class UserRepository extends EntityRepository<UserEntity> {
public static schema = z.object({ public static schema = z.object({
userid: z.string().trim().min(3).max(20), userid: z.string().trim().min(3).max(20),
username: z.string().trim().min(3).max(30), username: z.string().trim().min(3).max(30),
profile: z.string().max(4096).optional(),
email: z.string().min(6).trim().max(254).regex(EmailRegex), email: z.string().min(6).trim().max(254).regex(EmailRegex),
password: z.string().trim().min(8), password: z.string().trim().min(8),
isAdmin: z.boolean(), isAdmin: z.boolean(),
}); });
async createUser(data: z.infer<typeof UserRepository.schema>) { async createUser(data: Pick<z.infer<typeof UserRepository.schema>, "userid" | "email" | "password" | "isAdmin">) {
const hashed = await hash(data.password, { const hashed = await hash(data.password, {
type: argon2id, type: argon2id,
memoryCost: 2 ** 16, memoryCost: 2 ** 16,
@@ -23,6 +24,7 @@ export class UserRepository extends EntityRepository<UserEntity> {
const user = this.create({ const user = this.create({
...data, ...data,
username: data.userid,
password: hashed, password: hashed,
}); });
+4 -1
View File
@@ -6,6 +6,9 @@ export default async function Me(fastify: FastifyInstance) {
return res.code(400).send(req.token); return res.code(400).send(req.token);
const { password, email, ...safeUser } = req.token.user; const { password, email, ...safeUser } = req.token.user;
return res.send(safeUser); return res.send({
...safeUser,
success: true,
});
}); });
} }
+26 -3
View File
@@ -10,19 +10,42 @@ export default function SignUp(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => { fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json"); res.header("Content-Type", "application/json");
const result = UserRepository.schema.safeParse(req.body); const result = UserRepository.schema.pick({
userid: true,
email: true,
password: true,
}).safeParse(req.body);
if (!result.success) { if (!result.success) {
console.log(result.error.issues)
return res.code(400).send(InputError(result.error.issues)); return res.code(400).send(InputError(result.error.issues));
} }
try { try {
await fastify.orm.em.getRepository(UserEntity).createUser(result.data); await fastify.orm.em.getRepository(UserEntity).createUser({
...result.data,
isAdmin: false,
});
return res.code(200).send({ return res.code(200).send({
success: true, success: true,
}); });
} catch (err) { } catch (err: any) {
if (err.name === "UniqueConstraintViolationException") {
const duplicate = err.constraint.replace("user_", "").replace("_unique", "");
if (duplicate !== "id") {
return res.code(400).send(InputError([
{
validation: "regex",
code: "invalid_string",
message: "Duplicate",
path: [duplicate]
}
]))
}
}
logger.error("Database Error: User create failed.", err); logger.error("Database Error: User create failed.", err);
return res.code(500).send(DatabaseError()); return res.code(500).send(DatabaseError());
@@ -3,6 +3,7 @@ import Logger from "@/lib/logger";
import type { FastifyInstance } from "fastify"; import type { FastifyInstance } from "fastify";
import { UserRepository } from "@/modules/repositories/User"; import { UserRepository } from "@/modules/repositories/User";
import { DatabaseError, ErrorBase, InputError } from "@/errors"; import { DatabaseError, ErrorBase, InputError } from "@/errors";
import { UniqueConstraintViolationException } from "@mikro-orm/core";
export default function CreateAdmin(fastify: FastifyInstance) { export default function CreateAdmin(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | setup/create-admin"); const logger = new Logger("Endpoint | setup/create-admin");
@@ -10,7 +11,11 @@ export default function CreateAdmin(fastify: FastifyInstance) {
fastify.post("/", async (req, res) => { fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json"); res.header("Content-Type", "application/json");
const result = UserRepository.schema.omit({ isAdmin: true }).safeParse(req.body); const result = UserRepository.schema.pick({
userid: true,
email: true,
password: true,
}).safeParse(req.body);
if (!result.success) { if (!result.success) {
return res.code(400).send(InputError(result.error.issues)); return res.code(400).send(InputError(result.error.issues));
@@ -38,7 +43,7 @@ export default function CreateAdmin(fastify: FastifyInstance) {
isAdmin: true, isAdmin: true,
}); });
logger.warn("First administrator account has been created.") logger.warn("First administrator account has been created.");
return res.code(200).send({ return res.code(200).send({
success: true, success: true,
@@ -48,7 +48,7 @@ export default function Initialization(fastify: FastifyInstance) {
const entries = Object.entries(result.data).filter(([key]) => key !== "force"); const entries = Object.entries(result.data).filter(([key]) => key !== "force");
for (const [key, value] of entries) { for (const [key, value] of entries) {
const entity = fastify.orm.em.getRepository(ConfigEntity).set( const entity = await fastify.orm.em.getRepository(ConfigEntity).set(
key, key,
typeof value === "string" typeof value === "string"
? value ? value
+3 -1
View File
@@ -3,10 +3,12 @@ import ErrorBase from "../../modules/error";
import DatabaseError from "../../modules/error/database"; import DatabaseError from "../../modules/error/database";
import Success from "../../modules/response/success"; import Success from "../../modules/response/success";
import YetInitializationError from "../../modules/error/yet_init"; import YetInitializationError from "../../modules/error/yet_init";
import UserSchema from "../../modules/user";
export default interface Me { export default interface Me {
"me": { "me": {
body: never; body: never;
response: Success | DatabaseError | InputError | InputNoneError | YetInitializationError; response: (Success & Omit<UserSchema, "password" | "email">)
| DatabaseError | InputError | InputNoneError | YetInitializationError;
}; };
} }
@@ -7,7 +7,7 @@ import UserSchema from "../../modules/user";
export default interface PrimarySignup { export default interface PrimarySignup {
"primary/signup": { "primary/signup": {
body: UserSchema; body: Pick<UserSchema, "userid" | "email" | "password">;
response: Success | DatabaseError | InputError | InputNoneError | YetInitializationError; response: Success | DatabaseError | InputError | InputNoneError | YetInitializationError;
}; };
} }
@@ -7,7 +7,7 @@ import { UserSchema } from "../primary/signup";
export default interface SetupCreateAdmin { export default interface SetupCreateAdmin {
"setup/create-admin": { "setup/create-admin": {
body: UserSchema; body: Pick<UserSchema, "userid" | "email" | "password">;
response: Success | DatabaseError | ErrorBase<{ response: Success | DatabaseError | ErrorBase<{
bad: "client", bad: "client",
code: "first_admin_already_exists", code: "first_admin_already_exists",
@@ -1,6 +1,10 @@
export default interface UserSchema { export default interface UserSchema {
userid: string; userid: string;
username: string; username: string;
profile: string;
email: string; email: string;
password: string; password: string;
isSuspended: boolean;
isAdmin: boolean;
createdAt: string;
} }