Compare commits
15 Commits
1a69b521e6
...
3617292409
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3617292409 | ||
|
|
a3fc61494e | ||
|
|
fa62180667 | ||
|
|
e60c1cf1c4 | ||
|
|
7e60e4a4be | ||
|
|
f3a9cb8641 | ||
|
|
c577797e57 | ||
|
|
3a7e4970d4 | ||
|
|
0e14bb9a45 | ||
|
|
2869a77abd | ||
|
|
913d1f395f | ||
|
|
abf06c71af | ||
|
|
afeab61468 | ||
|
|
f06e93ef06 | ||
|
|
71b368a5ac |
@@ -1,4 +1,4 @@
|
||||
import { io, Socket } from 'https://unpkg.com/socket.io-client@4.8.1/dist/socket.io.esm.min.js'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
|
||||
import ApiCallbackMessage from './ApiCallbackMessage.ts'
|
||||
|
||||
@@ -10,7 +10,9 @@ class Client {
|
||||
static connect() {
|
||||
this.socket?.disconnect()
|
||||
this.socket && delete this.socket
|
||||
this.socket = io()
|
||||
this.socket = io({
|
||||
transports: ['websocket']
|
||||
})
|
||||
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
||||
try {
|
||||
if (name == null || data == null) return
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
"mdui": "npm:mdui@2.1.4",
|
||||
"split.js": "npm:split.js@1.3.2",
|
||||
"crypto-es": "npm:crypto-es@3.1.0"
|
||||
"crypto-es": "npm:crypto-es@3.1.0",
|
||||
"socket.io-client": "npm:socket.io-client@4.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import User from "../api/client_data/User.ts"
|
||||
import RecentChat from "../api/client_data/RecentChat.ts"
|
||||
|
||||
import * as React from 'react'
|
||||
import * as CryptoES from 'crypto-es'
|
||||
import { Button, Dialog, NavigationRail, snackbar, TextField } from "mdui"
|
||||
import { Button, Dialog, NavigationRail, TextField } from "mdui"
|
||||
import Split from 'split.js'
|
||||
import 'mdui/jsx.zh-cn.d.ts'
|
||||
import { checkApiSuccessOrSncakbar } from "./snackbar.ts";
|
||||
import RegisterDialog from "./dialog/RegisterDialog.tsx";
|
||||
|
||||
declare global {
|
||||
namespace React {
|
||||
@@ -26,7 +27,7 @@ declare global {
|
||||
|
||||
export default function App() {
|
||||
const [recentsList, setRecentsList] = React.useState([
|
||||
{
|
||||
{
|
||||
id: '0',
|
||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||
title: "麻油衣酱",
|
||||
@@ -37,11 +38,11 @@ export default function App() {
|
||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||
title: "Maya Fey",
|
||||
content: "我是绫里真宵, 是一名灵媒师~"
|
||||
},
|
||||
},
|
||||
] as RecentChat[])
|
||||
const [contactsMap, setContactsMap] = React.useState({
|
||||
所有: [
|
||||
{
|
||||
{
|
||||
id: '0',
|
||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||
nickname: "麻油衣酱",
|
||||
@@ -50,7 +51,7 @@ export default function App() {
|
||||
id: '0',
|
||||
avatar: "https://www.court-records.net/mugshot/aa6-004-maya.png",
|
||||
nickname: "Maya Fey",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as { [key: string]: User[] })
|
||||
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
|
||||
@@ -61,27 +62,16 @@ export default function App() {
|
||||
})
|
||||
|
||||
const loginDialogRef: React.MutableRefObject<Dialog | null> = React.useRef(null)
|
||||
const inputAccountRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
const inputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
const registerButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const loginButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const loginInputAccountRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
const loginInputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
|
||||
useEventListener(loginButtonRef as React.MutableRefObject<Button>, 'click', async () => {
|
||||
const account = inputAccountRef.current!.value
|
||||
const password = inputPasswordRef.current!.value
|
||||
|
||||
const re = await Client.invoke("User.login", {
|
||||
account: account,
|
||||
password: CryptoES.SHA256(password),
|
||||
})
|
||||
if (re.code != 200)
|
||||
snackbar({
|
||||
message: "登錄失敗: " + re.msg
|
||||
})
|
||||
})
|
||||
const registerDialogRef: React.MutableRefObject<Dialog | null> = React.useRef(null)
|
||||
const registerInputUserNameRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
const registerInputNickNameRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
const registerInputPasswordRef: React.MutableRefObject<TextField | null> = React.useRef(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
;(async () => {
|
||||
; (async () => {
|
||||
Split(['#SideBar', '#ChatFragment'], {
|
||||
sizes: [25, 75],
|
||||
minSize: [200, 400],
|
||||
@@ -90,14 +80,12 @@ export default function App() {
|
||||
|
||||
Client.connect()
|
||||
const re = await Client.invoke("User.auth", {
|
||||
access_token: data.access_token,
|
||||
access_token: data.access_token || '',
|
||||
})
|
||||
if (re.code == 401)
|
||||
loginDialogRef.current!.open = true
|
||||
else if (re.code != 200)
|
||||
snackbar({
|
||||
message: "驗證失敗: " + re.msg
|
||||
})
|
||||
if (checkApiSuccessOrSncakbar(re, "驗證失敗")) return
|
||||
})()
|
||||
}, [])
|
||||
|
||||
@@ -110,10 +98,18 @@ export default function App() {
|
||||
}}>
|
||||
<LoginDialog
|
||||
loginDialogRef={loginDialogRef}
|
||||
inputAccountRef={inputAccountRef}
|
||||
inputPasswordRef={inputPasswordRef}
|
||||
registerButtonRef={registerButtonRef}
|
||||
loginButtonRef={loginButtonRef} />
|
||||
loginInputAccountRef={loginInputAccountRef}
|
||||
loginInputPasswordRef={loginInputPasswordRef}
|
||||
registerDialogRef={registerDialogRef} />
|
||||
|
||||
<RegisterDialog
|
||||
registerDialogRef={registerDialogRef}
|
||||
registerInputUserNameRef={registerInputUserNameRef}
|
||||
registerInputNickNameRef={registerInputNickNameRef}
|
||||
registerInputPasswordRef={registerInputPasswordRef}
|
||||
loginInputAccountRef={loginInputAccountRef}
|
||||
loginInputPasswordRef={loginInputPasswordRef} />
|
||||
|
||||
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
|
||||
<mdui-button-icon icon="menu" slot="top"></mdui-button-icon>
|
||||
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, TextField } from "mdui";
|
||||
import { Button, Dialog, TextField } from "mdui"
|
||||
import useEventListener from "../useEventListener.ts"
|
||||
import { checkApiSuccessOrSncakbar } from "../snackbar.ts"
|
||||
import Client from "../../api/Client.ts"
|
||||
|
||||
import * as CryptoES from 'crypto-es'
|
||||
import data from "../../Data.ts";
|
||||
|
||||
interface Refs {
|
||||
inputAccountRef: React.MutableRefObject<TextField | null>
|
||||
inputPasswordRef: React.MutableRefObject<TextField | null>
|
||||
registerButtonRef: React.MutableRefObject<Button | null>
|
||||
loginButtonRef: React.MutableRefObject<Button | null>
|
||||
loginInputAccountRef: React.MutableRefObject<TextField | null>
|
||||
loginInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||
loginDialogRef: React.MutableRefObject<Dialog | null>
|
||||
registerDialogRef: React.MutableRefObject<Dialog | null>
|
||||
}
|
||||
|
||||
export default function LoginDialog({
|
||||
inputAccountRef,
|
||||
inputPasswordRef,
|
||||
registerButtonRef,
|
||||
loginButtonRef,
|
||||
loginDialogRef
|
||||
loginInputAccountRef,
|
||||
loginInputPasswordRef,
|
||||
loginDialogRef,
|
||||
registerDialogRef
|
||||
}: Refs) {
|
||||
return (
|
||||
<mdui-dialog headline="登录" ref={loginDialogRef}>
|
||||
const loginButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const registerButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
useEventListener(registerButtonRef, 'click', () => registerDialogRef.current!.open = true)
|
||||
useEventListener(loginButtonRef, 'click', async () => {
|
||||
const account = loginInputAccountRef.current!.value
|
||||
const password = loginInputPasswordRef.current!.value
|
||||
|
||||
<mdui-text-field label="账号" ref={inputAccountRef}></mdui-text-field>
|
||||
const re = await Client.invoke("User.login", {
|
||||
account: account,
|
||||
password: CryptoES.SHA256(password).toString(CryptoES.Hex),
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "登錄失敗")) return
|
||||
|
||||
data.access_token = re.data!.access_token as string
|
||||
location.reload()
|
||||
})
|
||||
return (
|
||||
<mdui-dialog headline="登錄" ref={loginDialogRef}>
|
||||
|
||||
<mdui-text-field label="用戶 ID / 用戶名" ref={loginInputAccountRef}></mdui-text-field>
|
||||
<div style={{
|
||||
height: "10px",
|
||||
}}></div>
|
||||
<mdui-text-field label="密码" ref={inputPasswordRef}></mdui-text-field>
|
||||
<mdui-text-field label="密碼" type="password" toggle-password ref={loginInputPasswordRef}></mdui-text-field>
|
||||
|
||||
<mdui-button slot="action" variant="text" ref={registerButtonRef}>注册</mdui-button>
|
||||
<mdui-button slot="action" variant="text" ref={loginButtonRef}>登录</mdui-button>
|
||||
|
||||
67
client/ui/dialog/RegisterDialog.tsx
Normal file
67
client/ui/dialog/RegisterDialog.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, TextField } from "mdui";
|
||||
import useEventListener from "../useEventListener.ts";
|
||||
import Client from "../../api/Client.ts";
|
||||
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts";
|
||||
|
||||
import * as CryptoES from 'crypto-es'
|
||||
|
||||
interface Refs {
|
||||
loginInputAccountRef: React.MutableRefObject<TextField | null>
|
||||
loginInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||
registerInputUserNameRef: React.MutableRefObject<TextField | null>
|
||||
registerInputNickNameRef: React.MutableRefObject<TextField | null>
|
||||
registerInputPasswordRef: React.MutableRefObject<TextField | null>
|
||||
registerDialogRef: React.MutableRefObject<Dialog | null>
|
||||
}
|
||||
|
||||
export default function RegisterDialog({
|
||||
loginInputAccountRef,
|
||||
loginInputPasswordRef,
|
||||
registerInputUserNameRef,
|
||||
registerInputNickNameRef,
|
||||
registerInputPasswordRef,
|
||||
registerDialogRef
|
||||
}: Refs) {
|
||||
const registerBackButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
const doRegisterButtonRef: React.MutableRefObject<Button | null> = React.useRef(null)
|
||||
useEventListener(registerBackButtonRef, 'click', () => registerDialogRef.current!.open = false)
|
||||
useEventListener(doRegisterButtonRef, 'click', async () => {
|
||||
const username = registerInputUserNameRef.current!.value
|
||||
const re = await Client.invoke("User.register", {
|
||||
username: username,
|
||||
nickname: registerInputNickNameRef.current!.value,
|
||||
password: CryptoES.SHA256(registerInputPasswordRef.current!.value).toString(CryptoES.Hex),
|
||||
})
|
||||
|
||||
if (checkApiSuccessOrSncakbar(re, "注冊失敗")) return
|
||||
loginInputAccountRef.current!.value = username == "" ? re.data!.userid as string : username
|
||||
loginInputPasswordRef.current!.value = registerInputPasswordRef.current!.value
|
||||
|
||||
registerInputUserNameRef.current!.value = ""
|
||||
registerInputNickNameRef.current!.value = ""
|
||||
registerInputPasswordRef.current!.value = ""
|
||||
registerDialogRef.current!.open = false
|
||||
snackbar({
|
||||
message: "注冊成功!",
|
||||
placement: "top",
|
||||
})
|
||||
})
|
||||
return (
|
||||
<mdui-dialog headline="注冊" ref={registerDialogRef}>
|
||||
|
||||
<mdui-text-field label="用戶名 (可選)" ref={registerInputUserNameRef}></mdui-text-field>
|
||||
<div style={{
|
||||
height: "10px",
|
||||
}}></div>
|
||||
<mdui-text-field label="昵稱" ref={registerInputNickNameRef}></mdui-text-field>
|
||||
<div style={{
|
||||
height: "10px",
|
||||
}}></div>
|
||||
<mdui-text-field label="密码" type="password" toggle-password ref={registerInputPasswordRef}></mdui-text-field>
|
||||
|
||||
<mdui-button slot="action" variant="text" ref={registerBackButtonRef}>返回</mdui-button>
|
||||
<mdui-button slot="action" variant="text" ref={doRegisterButtonRef}>注冊</mdui-button>
|
||||
</mdui-dialog>
|
||||
)
|
||||
}
|
||||
98
client/ui/snackbar.ts
Normal file
98
client/ui/snackbar.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { snackbar as mduiSnackbar, Snackbar } from "mdui"
|
||||
import ApiCallbackMessage from "../api/ApiCallbackMessage.ts"
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Snackbar 出现的位置。默认为 `bottom`。可选值为:
|
||||
* * `top`:位于顶部,居中对齐
|
||||
* * `top-start`:位于顶部,左对齐
|
||||
* * `top-end`:位于顶部,右对齐
|
||||
* * `bottom`:位于底部,居中对齐
|
||||
* * `bottom-start`:位于底部,左对齐
|
||||
* * `bottom-end`:位于底部,右对齐
|
||||
*/
|
||||
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
|
||||
/**
|
||||
* 操作按钮的文本
|
||||
*/
|
||||
action?: string;
|
||||
/**
|
||||
* 是否在右侧显示关闭按钮
|
||||
*/
|
||||
closeable?: boolean;
|
||||
/**
|
||||
* 消息文本最多显示几行。默认不限制行数。可选值为
|
||||
* * `1`:消息文本最多显示一行
|
||||
* * `2`:消息文本最多显示两行
|
||||
*/
|
||||
messageLine?: 1 | 2;
|
||||
/**
|
||||
* 在多长时间后自动关闭(单位为毫秒)。设置为 0 时,不自动关闭。默认为 5 秒后自动关闭。
|
||||
*/
|
||||
autoCloseDelay?: number;
|
||||
/**
|
||||
* 点击或触摸 Snackbar 以外的区域时是否关闭 Snackbar
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
/**
|
||||
* 队列名称。
|
||||
* 默认不启用队列,在多次调用该函数时,将同时显示多个 snackbar。
|
||||
* 可在该参数中传入一个队列名称,具有相同队列名称的 snackbar 函数,将在上一个 snackbar 关闭后才打开下一个 snackbar。
|
||||
*/
|
||||
queue?: string;
|
||||
/**
|
||||
* 点击 Snackbar 时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClick?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* 点击操作按钮时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* 默认点击后会关闭 snackbar;若返回值为 false,则不关闭 snackbar;若返回值为 promise,则将在 promise 被 resolve 后,关闭 snackbar。
|
||||
* @param snackbar
|
||||
*/
|
||||
onActionClick?: (snackbar: Snackbar) => void | boolean | Promise<void>;
|
||||
/**
|
||||
* Snackbar 开始显示时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onOpen?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 显示动画完成时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onOpened?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 开始隐藏时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClose?: (snackbar: Snackbar) => void;
|
||||
/**
|
||||
* Snackbar 隐藏动画完成时的回调函数。
|
||||
* 函数参数为 snackbar 实例,`this` 也指向 snackbar 实例。
|
||||
* @param snackbar
|
||||
*/
|
||||
onClosed?: (snackbar: Snackbar) => void;
|
||||
}
|
||||
|
||||
interface SnackbarOptions extends Options {
|
||||
message: string
|
||||
}
|
||||
|
||||
export function checkApiSuccessOrSncakbar(re: ApiCallbackMessage, msg_ahead: string, opinions_override: Options = {}): Snackbar | null {
|
||||
return re.code != 200 ? snackbar(
|
||||
Object.assign({
|
||||
message: `${msg_ahead}: ${re.msg} [${re.code}]`,
|
||||
placement: "top",
|
||||
} as SnackbarOptions, opinions_override)
|
||||
) : null
|
||||
}
|
||||
|
||||
export function snackbar(opinions: SnackbarOptions) {
|
||||
opinions.autoCloseDelay == null && (opinions.autoCloseDelay = 2500)
|
||||
return mduiSnackbar(opinions)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export default function useEventListener<T extends HTMLElement>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
|
||||
export default function useEventListener<T extends HTMLElement | null>(ref: React.MutableRefObject<T>, eventName: string, callback: (event: Event) => void) {
|
||||
React.useEffect(() => {
|
||||
ref.current.addEventListener(eventName, callback)
|
||||
return () => ref.current.removeEventListener(eventName, callback)
|
||||
ref.current!.addEventListener(eventName, callback)
|
||||
return () => ref.current!.removeEventListener(eventName, callback)
|
||||
}, [ref, eventName, callback])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import deno from '@deno/vite-plugin'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"tasks": {
|
||||
"server": "deno task build && deno run --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
||||
"debug": "deno task build && deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys ./server/main.ts",
|
||||
"server": "deno task build && deno run --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||
"debug": "deno task build && deno run --watch --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run=deno ./server/main.ts",
|
||||
"build": "cd ./client && deno task build"
|
||||
},
|
||||
"imports": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as SocketIo from "socket.io"
|
||||
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
||||
import EventCallbackFunction from "../typedef/EventCallbackFunction.ts"
|
||||
import BaseApi from "./BaseApi.ts"
|
||||
import DataWrongError from "./DataWrongError.ts";
|
||||
|
||||
export default class ApiManager {
|
||||
static httpServer: HttpServerLike
|
||||
@@ -40,11 +41,11 @@ export default class ApiManager {
|
||||
|
||||
return callback(this.event_listeners[name]?.(args))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
const err = e as Error
|
||||
try {
|
||||
callback({
|
||||
code: 500,
|
||||
msg: "錯誤: " + e
|
||||
code: err instanceof DataWrongError ? 400 : 500,
|
||||
msg: "錯誤: " + err.message
|
||||
})
|
||||
} catch(_e) {}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ export default abstract class BaseApi {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
checkArgsEmpty(args: { [key: string]: unknown }, names: string[]) {
|
||||
for (const k of names)
|
||||
if (k in args && args[k] == '')
|
||||
return true
|
||||
return false
|
||||
}
|
||||
registerEvent(name: CallMethod, func: EventCallbackFunction) {
|
||||
if (!name.startsWith(this.getName() + ".")) throw Error("注冊的事件應該與接口集合命名空間相匹配: " + name)
|
||||
ApiManager.addEventListener(name, func)
|
||||
|
||||
1
server/api/DataWrongError.ts
Normal file
1
server/api/DataWrongError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class DataWrongError extends Error {}
|
||||
53
server/api/TokenManager.ts
Normal file
53
server/api/TokenManager.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import config from "../config.ts"
|
||||
import User from "../data/User.ts"
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
interface Token {
|
||||
author: string
|
||||
auth: string
|
||||
made_time: number
|
||||
expired_time: number
|
||||
}
|
||||
|
||||
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 TokenManager {
|
||||
// TODO: 單令牌 -》 單 + 刷新 with 多設備管理
|
||||
static makeAuth(user: User) {
|
||||
return crypto.createHash("sha256").update(user.bean.id + user.getPassword() + config.salt).digest().toString('hex')
|
||||
}
|
||||
static encode(token: Token) {
|
||||
return crypto.createCipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
|
||||
JSON.stringify(token)
|
||||
).toString('hex')
|
||||
}
|
||||
static decode(token: string) {
|
||||
return JSON.parse(crypto.createDecipheriv("aes-256-gcm", normalizeKey(config.aes_key), '01234567890123456').update(
|
||||
Buffer.from(token, 'hex')
|
||||
).toString()) as Token
|
||||
}
|
||||
|
||||
static make(user: User, time: number = Date.now()) {
|
||||
return this.encode({
|
||||
author: user.bean.id,
|
||||
auth: this.makeAuth(user),
|
||||
made_time: time,
|
||||
expired_time: time + (1 * 1000 * 60 * 60 * 24),
|
||||
})
|
||||
}
|
||||
static makeNewer(user: User, token: string) {
|
||||
if (this.check(user, token))
|
||||
return this.make(user, Date.now() + (1 * 1000 * 60 * 60 * 24))
|
||||
}
|
||||
static check(user: User, token: string) {
|
||||
const tk = this.decode(token)
|
||||
|
||||
return this.makeAuth(user) == tk.auth
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,94 @@
|
||||
import User from "../data/User.ts";
|
||||
import BaseApi from "./BaseApi.ts"
|
||||
import TokenManager from "./TokenManager.ts";
|
||||
|
||||
export default class UserApi extends BaseApi {
|
||||
override getName(): string {
|
||||
return "User"
|
||||
}
|
||||
override onInit(): void {
|
||||
// 驗證
|
||||
this.registerEvent("User.auth", (args) => {
|
||||
return {
|
||||
msg: "",
|
||||
code: 401,
|
||||
if (this.checkArgsMissing(args, ['access_token'])) return {
|
||||
msg: "參數缺失",
|
||||
code: 400,
|
||||
}
|
||||
try {
|
||||
const access_token = TokenManager.decode(args.access_token as string)
|
||||
|
||||
if (access_token.expired_time > Date.now()) return {
|
||||
msg: "登錄令牌失效",
|
||||
code: 401,
|
||||
}
|
||||
|
||||
return {
|
||||
msg: "成功",
|
||||
code: 200,
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
if (err.message.indexOf("JSON") != -1)
|
||||
return {
|
||||
msg: "無效的登錄令牌",
|
||||
code: 401,
|
||||
}
|
||||
else
|
||||
throw e
|
||||
}
|
||||
})
|
||||
// 登錄
|
||||
this.registerEvent("User.login", (args) => {
|
||||
if (this.checkArgsMissing(args, ['account', 'password'])) return {
|
||||
msg: "",
|
||||
msg: "參數缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (this.checkArgsEmpty(args, ['account', 'password'])) return {
|
||||
msg: "參數不得為空",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const user = (User.findByUserName(args.account as string) || User.findById(args.account as string)) as User
|
||||
if (user == null) return {
|
||||
msg: "賬號或密碼錯誤",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
if (user.getPassword() == args.password) return {
|
||||
msg: "成功",
|
||||
code: 200,
|
||||
data: {
|
||||
access_token: TokenManager.make(user)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
msg: "",
|
||||
code: 501,
|
||||
msg: "賬號或密碼錯誤",
|
||||
code: 400,
|
||||
}
|
||||
})
|
||||
// 注冊
|
||||
this.registerEvent("User.register", (args) => {
|
||||
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
|
||||
msg: "參數缺失",
|
||||
code: 400,
|
||||
}
|
||||
if (this.checkArgsEmpty(args, ['nickname', 'password'])) return {
|
||||
msg: "參數不得為空",
|
||||
code: 400,
|
||||
}
|
||||
|
||||
const username: string | null = args.username as string
|
||||
const nickname: string = args.nickname as string
|
||||
const password: string = args.password as string
|
||||
|
||||
const user = User.createWithUserNameChecked(username, password, nickname, null)
|
||||
|
||||
return {
|
||||
msg: "成功",
|
||||
code: 200,
|
||||
data: {
|
||||
userid: user.bean.id
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ const prefix = isCompilingClient ? '.' : ''
|
||||
const default_data_path = "./thewhitesilk_data"
|
||||
let config = {
|
||||
data_path: default_data_path,
|
||||
salt: "TWS_Demo",
|
||||
aes_key: "01234567890123456",
|
||||
server: {
|
||||
use: "http",
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import config from '../config.ts'
|
||||
import UserBean from './UserBean.ts'
|
||||
|
||||
import FileManager from './FileManager.ts'
|
||||
import { SQLInputValue } from "node:sqlite";
|
||||
import DataWrongError from "../api/DataWrongError.ts";
|
||||
|
||||
/**
|
||||
* User.ts - Wrapper and manager
|
||||
@@ -18,12 +20,13 @@ import FileManager from './FileManager.ts'
|
||||
export default class User {
|
||||
static table_name: string = "Users"
|
||||
private static database: DatabaseSync = User.init()
|
||||
private static init(): DatabaseSync {
|
||||
private static init() {
|
||||
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, User.table_name + '.db'))
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ${User.table_name} (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 用户 ID, 哈希 */ id TEXT,
|
||||
/* 用户 ID, UUID */ id TEXT,
|
||||
/* 密碼, 哈希 */ password TEXT,
|
||||
/* 注册时间, 时间戳 */ registered_time INT8 NOT NULL,
|
||||
/* 用戶名, 可選 */ username TEXT,
|
||||
/* 昵称 */ nickname TEXT NOT NULL,
|
||||
@@ -34,29 +37,32 @@ export default class User {
|
||||
return db
|
||||
}
|
||||
|
||||
static createWithUserNameChecked(userName: string | null, nickName: string, avatar: Buffer | null): User {
|
||||
if (User.findAllBeansByCondition('username = ?', userName).length > 0)
|
||||
throw new Error(`用户名 ${userName} 已存在`)
|
||||
static createWithUserNameChecked(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
||||
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
|
||||
throw new DataWrongError(`用户名 ${userName} 已存在`)
|
||||
return User.create(
|
||||
userName,
|
||||
password,
|
||||
nickName,
|
||||
avatar
|
||||
)
|
||||
}
|
||||
|
||||
static create(userName: string | null, nickName: string, avatar: Buffer | null): User {
|
||||
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
||||
const user = new User(
|
||||
User.findAllBeansByCondition(
|
||||
'count = ?',
|
||||
User.database.prepare(`INSERT INTO ${User.table_name} (
|
||||
id,
|
||||
password,
|
||||
registered_time,
|
||||
username,
|
||||
nickname,
|
||||
avatar_file_hash,
|
||||
settings
|
||||
) VALUES (?, ?, ?, ?, ?, ?);`).run(
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?);`).run(
|
||||
crypto.randomUUID(),
|
||||
password,
|
||||
Date.now(),
|
||||
userName,
|
||||
nickName,
|
||||
@@ -69,21 +75,21 @@ export default class User {
|
||||
return user
|
||||
}
|
||||
|
||||
private static findAllBeansByCondition(condition: string, ...args: unknown[]): UserBean[] {
|
||||
return User.database.prepare(`SELECT * FROM ${User.table_name} WHERE ${condition};`).all(...args)
|
||||
private static findAllBeansByCondition(condition: string, ...args: SQLInputValue[]): UserBean[] {
|
||||
return User.database.prepare(`SELECT * FROM ${User.table_name} WHERE ${condition};`).all(...args) as unknown as UserBean[]
|
||||
}
|
||||
static findById(id: string): User {
|
||||
static findById(id: string) {
|
||||
const beans = User.findAllBeansByCondition('id = ?', id)
|
||||
if (beans.length == 0)
|
||||
throw new Error(`找不到用户 ID 为 ${id} 的用户`)
|
||||
return null
|
||||
else if (beans.length > 1)
|
||||
console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同用户 ID 的用户`))
|
||||
return new User(beans[0])
|
||||
}
|
||||
static findByUserName(userName: string): User {
|
||||
static findByUserName(userName: string) {
|
||||
const beans = User.findAllBeansByCondition('username = ?', userName)
|
||||
if (beans.length == 0)
|
||||
throw new Error(`找不到用户名为 ${userName} 的用户`)
|
||||
return null
|
||||
else if (beans.length > 1)
|
||||
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
||||
return new User(beans[0])
|
||||
@@ -94,30 +100,36 @@ export default class User {
|
||||
this.bean = bean
|
||||
}
|
||||
/* 一切的基础都是 count ID */
|
||||
private setAttr(key: string, value: unknown): void {
|
||||
private setAttr(key: string, value: unknown) {
|
||||
User.database.prepare(`UPDATE ${User.table_name} SET ${key} = ? WHERE count = ?`).run(value, this.bean.count)
|
||||
this.bean[key] = value
|
||||
}
|
||||
getUserName(): string | null {
|
||||
return this.bean.username
|
||||
}
|
||||
setUserName(userName: string): void {
|
||||
setUserName(userName: string) {
|
||||
this.setAttr("username", userName)
|
||||
}
|
||||
getNickName(): string {
|
||||
return this.bean.nickname
|
||||
}
|
||||
setNickName(nickName: string): void {
|
||||
setNickName(nickName: string) {
|
||||
this.setAttr("nickname", nickName)
|
||||
}
|
||||
getPassword(): string {
|
||||
return this.bean.password
|
||||
}
|
||||
setPassword(password: string) {
|
||||
this.setAttr("password", password)
|
||||
}
|
||||
getAvatar(): Buffer | null {
|
||||
return FileManager.findByHash(this.bean.avatar_file_hash)?.readSync()
|
||||
}
|
||||
setAvatar(avatar: Buffer): void {
|
||||
setAvatar(avatar: Buffer) {
|
||||
this.setAttr("avatar_file_hash", FileManager.uploadFile(`avatar_user_${this.bean.count}`, avatar).getHash())
|
||||
}
|
||||
|
||||
getSettings(): User.Settings {
|
||||
getSettings() {
|
||||
return new User.Settings(this, JSON.parse(this.bean.settings))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default class UserBean {
|
||||
declare count: number
|
||||
declare id: string
|
||||
declare password: string
|
||||
declare username: string | null
|
||||
declare registered_time: number
|
||||
declare nickname: string
|
||||
|
||||
@@ -9,6 +9,7 @@ import https from 'node:https'
|
||||
import readline from 'node:readline'
|
||||
import process from "node:process"
|
||||
import chalk from "chalk"
|
||||
import child_process from "node:child_process"
|
||||
|
||||
const app = express()
|
||||
app.use((req, res, next) => {
|
||||
@@ -20,12 +21,12 @@ app.use((req, res, next) => {
|
||||
app.use('/', express.static(config.data_path + '/page_compiled'))
|
||||
|
||||
const httpServer: HttpServerLike = (
|
||||
((config.server.use == 'http') && http.createServer(app)) ||
|
||||
((config.server.use == 'https') && https.createServer(config.server.https, app)) ||
|
||||
((config.server.use == 'http') && http.createServer(app)) ||
|
||||
((config.server.use == 'https') && https.createServer(config.server.https, app)) ||
|
||||
http.createServer(app)
|
||||
)
|
||||
const io = new SocketIo.Server(httpServer, {
|
||||
|
||||
|
||||
})
|
||||
|
||||
ApiManager.initServer(httpServer, io)
|
||||
@@ -33,13 +34,24 @@ ApiManager.initEvents()
|
||||
ApiManager.initAllApis()
|
||||
|
||||
httpServer.listen(config.server.listen)
|
||||
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
|
||||
console.log(chalk.green("API & Web 服務已經開始運作"))
|
||||
function help() {
|
||||
console.log(chalk.yellow("===== TheWhiteSilk Server ====="))
|
||||
console.log(chalk.yellow("b - 重新編譯前端"))
|
||||
}
|
||||
help()
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
rl.on('line', async (text) => {
|
||||
|
||||
rl.on('line', (text) => {
|
||||
if (text == 'b') {
|
||||
console.log(chalk.green("重新編譯..."))
|
||||
child_process.spawnSync("deno", ["task", "build"], {
|
||||
stdio: [process.stdin, process.stdout, process.stderr]
|
||||
})
|
||||
console.log(chalk.green("✓ 編譯完畢"))
|
||||
help()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user