Chg: exactOptionalPropertyTypesをfalseに変更 / Chg(Security): トークンが不正な場合のエラーを全てtoken_invalidに変更 / New: channelテーブル・リポジトリ / Chg: configテーブルのvalueをstringに / Chg: configテーブルのlengthを4096に / New: messageテーブル / Chg: 安全のためuserテーブルのOptionalPropsにidを追加 / New: channel/createエンドポイント / New: channel/listエンドポイント / New: channel/editエンドポイント / Enhance: primary/signupエンドポイントの重複エラーの実装で末尾カンマなどの改善 / Chg: setup/initializationのdescriptionに最大文字数4096を制定 / Chg: serverInfoをdefault exportからexportに変更 / New: フロントエンドでmeを読み込み / New: フロントエンドでchannelを読み込み / New: client.tsでトークンがある場合はトークンを指定 / Chg: clientをrefに / Del: IndexedDBからserverテーブルを削除 / Fix: Dexieのclassに命名 / Feat: フロントエンドでのサインインページ / Fix: L.jsで任意のbodyがあるエンドポイントが定義できない問題を修正 / Del: L.jsのserver-infoでの不要なimportを削除 / Fix: L.jsでトークンのエラーを追加 / Fix: L.jsのUserSchemaにlastUsedAtを追加

This commit is contained in:
2026-03-30 11:37:57 +09:00
parent 6b54ae4306
commit d129c95aa4
33 changed files with 683 additions and 39 deletions
+3 -3
View File
@@ -16,10 +16,10 @@ const Authorization: FastifyPluginCallback = (fastify) => {
fastify.addHook("onRequest", async (req, res) => {
let token = req.headers["authorization"];
if (typeof token !== "string") {
return req.token = ErrorBase({
return ErrorBase({
bad: "client",
code: "token_none",
message: "トークンが設定されていません。",
code: "token_invalid",
message: "トークンが不正です。",
});
}
@@ -0,0 +1,43 @@
import generateUniqueId from "@/lib/id";
import { Entity, EntityRepositoryType, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserEntity } from "@/modules/entities/User";
import { ChannelRepository } from "@/modules/repositories/Channel";
@Entity({
tableName: "channel",
repository: () => ChannelRepository,
})
export class ChannelEntity {
[EntityRepositoryType]?: ChannelRepository;
[OptionalProps]?: "id" | "createdAt";
@PrimaryKey({
type: "string",
length: 10,
onCreate: () => generateUniqueId(),
})
id!: string;
@Property({
type: "string",
length: 20,
unique: true,
})
@Index()
name!: string;
@Property({
type: "string",
length: 4096,
})
description!: string;
@ManyToOne(() => UserEntity)
createdBy!: UserEntity;
@Property({
type: "datetime",
onCreate: () => new Date(),
})
createdAt!: Date;
}
@@ -11,6 +11,9 @@ export class ConfigEntity {
@PrimaryKey({ type: "string" })
name!: string;
@Property({ type: "text" })
@Property({
type: "string",
length: 4096,
})
value!: string;
}
@@ -0,0 +1,36 @@
import generateUniqueId from "@/lib/id";
import { Entity, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserEntity } from "@/modules/entities/User";
import { ChannelEntity } from "@/modules/entities/Channel";
@Entity({
tableName: "message",
})
export class MessageEntity {
[OptionalProps]?: "id" | "createdAt";
@PrimaryKey({
type: "string",
length: 10,
onCreate: () => generateUniqueId(),
})
id!: string;
@Property({
type: "string",
length: 4096,
})
message!: string;
@ManyToOne(() => ChannelEntity)
channel!: ChannelEntity;
@ManyToOne(() => UserEntity)
createdBy!: UserEntity;
@Property({
type: "datetime",
onCreate: () => new Date(),
})
createdAt!: Date;
}
@@ -8,7 +8,7 @@ import generateUniqueId from "@/lib/id";
})
export class UserEntity {
[EntityRepositoryType]?: UserRepository;
[OptionalProps]?: "profile" | "isSuspended" | "createdAt";
[OptionalProps]?: "id" | "profile" | "isSuspended" | "createdAt";
@PrimaryKey({
type: "string",
@@ -0,0 +1,65 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import type { ChannelEntity } from "@/modules/entities/Channel";
import { ErrorBase } from "@/errors";
import z from "zod/v3";
import { UserRepository } from "@/modules/repositories/User";
export class ChannelRepository extends EntityRepository<ChannelEntity> {
public static schema = z.object({
name: z.string().trim().min(1).max(20),
description: z.string().trim().min(1).max(4096),
userid: UserRepository.schema.shape.userid,
});
async findChannel(limit: number = 20, sinceData?: string) {
let since = sinceData ?? new Date();
if (
sinceData &&
!isNaN(new Date(sinceData).getTime())
) {
const itChannel = await this.findOne({ id: sinceData });
if (!itChannel) {
return ErrorBase({
bad: "client",
code: "channel_not_found",
message: "対象のチャンネルが見つかりませんでした。",
});
}
since = itChannel.createdAt;
}
const findResult = await this.find({
createdAt: {
$lt: since,
},
}, {
orderBy: {
createdAt: "DESC",
},
limit: limit,
});
return findResult ?? [];
}
async createChannel(data: z.infer<typeof ChannelRepository.schema>) {
const channel = this.create({
...data,
createdBy: data.userid,
});
await this.em.persist(channel).flush();
return channel.id;
}
async editChannel(
target: ChannelEntity,
data: Partial<Omit<z.infer<typeof ChannelRepository.schema>, "userid">>
) {
await this.nativeUpdate(target, data);
return target.id;
}
}
@@ -52,9 +52,9 @@ export class TokenRepository extends EntityRepository<TokenEntity> {
)
return ErrorBase({
bad: "client",
code: "token_length_wrong",
message: "トークンの文字数が不正です。",
});
code: "token_invalid",
message: "トークンが不正です。",
})
const token = await this.findOne({ name: tokenArr[0] }, { populate: ["user"] });
if (!token)
@@ -0,0 +1,55 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { ChannelEntity } from "@/modules/entities/Channel";
import { ChannelRepository } from "@/modules/repositories/Channel";
import type { FastifyInstance } from "fastify";
export default async function ChannelCreate(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | channel/create");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
if ("error" in req.token)
return res.code(400).send(req.token);
const result = ChannelRepository.schema.safeParse({
...req.body as any,
userid: req.token.user.userid,
});
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const channelRepo = fastify.orm.em.getRepository(ChannelEntity);
const id = await channelRepo.createChannel(result.data);
return res.send({
success: true,
id,
});
} catch (err: any) {
if (err.name === "UniqueConstraintViolationException") {
const duplicate = err.constraint.replace("channel_", "").replace("_unique", "");
if (duplicate !== "id") {
return res.code(400).send(InputError([
{
validation: "regex",
code: "invalid_string",
message: "Duplicate",
path: [duplicate],
},
]));
}
}
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -0,0 +1,52 @@
import { DatabaseError, ErrorBase, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { ChannelEntity } from "@/modules/entities/Channel";
import { ChannelRepository } from "@/modules/repositories/Channel";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
export default async function ChannelEdit(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | channel/edit");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
if ("error" in req.token)
return res.code(400).send(req.token);
const result = ChannelRepository.schema
.omit({ userid: true }).partial()
.merge(z.object({ id: z.string().length(10) }))
.refine(data =>
!Object.keys(data).length
).safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const channelRepo = fastify.orm.em.getRepository(ChannelEntity);
const itChannel = await channelRepo.findOne({ id: result.data.id });
if (!itChannel) {
return res.code(400).send(ErrorBase({
bad: "client",
code: "channel_not_found",
message: "対象のチャンネルが見つかりませんでした。",
}));
}
const id = await channelRepo.editChannel(itChannel, result.data);
return res.send({
success: true,
id,
});
} catch (err) {
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -0,0 +1,18 @@
import type { FastifyInstance } from "fastify";
import ChannelList from "./list";
import ChannelCreate from "./create";
import ChannelEdit from "./edit";
export default async function Setup(fastify: FastifyInstance) {
await fastify.register(ChannelList, {
prefix: "/list",
});
await fastify.register(ChannelCreate, {
prefix: "/create",
});
await fastify.register(ChannelEdit, {
prefix: "/edit",
});
}
@@ -0,0 +1,49 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { ChannelEntity } from "@/modules/entities/Channel";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
export default async function ChannelList(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | channel/list");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
if ("error" in req.token)
return res.code(400).send(req.token);
const bodySchema = z.object({
limit: z.number().optional(),
since: z.string().optional(),
});
const result = bodySchema.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const channelRepo = fastify.orm.em.getRepository(ChannelEntity);
const findResult = await channelRepo.findChannel(
result.data.limit,
result.data.since,
) ?? [];
if ("error" in findResult) {
return res.code(400).send(findResult);
}
return res.send({
success: true,
channels: findResult,
});
} catch (err) {
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -40,9 +40,9 @@ export default function PrimarySignUp(fastify: FastifyInstance) {
validation: "regex",
code: "invalid_string",
message: "Duplicate",
path: [duplicate]
}
]))
path: [duplicate],
},
]));
}
}
@@ -13,7 +13,7 @@ export default function SetupInitialization(fastify: FastifyInstance) {
const bodySchema = z.object({
name: z.string().trim().min(1).max(20),
description: z.string().trim().min(1),
description: z.string().trim().min(1).max(4096),
requiredInvitationCode: z.boolean(),
force: z.literal("use_force_initialization").refine(() => process.env.NODE_ENV !== "production").optional(),
});
+1 -1
View File
@@ -10,7 +10,7 @@
"./src/types",
],
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"exactOptionalPropertyTypes": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,