diff --git a/server/api/ApiDeclare.ts b/server/api/ApiDeclare.ts index d503082..4edba17 100644 --- a/server/api/ApiDeclare.ts +++ b/server/api/ApiDeclare.ts @@ -15,7 +15,9 @@ export type CallMethod = "Chat.getInfo" | "Chat.sendMessage" | - "Chat.getMessageHistory" + "Chat.getMessageHistory" | + + "Chat.uploadFile" export type ClientEvent = "Client.onMessage" diff --git a/server/api/ChatApi.ts b/server/api/ChatApi.ts index ec1f603..dc298f5 100644 --- a/server/api/ChatApi.ts +++ b/server/api/ChatApi.ts @@ -1,8 +1,11 @@ +import { Buffer } from "node:buffer"; import Chat from "../data/Chat.ts"; -import ChatPrivate from "../data/ChatPrivate.ts"; -import MessagesManager from "../data/MessagesManager.ts"; +import ChatPrivate from "../data/ChatPrivate.ts" +import FileManager from "../data/FileManager.ts" +import MessagesManager from "../data/MessagesManager.ts" import User from "../data/User.ts" -import ApiManager from "./ApiManager.ts"; +import UserChatLinker from "../data/UserChatLinker.ts" +import ApiManager from "./ApiManager.ts" import BaseApi from "./BaseApi.ts" import TokenManager from "./TokenManager.ts" @@ -77,6 +80,10 @@ export default class ChatApi extends BaseApi { code: 404, msg: "對話不存在", } + if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { + code: 400, + msg: "用戶無權訪問該對話", + } const msg = { text: args.text as string, @@ -133,6 +140,49 @@ export default class ChatApi extends BaseApi { code: 404, msg: "對話不存在", } + if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { + code: 400, + msg: "用戶無權訪問該對話", + } + + return { + code: 200, + msg: "成功", + data: { + messages: MessagesManager.getInstanceForChat(chat).getMessagesWithPage(15, args.page as number), + }, + } + }) + /** + * 上傳文件 + * @param token 令牌 + * @param target 目標對話 + * @param file_name 文件名稱 + * @param data 文件二進制數據 + */ + this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => { + if (this.checkArgsMissing(args, ['token', 'target', 'data', 'file_name'])) return { + msg: "參數缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token, deviceId)) return { + code: 401, + msg: "令牌無效", + } + + const chat = Chat.findById(args.target as string) + if (chat == null) return { + code: 404, + msg: "對話不存在", + } + if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { + code: 400, + msg: "用戶無權訪問該對話", + } + + const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer) return { code: 200, diff --git a/server/api/FileTokenManager.ts b/server/api/FileTokenManager.ts new file mode 100644 index 0000000..5f53877 --- /dev/null +++ b/server/api/FileTokenManager.ts @@ -0,0 +1,55 @@ +import { Buffer } from "node:buffer" +import config from "../config.ts" +import User from "../data/User.ts" +import crypto from 'node:crypto' +import Token from "./Token.ts" + +function normalizeKey(key: string, keyLength = 32) { + const hash = crypto.createHash('sha256') + hash.update(key) + const keyBuffer = hash.digest() + return keyLength ? keyBuffer.slice(0, keyLength) : keyBuffer +} + +export default class FileTokenManager { + static makeAuth(user: User) { + return crypto.createHash("sha256").update(user.bean.id + user.getPassword() + config.salt + '_file').digest().toString('hex') + } + static encode(token: Token) { + return crypto.createCipheriv("aes-256-gcm", normalizeKey(config.aes_key + '_file'), '01234567890123456').update( + JSON.stringify(token) + ).toString('hex') + } + static decode(token: string) { + if (token == null) throw new Error('令牌為空!') + return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key + '_file'), '01234567890123456').update( + Buffer.from(token, 'hex') + ).toString()) as Token + } + + /** + * 簽發文件令牌 + */ + static make(user: User, device_id: string) { + const time = Date.now() + return this.encode({ + author: user.bean.id, + auth: this.makeAuth(user), + made_time: time, + // 過期時間: 2分鐘 + expired_time: time + (1 * 1000 * 60 * 2), + device_id: device_id + }) + } + /** + * 校驗文件令牌 + */ + static check(user: User, token: string) { + const tk = this.decode(token) + + return ( + this.makeAuth(user) == tk.auth + && tk.expired_time < Date.now() + ) + } +}