Compare commits

..

15 Commits

Author SHA1 Message Date
CrescentLeaf
3617292409 chore: add salt&key in config 2025-09-08 03:10:57 +08:00
CrescentLeaf
a3fc61494e feat: token 2025-09-08 03:10:45 +08:00
CrescentLeaf
fa62180667 feat: login & register 2025-09-08 03:10:36 +08:00
CrescentLeaf
e60c1cf1c4 feat: user password 2025-09-08 03:10:26 +08:00
CrescentLeaf
7e60e4a4be chore: add checkArgsEmpty 2025-09-08 03:09:56 +08:00
CrescentLeaf
f3a9cb8641 chore: add DataWrongError 2025-09-08 03:09:41 +08:00
CrescentLeaf
c577797e57 chore: add DataWrongError 2025-09-08 03:09:21 +08:00
CrescentLeaf
3a7e4970d4 ui: login & register 2025-09-08 03:09:05 +08:00
CrescentLeaf
0e14bb9a45 dep: add socket.io-client 2025-09-08 03:08:11 +08:00
CrescentLeaf
2869a77abd chore: add mising "id" 2025-09-07 22:30:45 +08:00
CrescentLeaf
913d1f395f chore: make lint UNHAPPY 2025-09-07 22:30:31 +08:00
CrescentLeaf
abf06c71af chore: useEventListener allow Ref<null> 2025-09-07 21:53:53 +08:00
CrescentLeaf
afeab61468 chore: make lint unhappy 2025-09-07 21:53:26 +08:00
CrescentLeaf
f06e93ef06 ui: add snackbar util 2025-09-07 20:07:06 +08:00
CrescentLeaf
71b368a5ac feat: 在服務端重新編譯前端 2025-09-07 18:21:49 +08:00
18 changed files with 432 additions and 90 deletions

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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>

View 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
View 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)
}

View File

@@ -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])
}

View File

@@ -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'

View File

@@ -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": {

View File

@@ -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) {}
}

View File

@@ -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)

View File

@@ -0,0 +1 @@
export default class DataWrongError extends Error {}

View 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
}
}

View File

@@ -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
},
}
})
}

View File

@@ -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",
/**

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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()
}
})