Feat: メッセージの送受信 / New: ユーザーのiconプロパティ / New: logエンティティ・リポジトリ / Chg: コミュニティリポジトリのスキーマのiconをoptionalに / Del: 不要なimport / Fix: Vue起動前のindex.htmlの背景色をVueと同期 / Enhance: Service Workerを改善 / Fix: 最初に開いたページが動作しない問題 / Feat: 上部の通知モーダル / Feat: 閉じることができないエラーのモーダルに再読み込みボタンを追加 / Fix: はみ出す挙動などのCSSを修正
This commit is contained in:
@@ -22,12 +22,12 @@
|
||||
"@mikro-orm/postgresql": "^6.6.7",
|
||||
"@mikro-orm/reflection": "^6.6.7",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"lynqchat-js": "workspace:*",
|
||||
"argon2": "^0.44.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"fastify": "^5.7.4",
|
||||
"fastify-plugin": "^5.1.0",
|
||||
"fs": "0.0.1-security",
|
||||
"lynqchat-js": "workspace:*",
|
||||
"os": "^0.1.2",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const Pick = <T extends Object>(object: T, keys: (keyof T)[]) =>
|
||||
Object.entries(object)
|
||||
.filter(([key]) => keys.includes(key as keyof T))
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
import { LogRepository } from "@/modules/repositories/Log";
|
||||
import generateUniqueId from "@/lib/id";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
|
||||
@Entity({
|
||||
tableName: "log",
|
||||
repository: () => LogRepository,
|
||||
})
|
||||
export class LogEntity {
|
||||
[OptionalProps]?: "id" | "createdBy" | "createdAt";
|
||||
[EntityRepositoryType]?: LogRepository;
|
||||
|
||||
@PrimaryKey({
|
||||
type: "string",
|
||||
length: 10,
|
||||
onCreate: () => generateUniqueId(),
|
||||
})
|
||||
id!: string;
|
||||
|
||||
@Property({
|
||||
type: "string",
|
||||
length: 4096
|
||||
})
|
||||
text!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, {
|
||||
nullable: true,
|
||||
})
|
||||
createdBy?: UserEntity;
|
||||
|
||||
@Property({
|
||||
type: "datetime",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import generateUniqueId from "@/lib/id";
|
||||
import { Entity, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
import { ChannelEntity } from "@/modules/entities/Channel";
|
||||
import { MessageRepository } from "@/modules/repositories/Message";
|
||||
|
||||
@Entity({
|
||||
tableName: "message",
|
||||
repository: () => MessageRepository,
|
||||
})
|
||||
export class MessageEntity {
|
||||
[EntityRepositoryType]?: MessageRepository;
|
||||
[OptionalProps]?: "id" | "createdAt";
|
||||
|
||||
@PrimaryKey({
|
||||
|
||||
@@ -30,6 +30,12 @@ export class UserEntity {
|
||||
})
|
||||
username!: string;
|
||||
|
||||
@Property({
|
||||
type: "string",
|
||||
nullable: true,
|
||||
})
|
||||
icon?: string;
|
||||
|
||||
@Property({
|
||||
type: "string",
|
||||
length: 4096,
|
||||
|
||||
@@ -9,7 +9,7 @@ export class CommunityRepository extends EntityRepository<CommunityEntity> {
|
||||
name: z.string().trim().min(1).max(20),
|
||||
description: z.string().trim().min(1).max(4096),
|
||||
userid: UserRepository.schema.shape.userid,
|
||||
icon: z.string().url(),
|
||||
icon: z.string().url().optional(),
|
||||
});
|
||||
|
||||
async listCommunity(limit: number = 20, sinceData?: string) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
import type { LogEntity } from "@/modules/entities/Log";
|
||||
import z from "zod/v3";
|
||||
|
||||
export class LogRepository extends EntityRepository<LogEntity> {
|
||||
public static schema = z.string().min(1).max(4096);
|
||||
|
||||
async insertLog(text: string, userid?: string) {
|
||||
const log = this.create({
|
||||
text,
|
||||
createdBy: userid,
|
||||
});
|
||||
|
||||
await this.em.persist(log).flush();
|
||||
return log.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
import type { MessageEntity } from "@/modules/entities/Message";
|
||||
import z from "zod/v3";
|
||||
import { ErrorBase } from "@/errors";
|
||||
|
||||
export class MessageRepository extends EntityRepository<MessageEntity> {
|
||||
public static schema = z.object({
|
||||
message: z.string().trim().min(0).max(4096),
|
||||
channel: z.string().length(10),
|
||||
userid: z.string().length(10),
|
||||
});
|
||||
|
||||
async sendMessage(messageText: string, channel: string, user: string) {
|
||||
const message = this.create({
|
||||
message: messageText,
|
||||
channel,
|
||||
createdBy: user,
|
||||
});
|
||||
|
||||
await this.em.persist(message).flush();
|
||||
return message.id;
|
||||
}
|
||||
|
||||
async deleteMessage(id: string) {
|
||||
const message = this.getReference(id);
|
||||
await this.em.remove(message).flush();
|
||||
}
|
||||
|
||||
async listMessage(channel: string, limit: number = 20, untilData?: string) {
|
||||
let until = untilData ?? new Date();
|
||||
|
||||
if (
|
||||
untilData &&
|
||||
isNaN(new Date(untilData).getTime())
|
||||
) {
|
||||
const itMessage = await this.findOne({ id: untilData });
|
||||
|
||||
if (!itMessage) {
|
||||
return ErrorBase({
|
||||
bad: "client",
|
||||
code: "message_not_found",
|
||||
message: "対象のメッセージが見つかりませんでした。",
|
||||
});
|
||||
}
|
||||
|
||||
until = itMessage.createdAt;
|
||||
}
|
||||
|
||||
const findResult = await this.find({
|
||||
channel,
|
||||
createdAt: {
|
||||
$lt: until,
|
||||
},
|
||||
}, {
|
||||
orderBy: {
|
||||
createdAt: "DESC",
|
||||
},
|
||||
limit: limit,
|
||||
});
|
||||
|
||||
return findResult ?? [];
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ 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),
|
||||
icon: z.string().url().optional(),
|
||||
profile: z.string().max(4096).optional(),
|
||||
email: z.string().trim().min(6).max(254).regex(EmailRegex),
|
||||
password: z.string().trim().min(8),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DatabaseError, InputError } from "@/errors";
|
||||
import Logger from "@/lib/logger";
|
||||
import { CommunityEntity } from "@/modules/entities/Community";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import z from "zod/v3";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Me from "./me";
|
||||
import ServerInfo from "./server-info";
|
||||
import Community from "./community";
|
||||
import Channel from "./channel";
|
||||
import Message from "./message";
|
||||
|
||||
export default async function Routes(fastify: FastifyInstance) {
|
||||
await fastify.register(Setup, {
|
||||
@@ -30,4 +31,8 @@ export default async function Routes(fastify: FastifyInstance) {
|
||||
await fastify.register(Channel, {
|
||||
prefix: "/channel",
|
||||
});
|
||||
|
||||
await fastify.register(Message, {
|
||||
prefix: "/message",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DatabaseError, InputError } from "@/errors";
|
||||
import Logger from "@/lib/logger";
|
||||
import { MessageEntity } from "@/modules/entities/Message";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import z from "zod/v3";
|
||||
|
||||
export default async function MessageDelete(fastify: FastifyInstance) {
|
||||
const logger = new Logger("Endpoint | message/delete");
|
||||
|
||||
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 = z.object({
|
||||
id: z.string().length(10),
|
||||
}).safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.code(400).send(InputError(result.error.issues));
|
||||
}
|
||||
|
||||
try {
|
||||
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
|
||||
await messageRepo.deleteMessage(result.data.id);
|
||||
|
||||
return res.send({
|
||||
success: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Database Error:", err);
|
||||
|
||||
return res.code(500).send(DatabaseError());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import MessageList from "./list";
|
||||
import MessageSend from "./send";
|
||||
import MessageDelete from "./delete";
|
||||
|
||||
export default async function Message(fastify: FastifyInstance) {
|
||||
await fastify.register(MessageList, {
|
||||
prefix: "/list",
|
||||
});
|
||||
|
||||
await fastify.register(MessageSend, {
|
||||
prefix: "/send",
|
||||
});
|
||||
|
||||
await fastify.register(MessageDelete, {
|
||||
prefix: "/delete",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DatabaseError, InputError } from "@/errors";
|
||||
import Logger from "@/lib/logger";
|
||||
import { Pick } from "@/lib/object";
|
||||
import { MessageEntity } from "@/modules/entities/Message";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import z from "zod/v3";
|
||||
|
||||
export default async function MessageList(fastify: FastifyInstance) {
|
||||
const logger = new Logger("Endpoint | message/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(),
|
||||
until: z.string().optional(),
|
||||
channel: z.string().length(10),
|
||||
});
|
||||
|
||||
const result = bodySchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.code(400).send(InputError(result.error.issues));
|
||||
}
|
||||
|
||||
try {
|
||||
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
|
||||
|
||||
const findResult = await messageRepo.listMessage(
|
||||
result.data.channel,
|
||||
result.data.limit,
|
||||
result.data.until,
|
||||
);
|
||||
|
||||
if ("error" in findResult) {
|
||||
return res.code(400).send(findResult);
|
||||
}
|
||||
|
||||
return res.send({
|
||||
success: true,
|
||||
messages: findResult.map(message => ({
|
||||
...message,
|
||||
createdBy: Pick(message.createdBy, ["id", "userid", "username", "icon"]),
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Database Error:", err);
|
||||
|
||||
return res.code(500).send(DatabaseError());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DatabaseError, InputError } from "@/errors";
|
||||
import Logger from "@/lib/logger";
|
||||
import { MessageEntity } from "@/modules/entities/Message";
|
||||
import { MessageRepository } from "@/modules/repositories/Message";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
export default async function MessageSend(fastify: FastifyInstance) {
|
||||
const logger = new Logger("Endpoint | message/send");
|
||||
|
||||
fastify.post("/", async (req, res) => {
|
||||
res.header("Content-Type", "application/json");
|
||||
res.header("Connection", "close");
|
||||
|
||||
if ("error" in req.token)
|
||||
return res.code(400).send(req.token);
|
||||
|
||||
const result = MessageRepository.schema
|
||||
.omit({ userid: true })
|
||||
.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.code(400).send(InputError(result.error.issues));
|
||||
}
|
||||
|
||||
try {
|
||||
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
|
||||
const id = await messageRepo.sendMessage(result.data.message, result.data.channel, req.token.user.id);
|
||||
|
||||
return res.send({
|
||||
success: true,
|
||||
id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Database Error:", err);
|
||||
|
||||
return res.code(500).send(DatabaseError());
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user