From d129c95aa4430e13cba81712e8b47153c47ed5f0 Mon Sep 17 00:00:00 2001 From: Last2014 Date: Mon, 30 Mar 2026 11:37:57 +0900 Subject: [PATCH] =?UTF-8?q?Chg:=20exactOptionalPropertyTypes=E3=82=92false?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=20/=20Chg(Security):=20=E3=83=88?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=8C=E4=B8=8D=E6=AD=A3=E3=81=AA?= =?UTF-8?q?=E5=A0=B4=E5=90=88=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E5=85=A8=E3=81=A6token=5Finvalid=E3=81=AB=E5=A4=89=E6=9B=B4=20?= =?UTF-8?q?/=20New:=20channel=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB=E3=83=BB?= =?UTF-8?q?=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88=E3=83=AA=20/=20Chg:=20conf?= =?UTF-8?q?ig=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB=E3=81=AEvalue=E3=82=92st?= =?UTF-8?q?ring=E3=81=AB=20/=20Chg:=20config=E3=83=86=E3=83=BC=E3=83=96?= =?UTF-8?q?=E3=83=AB=E3=81=AElength=E3=82=924096=E3=81=AB=20/=20New:=20mes?= =?UTF-8?q?sage=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB=20/=20Chg:=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E3=81=AE=E3=81=9F=E3=82=81user=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=96=E3=83=AB=E3=81=AEOptionalProps=E3=81=ABid=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20/=20New:=20channel/create=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=20/=20Ne?= =?UTF-8?q?w:=20channel/list=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=20/=20New:=20channel/edit=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=20/=20Enhance:=20?= =?UTF-8?q?primary/signup=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=81=AE=E9=87=8D=E8=A4=87=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E5=AE=9F=E8=A3=85=E3=81=A7=E6=9C=AB=E5=B0=BE?= =?UTF-8?q?=E3=82=AB=E3=83=B3=E3=83=9E=E3=81=AA=E3=81=A9=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E5=96=84=20/=20Chg:=20setup/initialization=E3=81=AEdescription?= =?UTF-8?q?=E3=81=AB=E6=9C=80=E5=A4=A7=E6=96=87=E5=AD=97=E6=95=B04096?= =?UTF-8?q?=E3=82=92=E5=88=B6=E5=AE=9A=20/=20Chg:=20serverInfo=E3=82=92def?= =?UTF-8?q?ault=20export=E3=81=8B=E3=82=89export=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20/=20New:=20=E3=83=95=E3=83=AD=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=81=A7me=E3=82=92=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=81=BF=20/=20New:=20=E3=83=95=E3=83=AD?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=A8=E3=83=B3=E3=83=89=E3=81=A7channel?= =?UTF-8?q?=E3=82=92=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF=20/=20New:=20clie?= =?UTF-8?q?nt.ts=E3=81=A7=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=8C?= =?UTF-8?q?=E3=81=82=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=AF=E3=83=88=E3=83=BC?= =?UTF-8?q?=E3=82=AF=E3=83=B3=E3=82=92=E6=8C=87=E5=AE=9A=20/=20Chg:=20clie?= =?UTF-8?q?nt=E3=82=92ref=E3=81=AB=20/=20Del:=20IndexedDB=E3=81=8B?= =?UTF-8?q?=E3=82=89server=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=20/=20Fix:=20Dexie=E3=81=AEclass=E3=81=AB?= =?UTF-8?q?=E5=91=BD=E5=90=8D=20/=20Feat:=20=E3=83=95=E3=83=AD=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=A8=E3=83=B3=E3=83=89=E3=81=A7=E3=81=AE=E3=82=B5?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=A4=E3=83=B3=E3=83=9A=E3=83=BC=E3=82=B8?= =?UTF-8?q?=20/=20Fix:=20L.js=E3=81=A7=E4=BB=BB=E6=84=8F=E3=81=AEbody?= =?UTF-8?q?=E3=81=8C=E3=81=82=E3=82=8B=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=E3=81=8C=E5=AE=9A=E7=BE=A9=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20/=20Del:=20L.js=E3=81=AEserver-info=E3=81=A7?= =?UTF-8?q?=E3=81=AE=E4=B8=8D=E8=A6=81=E3=81=AAimport=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4=20/=20Fix:=20L.js=E3=81=A7=E3=83=88=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20/=20Fix:=20L.js=E3=81=AEUserSchema=E3=81=ABlastUsed?= =?UTF-8?q?At=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/lib/auth.ts | 6 +- .../backend/src/modules/entities/Channel.ts | 43 +++++ .../backend/src/modules/entities/Config.ts | 5 +- .../backend/src/modules/entities/Message.ts | 36 ++++ packages/backend/src/modules/entities/User.ts | 2 +- .../src/modules/repositories/Channel.ts | 65 +++++++ .../backend/src/modules/repositories/Token.ts | 6 +- packages/backend/src/routes/channel/create.ts | 55 ++++++ packages/backend/src/routes/channel/edit.ts | 52 ++++++ packages/backend/src/routes/channel/index.ts | 18 ++ packages/backend/src/routes/channel/list.ts | 49 ++++++ packages/backend/src/routes/primary/signup.ts | 6 +- .../src/routes/setup/initialization.ts | 2 +- packages/backend/tsconfig.json | 2 +- packages/frontend/src/Layout.vue | 2 +- packages/frontend/src/lib/account.ts | 54 +++++- packages/frontend/src/lib/client.ts | 15 +- packages/frontend/src/lib/db.ts | 9 +- packages/frontend/src/main.ts | 9 +- packages/frontend/src/routes/index.vue | 8 + .../src/routes/setup/create-admin.vue | 6 +- .../src/routes/setup/initialization.vue | 4 +- packages/frontend/src/routes/signin.vue | 165 ++++++++++++++++++ .../src/1.0.0-alpha.0/api/channel/create.d.ts | 17 ++ .../src/1.0.0-alpha.0/api/channel/edit.d.ts | 22 +++ .../src/1.0.0-alpha.0/api/channel/list.d.ts | 24 +++ .../src/1.0.0-alpha.0/api/me/index.d.ts | 3 +- .../src/1.0.0-alpha.0/api/server-info.d.ts | 1 - .../lynqchat-js/src/1.0.0-alpha.0/map.d.ts | 8 +- .../src/1.0.0-alpha.0/modules/channel.d.ts | 5 + .../src/1.0.0-alpha.0/modules/error/auth.d.ts | 9 + .../src/1.0.0-alpha.0/modules/user.d.ts | 1 + packages/lynqchat-js/src/index.ts | 13 +- 33 files changed, 683 insertions(+), 39 deletions(-) create mode 100644 packages/backend/src/modules/entities/Channel.ts create mode 100644 packages/backend/src/modules/entities/Message.ts create mode 100644 packages/backend/src/modules/repositories/Channel.ts create mode 100644 packages/backend/src/routes/channel/create.ts create mode 100644 packages/backend/src/routes/channel/edit.ts create mode 100644 packages/backend/src/routes/channel/index.ts create mode 100644 packages/backend/src/routes/channel/list.ts create mode 100644 packages/frontend/src/routes/signin.vue create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/create.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/edit.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/list.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/modules/channel.d.ts create mode 100644 packages/lynqchat-js/src/1.0.0-alpha.0/modules/error/auth.d.ts diff --git a/packages/backend/src/lib/auth.ts b/packages/backend/src/lib/auth.ts index dedefe9..511c86e 100644 --- a/packages/backend/src/lib/auth.ts +++ b/packages/backend/src/lib/auth.ts @@ -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: "トークンが不正です。", }); } diff --git a/packages/backend/src/modules/entities/Channel.ts b/packages/backend/src/modules/entities/Channel.ts new file mode 100644 index 0000000..110956b --- /dev/null +++ b/packages/backend/src/modules/entities/Channel.ts @@ -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; +} \ No newline at end of file diff --git a/packages/backend/src/modules/entities/Config.ts b/packages/backend/src/modules/entities/Config.ts index a62e370..f9a6542 100644 --- a/packages/backend/src/modules/entities/Config.ts +++ b/packages/backend/src/modules/entities/Config.ts @@ -11,6 +11,9 @@ export class ConfigEntity { @PrimaryKey({ type: "string" }) name!: string; - @Property({ type: "text" }) + @Property({ + type: "string", + length: 4096, + }) value!: string; } \ No newline at end of file diff --git a/packages/backend/src/modules/entities/Message.ts b/packages/backend/src/modules/entities/Message.ts new file mode 100644 index 0000000..ff4900c --- /dev/null +++ b/packages/backend/src/modules/entities/Message.ts @@ -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; +} \ No newline at end of file diff --git a/packages/backend/src/modules/entities/User.ts b/packages/backend/src/modules/entities/User.ts index 6b7c94f..ff1b0b1 100644 --- a/packages/backend/src/modules/entities/User.ts +++ b/packages/backend/src/modules/entities/User.ts @@ -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", diff --git a/packages/backend/src/modules/repositories/Channel.ts b/packages/backend/src/modules/repositories/Channel.ts new file mode 100644 index 0000000..7036833 --- /dev/null +++ b/packages/backend/src/modules/repositories/Channel.ts @@ -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 { + 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) { + const channel = this.create({ + ...data, + createdBy: data.userid, + }); + + await this.em.persist(channel).flush(); + return channel.id; + } + + async editChannel( + target: ChannelEntity, + data: Partial, "userid">> + ) { + await this.nativeUpdate(target, data); + return target.id; + } +} diff --git a/packages/backend/src/modules/repositories/Token.ts b/packages/backend/src/modules/repositories/Token.ts index ceaa970..40f68d9 100644 --- a/packages/backend/src/modules/repositories/Token.ts +++ b/packages/backend/src/modules/repositories/Token.ts @@ -52,9 +52,9 @@ export class TokenRepository extends EntityRepository { ) 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) diff --git a/packages/backend/src/routes/channel/create.ts b/packages/backend/src/routes/channel/create.ts new file mode 100644 index 0000000..b9403d8 --- /dev/null +++ b/packages/backend/src/routes/channel/create.ts @@ -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()); + } + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/channel/edit.ts b/packages/backend/src/routes/channel/edit.ts new file mode 100644 index 0000000..d6a7754 --- /dev/null +++ b/packages/backend/src/routes/channel/edit.ts @@ -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()); + } + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/channel/index.ts b/packages/backend/src/routes/channel/index.ts new file mode 100644 index 0000000..760a972 --- /dev/null +++ b/packages/backend/src/routes/channel/index.ts @@ -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", + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/channel/list.ts b/packages/backend/src/routes/channel/list.ts new file mode 100644 index 0000000..d1a1927 --- /dev/null +++ b/packages/backend/src/routes/channel/list.ts @@ -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()); + } + }); +} \ No newline at end of file diff --git a/packages/backend/src/routes/primary/signup.ts b/packages/backend/src/routes/primary/signup.ts index 48d1cd7..e0616b7 100644 --- a/packages/backend/src/routes/primary/signup.ts +++ b/packages/backend/src/routes/primary/signup.ts @@ -40,9 +40,9 @@ export default function PrimarySignUp(fastify: FastifyInstance) { validation: "regex", code: "invalid_string", message: "Duplicate", - path: [duplicate] - } - ])) + path: [duplicate], + }, + ])); } } diff --git a/packages/backend/src/routes/setup/initialization.ts b/packages/backend/src/routes/setup/initialization.ts index 3caeca1..ec38b26 100644 --- a/packages/backend/src/routes/setup/initialization.ts +++ b/packages/backend/src/routes/setup/initialization.ts @@ -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(), }); diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index a106303..99c361f 100755 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -10,7 +10,7 @@ "./src/types", ], "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, + "exactOptionalPropertyTypes": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, "esModuleInterop": true, diff --git a/packages/frontend/src/Layout.vue b/packages/frontend/src/Layout.vue index 80f6750..a7aab11 100755 --- a/packages/frontend/src/Layout.vue +++ b/packages/frontend/src/Layout.vue @@ -125,7 +125,7 @@ main.layout { import { RouterView, useRouter } from "vue-router"; import routerStatus from "@/lib/router"; import Progress from "@/components/Progress.vue"; -import serverInfo from "@/lib/account"; +import { serverInfo } from "@/lib/account"; const router = useRouter(); diff --git a/packages/frontend/src/lib/account.ts b/packages/frontend/src/lib/account.ts index 5ba982c..e8c7330 100644 --- a/packages/frontend/src/lib/account.ts +++ b/packages/frontend/src/lib/account.ts @@ -1,11 +1,57 @@ -import client from "@/lib/client"; +import client, { initClient } from "@/lib/client"; import type ApiMap from "lynqchat-js/1.0.0-alpha.0/map"; import { ref } from "vue"; +/* +[TODO] +キャッシュの類を全部account.tsに詰め込むのをやめる +*/ -let serverInfo = ref(await client.request("server-info")); +await initClient(); + +export let serverInfo = ref(await client.value.request("server-info")); +export let account = ref(await client.value.request("me")); + +export let channels = ref["channels"]>([]); +export let lastLoadedChannel = ref(); export const reloadServerInfo = async () => { - serverInfo.value = await client.request("server-info"); + serverInfo.value = await client.value.request("server-info"); } -export default serverInfo; \ No newline at end of file +export const reloadAccount = async () => { + await initClient(); + + account.value = await client.value.request("me"); +} + +export const initChannels = async () => { + lastLoadedChannel.value = undefined; + + const response = await client.value.request("channel/list"); + + if (!response.success) { + return false; + } + + channels.value = response.channels; + if (response.channels.length > 0) { + lastLoadedChannel.value = response.channels[response.channels.length - 1]?.id; + } + return true; +} + +export const loadChannels = async () => { + const response = await client.value.request("channel/list", lastLoadedChannel.value ? { + since: lastLoadedChannel.value, + } : undefined); + + if (!response.success) { + return false; + } + + channels.value = channels.value.concat(response.channels); + if (response.channels.length > 0) { + lastLoadedChannel.value = response.channels[response.channels.length - 1]?.id; + } + return true; +} \ No newline at end of file diff --git a/packages/frontend/src/lib/client.ts b/packages/frontend/src/lib/client.ts index ccebafb..afbbf3b 100644 --- a/packages/frontend/src/lib/client.ts +++ b/packages/frontend/src/lib/client.ts @@ -1,8 +1,19 @@ import LynqChat from "lynqchat-js"; import type ApiMap from "lynqchat-js/1.0.0-alpha.0/map"; +import Database, { getByIndex } from "./db"; +import { ref } from "vue"; -const client = new LynqChat({ +const client = ref>(new LynqChat({ origin: window.origin, -}); +})); + +export const initClient = async () => { + const db = new Database(); + const row = await getByIndex(db.settings, "name", "token"); + + if (row?.value) { + client.value.token = row.value; + } +} export default client; \ No newline at end of file diff --git a/packages/frontend/src/lib/db.ts b/packages/frontend/src/lib/db.ts index b7a81d5..baa020e 100644 --- a/packages/frontend/src/lib/db.ts +++ b/packages/frontend/src/lib/db.ts @@ -6,14 +6,7 @@ export interface Settings { value: string; } -export interface Server { - id: number; - name: string; - value: string; -} - -export default class extends Dexie { - server!: EntityTable; +export default class Database extends Dexie { settings!: EntityTable; constructor() { diff --git a/packages/frontend/src/main.ts b/packages/frontend/src/main.ts index 8ed6fc6..1fa233b 100755 --- a/packages/frontend/src/main.ts +++ b/packages/frontend/src/main.ts @@ -2,7 +2,7 @@ import { createApp } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import routerStatus from "@/lib/router"; import { createHead } from "@unhead/vue/client"; -import serverInfo from "@/lib/account"; +import { serverInfo } from "@/lib/account"; import "@/global.css"; import Layout from "@/Layout.vue"; @@ -39,6 +39,13 @@ const router = createRouter({ }, component: () => import("@/routes/setup/create-admin.vue"), }, + { + path: "/signin", + meta: { + title: "サインイン", + }, + component: () => import("@/routes/signin.vue"), + }, { path: "/:NotFound(.*)*", meta: { diff --git a/packages/frontend/src/routes/index.vue b/packages/frontend/src/routes/index.vue index 93f1d5f..4a1b65d 100755 --- a/packages/frontend/src/routes/index.vue +++ b/packages/frontend/src/routes/index.vue @@ -3,4 +3,12 @@ \ No newline at end of file diff --git a/packages/frontend/src/routes/setup/create-admin.vue b/packages/frontend/src/routes/setup/create-admin.vue index 794d1f2..6077cfa 100644 --- a/packages/frontend/src/routes/setup/create-admin.vue +++ b/packages/frontend/src/routes/setup/create-admin.vue @@ -89,7 +89,7 @@ form { \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/create.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/create.d.ts new file mode 100644 index 0000000..4609a96 --- /dev/null +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/create.d.ts @@ -0,0 +1,17 @@ +import InputError from "../../modules/error/input"; +import ErrorBase from "../../modules/error"; +import DatabaseError from "../../modules/error/database"; +import UnknownError from "../../modules/error/unknown"; +import Success from "../../modules/response/success"; +import Channel from "../../modules/channel"; +import YetInitializationError from "../../modules/error/yet_init"; +import AuthError from "../..//modules/error/auth"; + +export default interface ChannelCreate { + "channel/create": { + body: Omit; + response: (Success & { + id: string; + }) | InputError | AuthError | DatabaseError | YetInitializationError | UnknownError; + }; +} \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/edit.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/edit.d.ts new file mode 100644 index 0000000..ad6b8ef --- /dev/null +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/edit.d.ts @@ -0,0 +1,22 @@ +import InputError from "../../modules/error/input"; +import ErrorBase from "../../modules/error"; +import DatabaseError from "../../modules/error/database"; +import UnknownError from "../../modules/error/unknown"; +import Success from "../../modules/response/success"; +import Channel from "../../modules/channel"; +import YetInitializationError from "../../modules/error/yet_init"; +import AuthError from "../..//modules/error/auth"; + +type RequireAtLeastOne = + Keys extends keyof T + ? Required> & Partial> + : never; + +export default interface ChannelEdit { + "channel/edit": { + body: RequireAtLeastOne>; + response: (Success & { + id: string; + }) | InputError | AuthError | DatabaseError | YetInitializationError | UnknownError; + }; +} \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/list.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/list.d.ts new file mode 100644 index 0000000..25d9289 --- /dev/null +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/api/channel/list.d.ts @@ -0,0 +1,24 @@ +import InputError from "../../modules/error/input"; +import ErrorBase from "../../modules/error"; +import DatabaseError from "../../modules/error/database"; +import UnknownError from "../../modules/error/unknown"; +import Success from "../../modules/response/success"; +import Channel from "../../modules/channel"; +import YetInitializationError from "../../modules/error/yet_init"; +import AuthError from "../..//modules/error/auth"; + +export default interface ChannelList { + "channel/list": { + body?: { + limit?: number; + since?: string | Date; + }; + response: (Success & { + channels: (Omit & { + id: string; + createdBy: string; + createdAt: string; + })[]; + }) | InputError | AuthError | DatabaseError | YetInitializationError | UnknownError; + }; +} \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/api/me/index.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/api/me/index.d.ts index af02945..c8d617b 100644 --- a/packages/lynqchat-js/src/1.0.0-alpha.0/api/me/index.d.ts +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/api/me/index.d.ts @@ -5,11 +5,12 @@ import Success from "../../modules/response/success"; import YetInitializationError from "../../modules/error/yet_init"; import UserSchema from "../../modules/user"; import UnknownError from "../../modules/error/unknown"; +import AuthError from "../../modules/error/auth"; export default interface Me { "me": { body: never; response: (Success & Omit) - | DatabaseError | InputError | YetInitializationError | UnknownError; + | DatabaseError | InputError | AuthError | YetInitializationError | UnknownError; }; } \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/api/server-info.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/api/server-info.d.ts index ae5e695..bcaf8c3 100644 --- a/packages/lynqchat-js/src/1.0.0-alpha.0/api/server-info.d.ts +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/api/server-info.d.ts @@ -1,4 +1,3 @@ -import { InputError, InputNoneError } from "../modules/error/input"; import ErrorBase from "../modules/error"; import DatabaseError from "../modules/error/database"; import UnknownError from "../modules/error/unknown"; diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/map.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/map.d.ts index e29ba0f..7511b73 100644 --- a/packages/lynqchat-js/src/1.0.0-alpha.0/map.d.ts +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/map.d.ts @@ -1,3 +1,6 @@ +import ChannelCreate from "./api/channel/create"; +import ChannelEdit from "./api/channel/edit"; +import ChannelList from "./api/channel/list"; import Me from "./api/me"; import PrimarySignin from "./api/primary/signin"; import PrimarySignup from "./api/primary/signup"; @@ -11,6 +14,9 @@ type ApiMap = ServerInfo & PrimarySignin & PrimarySignup & - Me; + Me & + ChannelCreate & + ChannelEdit & + ChannelList; export default ApiMap; \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/modules/channel.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/channel.d.ts new file mode 100644 index 0000000..e3c471f --- /dev/null +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/channel.d.ts @@ -0,0 +1,5 @@ +export default interface Channel { + name: string; + description: string; + userid: string; +} \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/modules/error/auth.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/error/auth.d.ts new file mode 100644 index 0000000..2fd3b48 --- /dev/null +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/error/auth.d.ts @@ -0,0 +1,9 @@ +import ErrorBase from "."; + +type AuthError = ErrorBase<{ + bad: "client", + code: "token_invalid", + message: "トークンが不正です。", +}>; + +export default AuthError; \ No newline at end of file diff --git a/packages/lynqchat-js/src/1.0.0-alpha.0/modules/user.d.ts b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/user.d.ts index 5b152c6..1d0c362 100644 --- a/packages/lynqchat-js/src/1.0.0-alpha.0/modules/user.d.ts +++ b/packages/lynqchat-js/src/1.0.0-alpha.0/modules/user.d.ts @@ -7,4 +7,5 @@ export default interface UserSchema { isSuspended: boolean; isAdmin: boolean; createdAt: string; + lastUsedAt: string; } \ No newline at end of file diff --git a/packages/lynqchat-js/src/index.ts b/packages/lynqchat-js/src/index.ts index 764231b..f0f96d5 100644 --- a/packages/lynqchat-js/src/index.ts +++ b/packages/lynqchat-js/src/index.ts @@ -14,7 +14,7 @@ type BodyArgs = []; export default class LynqChat< - M extends { [K in keyof M]: { body: any; response: any } } + M extends { [K in keyof M]: { body?: any; response: any } } > { readonly origin: string; readonly retry: number; @@ -37,7 +37,13 @@ export default class LynqChat< } set token(token: string) { - if (token.length !== 64) throw new lynqError("Invalid token."); + const tokenArr = token.split("_"); + + if ( + tokenArr[0]?.length !== 64 || + tokenArr[1]?.length !== 64 + ) + throw new lynqError("Invalid token."); this._token = token; } @@ -59,6 +65,9 @@ export default class LynqChat< cache: "no-store", headers: { "Content-Type": "application/json", + ...(this._token ? { + Authorization: `Bearer ${this._token}` + } : {}), }, body, }