Compare commits

...
21 Commits
Author SHA1 Message Date
539943419 79f3cffb5e fix 2024-07-29 15:02:55 +08:00
539943419 5ad3f87323 fix 2024-07-29 14:58:13 +08:00
539943419 5b5849744b ignore visualizer.html 2024-07-29 14:43:06 +08:00
539943419 44f3d57884 统一在store.js中管理baseUrl 2024-07-29 14:41:50 +08:00
539943419 dfc26aedea delete visualizer.html 2024-07-20 21:18:03 +08:00
539943419 1780a0bb8b 添加复制时提示 2024-07-20 21:16:10 +08:00
539943419 cf4297cc3c 修改chat界面样式,增加复制文本消息功能 2024-07-20 09:16:49 +08:00
539943419 b29add4ae5 修正SM4解密时产生错误结果的bug 2024-07-19 19:59:16 +08:00
539943419 f0e4a777d9 完善使用worker进行加密解密相关代码 2024-07-19 17:24:08 +08:00
539943419 3c2dc0f183 使用worker进行加解密,防止主线程卡死 2024-07-19 17:04:54 +08:00
539943419 8d08392cad 修复了chat中图片显示的相关问题 2024-07-07 12:40:25 +08:00
539943419 ba43ad2bbc 添加token过期检查 2024-07-02 20:13:07 +08:00
539943419 84025a83ab 优化vite配置 2024-07-02 19:27:26 +08:00
539943419 17caf491f1 create miaoworkerpool.js 2024-06-29 01:06:00 +08:00
539943419 133d0b7844 现在非开发环境下web端视为服务器端口与页面端口一致 2024-04-23 20:18:45 +08:00
539943419 5ca7125328 chat中可通过ctrl+enter发送消息 2024-04-03 13:00:18 +08:00
539943419 0389c8b7fe 优化细节表现 2024-04-03 12:45:13 +08:00
539943419 dd56403d93 chat中添加上传文件功能 2024-04-03 11:33:46 +08:00
539943419 709cac04f6 更新chat的ui界面 2024-04-02 23:18:54 +08:00
539943419 530e54a596 添加登录功能 2024-03-27 22:05:00 +08:00
539943419 20e9f4940e 修正AES加密字符串中对emoji处理的编码问题 2024-03-22 22:58:12 +08:00
31 changed files with 4106 additions and 345 deletions

No files matched your search

+7
View File
@@ -32,3 +32,10 @@ coverage
# files created with miaoShare service
temp
miaoShare.db
# addUser.js
# removeUser.js
# JWT SK
*.key
visualizer.html
+16
View File
@@ -0,0 +1,16 @@
import sha256 from 'crypto-js/sha256.js';
import Database from "better-sqlite3";
const dbPath = "./miaoShare.db";
const db = new Database(dbPath, { verbose: null });
const stmt_insertId2FileName = db.prepare(`INSERT INTO USERS (user_id, password_hash, user_group, uploadFilesSizeCount) VALUES (?, ?, ?, 0)`);
let userList = [
['539943419','zjh030628','admin'],
// ['guest','guest','guest']
].map(a=>{a[1]=sha256(a[0]+sha256(a[1]).toString()).toString();return a})
for(let i of userList){
stmt_insertId2FileName.run(...i)
}
+11 -1
View File
@@ -1,3 +1,13 @@
{
"PORT":17057
"PORT":17057,
"userGroupSetting":{
"admin":{
"cloudCapacity":2147483648
},
"guest":{
"cloudCapacity":2147483648
}
},
"enableGuest":false,
"tokenExpiresIn":168
}
+130 -12
View File
@@ -12,7 +12,9 @@
"crypto-js": "^4.2.0",
"express": "^4.18.2",
"file-saver": "^2.0.5",
"js-base64": "^3.7.6",
"js-base64": "^3.7.7",
"jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3",
"pinia": "^2.1.7",
"sm-crypto": "^0.3.13",
"uuid": "^9.0.1",
@@ -24,12 +26,13 @@
"ws": "^8.16.0"
},
"devDependencies": {
"@vicons/fluent": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue": "^5.0.3",
"better-sqlite3": "^8.7.0",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"naive-ui": "^2.37.3",
"naive-ui": "^2.38.1",
"vfonts": "^0.0.3",
"vite": "^5.0.11"
}
@@ -656,6 +659,12 @@
"@types/lodash": "*"
}
},
"node_modules/@vicons/fluent": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/@vicons/fluent/-/fluent-0.12.0.tgz",
"integrity": "sha512-ATCiqPuiJ6RI5GBlD3BIpZ9Xw4MsCA4RpI5oR6MCti4quS4mX1Gp6N74FCzw7lgOj+80rV4HMKhZTVInwimpVQ==",
"dev": true
},
"node_modules/@vicons/ionicons5": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz",
@@ -889,6 +898,11 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
@@ -1174,6 +1188,14 @@
"node": ">=8"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
@@ -1568,15 +1590,60 @@
}
},
"node_modules/js-base64": {
"version": "3.7.6",
"resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.6.tgz",
"integrity": "sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA=="
"version": "3.7.7",
"resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
@@ -1589,11 +1656,45 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -1690,9 +1791,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/naive-ui": {
"version": "2.37.3",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.37.3.tgz",
"integrity": "sha512-aUkHFXVIluSi8Me+npbcsdv1NYhVMj5t9YaruoCESlqmfqspj+R2QHEVXkTtUI1kQwVrABMCtAGq/wountqjZA==",
"version": "2.38.1",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.38.1.tgz",
"integrity": "sha512-AnU1FQ7K/CbhguAX++V4kCFjk7h7RvWt4nvZPRjORMpq+fUIlzD+EcQ5Cv1VqDloNF8+eMv4Akc2Ogacc9S+5A==",
"dev": true,
"dependencies": {
"@css-render/plugin-bem": "^0.15.12",
@@ -1756,6 +1857,25 @@
"node": ">=10"
}
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-3.0.3.tgz",
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"dependencies": {
"uuid": "8.3.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-cron/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
@@ -2056,7 +2176,6 @@
"version": "7.6.0",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -2581,8 +2700,7 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yargs": {
"version": "17.7.2",
+8 -2
View File
@@ -16,8 +16,11 @@
"crypto-js": "^4.2.0",
"express": "^4.18.2",
"file-saver": "^2.0.5",
"js-base64": "^3.7.6",
"js-base64": "^3.7.7",
"jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3",
"pinia": "^2.1.7",
"rollup-plugin-visualizer": "^5.12.0",
"sm-crypto": "^0.3.13",
"uuid": "^9.0.1",
"vicons": "^0.0.1",
@@ -25,15 +28,18 @@
"vue-clipboard3": "^2.0.0",
"vue-router": "^4.2.5",
"websocket-heartbeat-js": "^1.1.3",
"workerpool": "^9.1.3",
"ws": "^8.16.0"
},
"devDependencies": {
"@vicons/fluent": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue": "^5.0.3",
"better-sqlite3": "^8.7.0",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"naive-ui": "^2.37.3",
"naive-ui": "^2.38.1",
"sass": "^1.77.8",
"vfonts": "^0.0.3",
"vite": "^5.0.11"
}
+2386
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+15
View File
@@ -0,0 +1,15 @@
import sha256 from 'crypto-js/sha256.js';
import Database from "better-sqlite3";
const dbPath = "./miaoShare.db";
const db = new Database(dbPath, { verbose: null });
const stmt_ = db.prepare(`DELETE FROM USERS WHERE user_id = ?`);
let userList = [
['admin'],["guest"]
]
for(let i of userList){
stmt_.run(...i)
}
+244 -48
View File
@@ -1,3 +1,7 @@
/*
仅作为临时测试使用
后续对这个进行拆分
*/
import express from "express";
import cors from "cors";
import Database from "better-sqlite3";
@@ -5,15 +9,26 @@ import fs from "fs";
import { WebSocketServer } from "ws";
import http from "http";
import { v4 as uuid4 } from "uuid";
// import Config from "./miaoShareConfig.json"
import jwt from "jsonwebtoken";
import sha256 from "crypto-js/sha256.js";
import Config from "./miaoShareConfig.json" assert { type: "json" };
import { verify } from "crypto";
// function loadJSON(path){
// return JSON.parse(fs.readFileSync(path).toString());
// }
// const Config = loadJSON("./miaoShareConfig.json");
// 屏蔽console.log
console.log=()=>{}
const HOST = "0.0.0.0";
// const PORT = Config.PORT
const PORT = 17057;
const PORT = Config.PORT;
// const PORT = 17057;
const fileDir = "./temp";
const dbPath = "./miaoShare.db";
const PATH = "./dist";
const JWT_SK = fs.readFileSync("JWT_SK.key").toString();
// 上传文件最大大小:200MB
const uploadMaxSize = 200 * 1024 * 1024;
// 一天的毫秒数
@@ -24,27 +39,46 @@ const LifeOfFile = millisecondsOfOneDay * 3;
// 数据库相关
const db = new Database(dbPath, { verbose: null });
db.prepare(
db.exec(
`CREATE TABLE IF NOT EXISTS id2fileName (
id CHAR(8) PRIMARY KEY NOT NULL UNIQUE,
fileName TEXT NOT NULL,
timestamp INT NOT NULL,
freeze tinyint(1) NOT NULL,
downloadCredit INT NOT NULL
downloadCredit INT NOT NULL,
fileSize INT NOT NULL,
sender_id CHAR(32) NOT NULL
);`
).run();
);
db.exec(`
CREATE TABLE IF NOT EXISTS USERS(
user_id CHAR(32) PRIMARY KEY UNIQUE,
password_hash CHAR(32) NOT NULL,
user_group VARCHAR(32) NOT NULL,
uploadFilesSizeCount BIGINT NOT NULL,
email CHAR(64)
);`);
const stmt_getPermissionGroup = db.prepare("SELECT user_group FROM USERS WHERE user_id = ? AND password_hash = ?");
const stmt_getFileName = db.prepare(`SELECT fileName FROM id2fileName WHERE id = ?`);
const stmt_getFileInfo = db.prepare(`SELECT fileName,fileSize FROM id2fileName WHERE id = ?`);
const stmt_getUserUploadFilesSizeCount = db.prepare("SELECT uploadFilesSizeCount FROM USERS WHERE user_id = ?");
const stmt_getUserGroup = db.prepare("SELECT user_group FROM USERS WHERE user_id = ?");
const stmt_getTimestamp = db.prepare(`SELECT timestamp FROM id2fileName WHERE id = ?`);
const stmt_getFileSize = db.prepare(`SELECT fileSize FROM id2fileName WHERE id = ?`);
const stmt_getFreeze = db.prepare(`SELECT freeze FROM id2fileName WHERE id = ?`);
const stmt_getDownloadCredit = db.prepare(`SELECT downloadCredit FROM id2fileName WHERE id = ?`);
const stmt_getFreezenIDs = db.prepare(`SELECT id FROM id2fileName WHERE freeze = 1`);
const stmt_insertId2FileName = db.prepare(`INSERT INTO id2fileName (id, fileName, timestamp, freeze, downloadCredit) VALUES (?, ?, ?, ?, ?)`);
const stmt_getFreezenIDs = db.prepare(`SELECT id,fileSize,sender_id FROM id2fileName WHERE freeze = 1`);
const stmt_insertId2FileName = db.prepare(
`INSERT INTO id2fileName (id, fileName, timestamp, freeze, downloadCredit, fileSize, sender_id) VALUES (?, ?, ?, ?, ?, ?, ?)`
);
const stmt_deleteExpireData = db.prepare(`DELETE FROM id2fileName WHERE freeze=1`);
const stmt_freezeExpireData = db.prepare(`UPDATE id2fileName SET freeze=1 WHERE timestamp < ? OR downloadCredit < 1`);
const stmt_updateDownloadCredit = db.prepare(`UPDATE id2fileName SET downloadCredit=downloadCredit-1 WHERE id = ?`);
const stmt_updateUserUploadFilesSizeCount = db.prepare(`UPDATE USERS SET uploadFilesSizeCount=uploadFilesSizeCount+? WHERE user_id = ?`);
const stmt_updateUserUploadFilesSizeCount_d = db.prepare(`UPDATE USERS SET uploadFilesSizeCount=uploadFilesSizeCount-? WHERE user_id = ?`);
const db_insert = (id, fileName, downloadCredit) => {
stmt_insertId2FileName.run(id, fileName, parseInt(Date.now()), 0, downloadCredit);
const db_insert = (id, fileName, downloadCredit, fileSize, senderId) => {
stmt_insertId2FileName.run(id, fileName, parseInt(Date.now()), 0, downloadCredit, fileSize, senderId);
};
const db_getFileName = id => {
try {
@@ -54,6 +88,30 @@ const db_getFileName = id => {
return null;
}
};
// const db_getFileInfo = id => {
// try {
// if (stmt_getFreeze.get(id)["freeze"] == 1) return null;
// return stmt_getFileSize.get(id);
// } catch (e) {
// return null;
// }
// };
const db_getFileInfo = (function () {
const infoCache = {};
return function (id) {
if (infoCache[id]) {
return infoCache[id];
}
try {
if (stmt_getFreeze.get(id)["freeze"] == 1) return null;
const info = stmt_getFileInfo.get(id);
infoCache[id] = info;
return info;
} catch (e) {
return null;
}
};
})();
// 检查是否可下载
const db_downloadable = id => {
try {
@@ -78,6 +136,22 @@ const db_getTimestamp = id => {
return stmt_getTimestamp.get(id)["timestamp"];
};
const db_getFileSize = id => {
return stmt_getFileSize.get(id)["fileSize"];
};
const db_getUserUploadFilesSizeCount = id => {
return stmt_getUserUploadFilesSizeCount.get(id)["uploadFilesSizeCount"];
};
function db_updateUserUploadFilesSizeCount(id, size) {
return stmt_updateUserUploadFilesSizeCount.run(size, id);
}
// 减
function db_updateUserUploadFilesSizeCount_d(id, size) {
return stmt_updateUserUploadFilesSizeCount_d.run(size, id);
}
// 更新存储的文件,删除已经被冻结的文件以及相关记录,然后冻结过期文件
const updateDBAndFiles = () => {
try {
@@ -85,6 +159,9 @@ const updateDBAndFiles = () => {
stmt_deleteExpireData.run();
for (let row of rows) {
let fileName = row["id"];
let sid = row["sender_id"];
let fsize = row["fileSize"];
db_updateUserUploadFilesSizeCount_d(sid, fsize);
let path = fileDir + "/" + fileName;
try {
fs.unlink(path, e => {
@@ -117,6 +194,15 @@ if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir);
}
function db_getPermissionGroup(id, pwdH) {
let res = stmt_getPermissionGroup.get(id, pwdH);
// console.log(id,pwdH,res)
if (res && res.user_group) {
return res.user_group;
}
return null;
}
function generateId(isGID) {
var returnStr = "",
range = 8,
@@ -134,10 +220,11 @@ function generateId(isGID) {
}
// 上传文件接口
function uploadFile(req, res, fileName, downloadCredit) {
function uploadFile(req, res, fileName, downloadCredit, fileSize) {
let id = generateId();
const fileStream = fs.createWriteStream(fileDir + "/" + id);
db_insert(id, fileName, downloadCredit);
db_insert(id, fileName, downloadCredit, fileSize, req.sender_id);
db_updateUserUploadFilesSizeCount(req.sender_id, fileSize);
// tempMap[id] = fileName;
// console.log(tempMap);
req.pipe(fileStream);
@@ -184,9 +271,99 @@ function downloadFile(req, res, fileId) {
}
}
function getFileInfo(req, res, fileId) {
try {
if (db_downloadable(fileId) == false) {
res.writeHead(404);
res.end(null);
return;
}
// db_updateDownloadCredit(fileId);
let info = db_getFileInfo(fileId);
if (info == null) {
res.writeHead(404);
res.end(null);
return;
}
res.end(JSON.stringify(info));
} catch (err) {
console.log(err);
res.writeHead(404);
res.end(null);
}
}
const getUserGroup = (function () {
const cache = {};
return function (userId) {
if (!cache[userId]) {
cache[userId] = stmt_getUserGroup.get(userId)["user_group"];
}
return cache[userId];
};
})();
// 验证token
const verifyToken = (req, res, next) => {
try {
const token = req.headers.authorization.split(" ")[1]; // 从Authorization头中提取JWT
console.log(req.headers.authorization, token);
let verifyData = jwt.verify(token, JWT_SK);
if (verifyData.exp > parseInt(Date.now())) {
throw "token has expired";
}
req.sender_id = verifyData.id;
next();
} catch (err) {
console.log(err);
return res.status(401).json({ message: "Invalid token", err });
}
};
app.use(express.static(PATH));
app.post("/user/login", express.json(), function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "POST");
// console.log(req.body)
let { id, pwd } = req.body;
console.log(req.body);
let pwdH = sha256(id + pwd).toString();
let userGroup = db_getPermissionGroup(id, pwdH);
if (userGroup !== null) {
res.writeHead(200);
let token = jwt.sign({ id, pwd }, JWT_SK, { expiresIn: `${Config.tokenExpiresIn}h` });
res.end(JSON.stringify({ status: "Success", userGroup, token }));
} else {
res.end(JSON.stringify({ status: "Error" }));
}
// console.log(req)
});
app.post("/user/refrashToken", express.json(), function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "POST");
// console.log(req.body)
try {
const token = req.headers.authorization.split(" ")[1]; // 从Authorization头中提取JWT
// const token = req.body.token
console.log(token)
let verifyData = jwt.verify(token, JWT_SK);
if (verifyData.exp > parseInt(Date.now())) {
return res.status(401).json({ status: "Error", message: "Invalid token" });
}
let id = verifyData.id;
let pwd = verifyData.pwd;
let new_token = jwt.sign({ id, pwd }, JWT_SK, { expiresIn: `${Config.tokenExpiresIn}h` });
console.log({ token, new_token });
res.end(JSON.stringify({ status: "Success", new_token }));
} catch (e) {
return res.status(401).json({ status: "Error", message: "Invalid token" });
}
});
// app.all()
app.post("/file/upload", function (req, res) {
app.post("/file/upload", verifyToken, function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "POST");
@@ -198,15 +375,18 @@ app.post("/file/upload", function (req, res) {
if (!fileName) {
res.statusCode = 500;
res.end("FileName Not Found");
} else if (fileSize > uploadMaxSize) {
} else if (
fileSize > uploadMaxSize ||
db_getUserUploadFilesSizeCount(req.sender_id) + fileSize > Config.userGroupSetting[getUserGroup(req.sender_id)].cloudCapacity
) {
res.statusCode = 500;
res.end("Maximum size limit exceeded");
} else {
uploadFile(req, res, fileName, downloadCredit);
uploadFile(req, res, fileName, downloadCredit, fileSize);
}
});
app.get("/file/download", function (req, res) {
app.get("/file/download", verifyToken, function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "GET");
@@ -220,6 +400,20 @@ app.get("/file/download", function (req, res) {
}
});
app.get("/file/info", verifyToken, function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "GET");
res.header("Access-Control-Expose-Headers", "File-Name");
var fileId = req.query.fileName;
if (!fileId) {
res.statusCode = 500;
res.end("FileName Not Found");
} else {
getFileInfo(req, res, fileId);
}
});
// // 启动服务器
// app.listen(PORT, HOST, function(){
// console.log(app)
@@ -230,33 +424,33 @@ const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// 先实现最低限度功能,暂时不考虑身份认证
const groupsMap = {};
// 三天不活跃的群组将被清除
const LifeOfGroup = millisecondsOfOneDay * 3;
// 一天不活跃的群组将被清除
const LifeOfGroup = millisecondsOfOneDay * 1;
// const LifeOfGroup = 0;
// 根据uuid查询加入的群组
const uuid2Groups={}
const uuid2Groups = {};
db.exec(
`CREATE TABLE IF NOT EXISTS GROUPS (
`CREATE TABLE IF NOT EXISTS CHATGROUPS (
gID CHAR(8) PRIMARY KEY UNIQUE NOT NULL,
verify TEXT NOT NULL,
activeTimestamp INT NOT NULL
);`
);
const stmt_getExpiredGroups = db.prepare(`SELECT gID FROM GROUPS WHERE activeTimestamp < ? `);
const stmt_getExpiredGroups = db.prepare(`SELECT gID FROM CHATGROUPS WHERE activeTimestamp < ? `);
const stmt_getGroupVerify = db.prepare(`SELECT verify FROM GROUPS WHERE gID = ? `);
const stmt_getGroupVerify = db.prepare(`SELECT verify FROM CHATGROUPS WHERE gID = ? `);
const stmt_insertGROUPS = db.prepare(`INSERT INTO GROUPS (gID,verify, activeTimestamp) VALUES (?, ?, ?)`);
const stmt_insertCHATGROUPS = db.prepare(`INSERT INTO CHATGROUPS (gID,verify, activeTimestamp) VALUES (?, ?, ?)`);
const stmt_updateGROUPS_ActiveTimestamp = db.prepare(`UPDATE GROUPS SET activeTimestamp=? WHERE gID=?`);
const stmt_updateCHATGROUPS_ActiveTimestamp = db.prepare(`UPDATE CHATGROUPS SET activeTimestamp=? WHERE gID=?`);
const stmt_deleteGroup = db.prepare(`DELETE FROM GROUPS WHERE gID=?`);
const stmt_deleteGroup = db.prepare(`DELETE FROM CHATGROUPS WHERE gID=?`);
const generateStmt_insertMessage_map = {};
const updateGROUPS = () => {
const updateCHATGROUPS = () => {
// console.log(1)
try {
let gIDs = stmt_getExpiredGroups.all(parseInt(Date.now()) - LifeOfGroup);
@@ -270,8 +464,8 @@ const updateGROUPS = () => {
return;
}
};
updateGROUPS();
setInterval(updateGROUPS, 1000 * 3600 * 3);
updateCHATGROUPS();
setInterval(updateCHATGROUPS, 1000 * 3600 * 3);
// 创建群组的数据库
function createGroupTableDB(gID, verify) {
@@ -279,23 +473,24 @@ function createGroupTableDB(gID, verify) {
`CREATE TABLE IF NOT EXISTS ${gID} (
message TEXT NOT NULL,
type TEXT NOT NULL,
type1 TEXT NOT NULL,
sender TEXT NOT NULL,
timestamp INT NOT NULL
);`
);
stmt_insertGROUPS.run(gID, verify, parseInt(Date.now()));
stmt_insertCHATGROUPS.run(gID, verify, parseInt(Date.now()));
}
// 插入信息
function insertMessageByGID(gID, msg, from) {
function insertMessageByGID(gID, msg, type1, from, timestamp) {
if (!generateStmt_insertMessage_map[gID]) {
generateStmt_insertMessage_map[gID] = db.prepare(`INSERT INTO ${gID} (message,type,sender, timestamp) VALUES (?, ?, ?, ?)`);
generateStmt_insertMessage_map[gID] = db.prepare(`INSERT INTO ${gID} (message,type,type1,sender, timestamp) VALUES (?, ?, ?, ?, ?)`);
}
generateStmt_insertMessage_map[gID].run(msg, "gMsg", from, parseInt(Date.now()));
generateStmt_insertMessage_map[gID].run(msg, "gMsg", type1, from, timestamp);
}
function updateActiveTimestamp(gID) {
stmt_updateGROUPS_ActiveTimestamp.run(parseInt(Date.now()), gID);
stmt_updateCHATGROUPS_ActiveTimestamp.run(parseInt(Date.now()), gID);
}
function createGroup(verify) {
@@ -307,13 +502,13 @@ function createGroup(verify) {
}
function getGroupVerify(gID) {
initMsgHistory(gID)
initMsgHistory(gID);
return groupsMap[gID].verify;
}
// 正式加入群组
function joinGroup(socket, gID, callback) {
initMsgHistory(gID)
initMsgHistory(gID);
groupsMap[gID].member.push(socket);
updateActiveTimestamp(gID);
callback();
@@ -321,31 +516,32 @@ function joinGroup(socket, gID, callback) {
// 暂时这么写
function getHistory(gID) {
initMsgHistory(gID)
initMsgHistory(gID);
return groupsMap[gID].msgHistory;
}
function broadcastMsg(gID, msg, from) {
initMsgHistory(gID)
function broadcastMsg(gID, msg, type1, from) {
const timestamp = parseInt(Date.now());
initMsgHistory(gID);
updateActiveTimestamp(gID);
insertMessageByGID(gID, msg, from);
insertMessageByGID(gID, msg, type1, from, timestamp);
let bmsg = JSON.stringify({ type: "gMsg", gID, msg });
groupsMap[gID].msgHistory.push({ type: "gMsg", msg, from });
let bmsg = JSON.stringify({ type: `gMsg`, type1, gID, msg, from, timestamp });
groupsMap[gID].msgHistory.push({ type: `gMsg`, type1, msg, from, timestamp });
for (let socket of groupsMap[gID].member) {
socket.send(bmsg);
}
}
function initMsgHistory(gID){
function initMsgHistory(gID) {
if (!groupsMap[gID]) {
let verify = stmt_getGroupVerify.get(gID).verify;
// 暂时这么写
let msgHistory = db
.prepare(`SELECT message,type FROM ${gID}`)
.prepare(`SELECT message,type,type1,sender,timestamp FROM ${gID}`)
.all()
.map(v => {
return { msg: v.message, type: v.type };
return { type: v.type, type1: v.type1, msg: v.message, from: v.sender, timestamp: v.timestamp };
});
// console.log(msgHistory)
groupsMap[gID] = { verify, member: [], msgHistory };
@@ -358,8 +554,8 @@ wss.on("connection", socket => {
const no = wss.clients.size;
const joinedGroup = [];
var uuid = uuid4();
function setUuid(new_uuid){
uuid = new_uuid
function setUuid(new_uuid) {
uuid = new_uuid;
}
socket.on("message", message => {
try {
@@ -391,14 +587,14 @@ wss.on("connection", socket => {
}
} else if (type === "message") {
let gID = msg.gID;
broadcastMsg(gID, msg.msg,uuid);
broadcastMsg(gID, msg.msg, msg.type1, uuid);
} else if (type === "heartbeat") {
socket.send(JSON.stringify({ type: "sMsg", type1: "heartbeat" }));
} else if (type === "uuid") {
if (msg.type1 === "get") {
socket.send(JSON.stringify({ type: "sMsg", type1: "uuid", uuid }));
} else if (msg.type1 === "set") {
uuid = msg.uuid
uuid = msg.uuid;
}
}
}
+115 -44
View File
@@ -3,87 +3,157 @@ import upload from "./views/uploadFileMiao.vue";
import download from "./views/downloadFileMiao.vue";
import setting from "./views/setting.vue";
import chat from "./views/chat.vue";
import { NNotificationProvider, NMessageProvider, NCard, NMenu, NIcon } from 'naive-ui'
import { ref, watch, h ,shallowRef} from "vue";
// import { RouterView, useRouter } from "vue-router";
import login from "./views/login.vue";
import account from "./views/accountInfo.vue";
import getQueryString from '@/func/getQuery'
import { NNotificationProvider, NMessageProvider, NCard, NMenu, NIcon, NModalProvider } from 'naive-ui'
import { ref, watch, h, shallowRef, onMounted, computed } from "vue";
// import { useRoute } from "vue-router";
import {
ArrowUpCircleOutline as uploadIcon,
ArrowDownCircleOutline as downloadIcon,
ChatboxEllipsesOutline as chatIcon,
SettingsOutline as settingIcon
CloudUploadOutline as uploadIcon,
CloudDownloadOutline as downloadIcon,
// ChatboxEllipsesOutline as chatIcon,
ChatbubblesOutline as chatIcon,
SettingsOutline as settingIcon,
FootstepsOutline as AccountIcon
} from '@vicons/ionicons5'
import { useSettingStore } from "@/pinia/store";
import config from "../miaoShareConfig.json"
// console.log(process.env.NODE_ENV)
const settingData = useSettingStore();
const serverPort = settingData.serverPort;
const baseUrl = settingData.baseUrl
const tokenExpiresIn = config.tokenExpiresIn
const activePage = ref('upload')
const isComponent = shallowRef(upload)
watch(activePage, () => {
isComponent.value={upload,download,setting,chat}[activePage.value]
isComponent.value = { upload, download, setting, chat, login, account }[activePage.value]
})
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}
const menuOptions = [
const menuOptions = ref([
{
label: '上传',
key: 'upload',
icon: renderIcon(uploadIcon),
disabled: computed(() => {
return settingData.token === null
})
},
{
label: '下载',
key: 'download',
icon: renderIcon(downloadIcon),
disabled: computed(() => {
return settingData.token === null
})
},
{
label: 'Chat',
key: 'chat',
icon: renderIcon(chatIcon),
disabled: computed(() => {
return settingData.token === null
})
},
{
label: '设置',
key: 'setting',
icon: renderIcon(settingIcon),
disabled: computed(() => {
return settingData.token === null
})
},
{
label: '账户',
key: 'account',
icon: renderIcon(AccountIcon),
disabled: computed(() => {
return settingData.token === null
})
}
]
])
// function getQueryString(key) {
// var reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)', 'i');
// var r = window.location.search.substr(1).match(reg);
// if (r != null) {
// return unescape(r[2]);
// }
// return null;
// }
onMounted(async () => {
let aP = getQueryString('aP')
// console.log(aP)
if (aP) {
activePage.value = aP
}
if (settingData.getToken() === null) {
aP = activePage.value
activePage.value = 'login'
// 登录后跳原来的activePage
const w = watch(() => settingData.token, () => {
activePage.value = aP
w()
})
} else {
// 刷新token
(async function refreshToken() {
const old_token = settingData.getToken()
console.log(old_token)
const response = await fetch(`${baseUrl}user/refrashToken`, {
// credentials: 'include',
method: "POST",
headers: {
'Authorization': `Bearer ${settingData.token}`,
"Content-Type": "application/json"
},
// body: JSON.stringify({
// token:settingData.token
// })
});
console.log(response)
let data = await response.json()
if (data.status === 'Error') {
console.log('falied')
} else {
localStorage.setItem('token', data.new_token)
localStorage.setItem('tokenExpiredTime', parseInt(Date.now()) + parseInt(tokenExpiresIn) * 3600000)
}
})()
}
})
</script>
<template>
<div id="windows">
<n-notification-provider placement="bottom-right" :max="3">
<n-message-provider>
<n-card content-style="padding: 0; margin:0;" id="theCard">
<div id="menu">
<n-menu mode="horizontal" :responsive="true" v-model:value="activePage" :options="menuOptions" :icon-size="26"></n-menu>
</div>
<n-modal-provider>
<n-card content-style="padding: 0; margin:0" id="theCard">
<div id="menu">
<n-menu mode="horizontal" :responsive="true" v-model:value="activePage"
:options="menuOptions" :icon-size="26"></n-menu>
</div>
<div style="padding: 20px;">
<KeepAlive>
<component :is="isComponent"></component>
</KeepAlive>
<!-- <router-view v-slot="{ Component }">
<keep-alive :include=['uploadFileMiao']>
<component :is="Component" :key="$route.fullPath"/>
</keep-alive>
</router-view> -->
</div>
<!-- <n-tabs type="line" size="large" :tabs-padding="20" pane-style="padding: 20px;"
display-directive="show">
<n-tab-pane name="上传文件">
<uploadFileMiao style />
</n-tab-pane>
<n-tab-pane name="下载文件">
<downloadFileMiao style />
</n-tab-pane>
<n-tab-pane name="miaoChat">
<chat style />
</n-tab-pane>
<n-tab-pane name="设置">
<settingMiao style="max-width:100%" />
</n-tab-pane>
</n-tabs> -->
</n-card>
<div style="padding: 20px;">
<KeepAlive>
<component :is="isComponent"></component>
</KeepAlive>
</div>
</n-card>
</n-modal-provider>
</n-message-provider>
</n-notification-provider>
</div>
@@ -105,14 +175,15 @@ const menuOptions = [
height: 90vh;
}
#menu{
width: 100%;
#menu {
width: 100%;
height: auto;
/* background-color: blue; */
padding-top: 5px;
border-bottom: 1px solid rgb(60, 60, 60, 0.1);
border-bottom: 1px solid rgb(60, 60, 60, 0.1);
user-select: none
}
@media screen and (max-width:768px) {
#theCard {
width: 100vw;
+320
View File
@@ -0,0 +1,320 @@
<template>
<!-- <div style="border: 1px solid rgb(239, 239, 245);border-radius: 5px;position: relative;"> -->
<n-scrollbar class="scrollbar">
<n-image-group>
<div v-for="msg of props.messages">
<div class="message-title">{{ `${msg.from.split('-')[0]} ${msg.timestampString}` }}</div>
<div class="message-text" v-if="msg.type1 === 'text'">
<span>{{ msg.msg }}</span>
<n-icon :component="CopyOutline" class="message-text-copy" @click='_toClipboard(msg.msg)' />
</div>
<div v-if="msg.type1 === 'img'">
<span>
<n-image :src="imgUrlCache[msg.msg]" width="100" style="margin-top: 5px;border-radius: 5px;" />
<n-skeleton v-show="!imgUrlCache[msg.msg]" height="80px" width="100px" :sharp="false"
style="border-radius: 5px;" />
</span>
</div>
<div v-if="msg.type1 === 'file'" class="message-file">
<n-flex class="message-file-content" @click="downloadFile(msg.msg)">
<n-icon :component="DownloadIcon" size="50" style="margin-top: 0px;"></n-icon>
<n-flex vertical v-if="fileInfoCache[msg.msg]">
<div style="font-weight: 500;margin-top: 8px;font-size: large;">
{{ fileInfoCache[msg.msg].fileName ? fileInfoCache[msg.msg].fileName : 'Unknow' }}
</div>
<n-flex style="margin-bottom: 8px;" justify="space-between">
<div>
{{ fileInfoCache[msg.msg].fileSize ? fileInfoCache[msg.msg].fileSize : 'Unknow'
}}
</div>
<!-- lets show u a bullshit -->
<div>
{{ fileStatus2String[fileInfoCache[msg.msg].status ? fileInfoCache[msg.msg].status :
'Unknow'] ? fileStatus2String[fileInfoCache[msg.msg].status ?
fileInfoCache[msg.msg].status : 'Unknow'] : fileInfoCache[msg.msg].status ?
fileInfoCache[msg.msg].status : 'Unknow' }}
</div>
</n-flex>
</n-flex>
</n-flex>
<!-- {{ fileInfoCache[msg.msg] }} -->
</div>
</div>
<n-empty description="无可显示消息" v-show="props.messages.length === 0" style="margin-top: 25vh;">
<template #extra>
</template>
</n-empty>
<div style="height: 30px;" v-show="props.messages.length !== 0"></div>
</n-image-group>
</n-scrollbar>
<!-- </div> -->
</template>
<script setup>
import { NScrollbar, NEmpty, NImage, NImageGroup, NSkeleton, NIcon, NFlex } from 'naive-ui'
import { ref, watch } from 'vue';
import { useSettingStore } from "@/pinia/store";
import { decrypt, decryptString, downloadFromUint8Array } from '../func/download'
import { CubeOutline as DownloadIcon, CopyOutline } from '@vicons/ionicons5'
import fileSize2String from '@/func/fileSize2String'
import useClipboard from "vue-clipboard3";
import { useMessage } from "naive-ui";
const message = useMessage()
const { toClipboard } = useClipboard();
const _toClipboard = async (text) => {
await toClipboard(text)
message.info(`已复制 ${text.length < 10 ? text : text.substring(0,7) + '...'}`)
}
const setting = useSettingStore();
const serverPort = setting.serverPort;
const baseUrl = setting.baseUrl
const props = defineProps({
messages: {
type: Array,
required: true
},
})
async function getFileInfo(code) {
let id = code.substring(0, 8);
const params = {
fileName: id
};
const options = {
method: "GET",
headers: {
'Authorization': `Bearer ${setting.token}`
// 'Content-Type':'application/json'
},
};
let response = await fetch(baseUrl + "file/info?" + new URLSearchParams(params), options)
let info = await response.json()
return info
}
async function downloadFile(code) {
if (fileInfoCache.value[code].status === 'waittingDownload') {
fileInfoCache.value[code].status = `downloading`;
return new Promise((resolve, reject) => {
let id = code.substring(0, 8);
let key = code.substring(8);
const params = {
fileName: id
};
const options = {
method: "GET",
headers: {
'Authorization': `Bearer ${setting.token}`
},
};
fetch(baseUrl + "file/download?" + new URLSearchParams(params), options)
.then(res => {
// fileName = res.headers.get("File-Name");
// fileName = decryptString(key, fileName);
// updateDownloadQuene(code, "fileName", fileName);
const total = +res.headers.get("Content-Length");
let loaded = 0;
const clone = res.clone();
const reader = res.body.getReader();
// 感觉怪怪的
const read = async () => {
const { done, value } = await reader.read();
if (done) {
// console.log(clone)
return clone;
}
loaded += value.length;
const percent = (loaded / total) * 100;
fileInfoCache.value[code].status = `下载进度:${percent.toFixed(2)}%`;
return read();
};
return read();
})
.then(res => {
var data = res.blob();
return data;
})
.then(data => {
decrypt(key, data).then(data => {
fileInfoCache.value[code].status = `finish`;
fileInfoCache.value[code].data = data
downloadFromUint8Array(data, fileInfoCache.value[code].fileName)
resolve(data)
});
})
.catch(err => {
fileInfoCache.value[code].status = `error`;
console.error(err);
});
})
} else if (fileInfoCache.value[code].status === 'finish') {
downloadFromUint8Array(fileInfoCache.value[code].data, fileInfoCache.value[code].fileName)
}
};
async function downloadImage(code) {
return new Promise((resolve, reject) => {
var id = code.substring(0, 8);
var key = code.substring(8);
var fileName;
const params = {
fileName: id
};
const options = {
method: "GET",
headers: {
'Authorization': `Bearer ${setting.token}`
// 'Content-Type':'application/json'
},
};
fetch(baseUrl + "file/download?" + new URLSearchParams(params), options)
.then(res => {
// fileName = res.headers.get("File-Name");
// fileName = decryptString(key, fileName);
// updateDownloadQuene(code, "fileName", fileName);
const total = +res.headers.get("Content-Length");
let loaded = 0;
const clone = res.clone();
const reader = res.body.getReader();
// 感觉怪怪的
const read = async () => {
const { done, value } = await reader.read();
if (done) {
// console.log(clone)
return clone;
}
loaded += value.length;
// const percent = (loaded / total) * 100;
// downloadProcess['downloadProcess'] = `下载进度:${percent.toFixed(2)}%`;
// updateDownloadQuene(
// code,
// "downloadProcess",
// `下载进度:${percent.toFixed(2)}%`
// );
// downloadProcess.value = percent.toFixed(2);
return read();
};
return read();
})
.then(res => {
var data = res.blob();
return data;
})
.then(data => {
// downloadProcess['downloadProcess'] = '解密中'
// updateDownloadQuene(code, "downloadProcess", `解密中`);
decrypt(key, data).then(data => {
// updateDownloadQuene(code, "downloadProcess", `已完成`);
// const url = URL.createObjectURL(new Blob(data))
resolve(data)
});
})
.catch(err => {
// updateDownloadQuene(
// code,
// "fileName",
// `出错,可能是文件过期或下载次数用完`
// );
// updateDownloadQuene(code, "downloadProcess", `出错`);
console.error(err);
});
})
};
const imgUrlCache = ref({})
const fileInfoCache = ref({})
const fileStatus2String = ref({
waittingDownload: '点击下载',
downloading: '下载中',
finish: '下载完成',
Unknow: '获取文件信息中',
error: '出错'
})
const imgDownloadingQueue = []
const getFileInfoQueue = []
watch(() => props.messages, async () => {
for (let msg of props.messages) {
if (msg.type1 === 'img') {
if (!imgDownloadingQueue.includes(msg.msg)) {
imgDownloadingQueue.push(msg.msg)
const imgData = await downloadImage(msg.msg)
const url = URL.createObjectURL(new Blob([imgData], { type: "image/png" }))
imgUrlCache.value[msg.msg] = url
}
} else if (msg.type1 === 'file') {
if (!getFileInfoQueue.includes(msg.msg)) {
getFileInfoQueue.push(msg.msg)
let key = msg.msg.substring(8);
const info = await getFileInfo(msg.msg)
info.fileName = decryptString(key, info.fileName)
info.fileSize = fileSize2String(info.fileSize)
info.status = 'waittingDownload'
fileInfoCache.value[msg.msg] = info
console.log(fileInfoCache.value)
}
}
}
})
</script>
<style scoped lang="scss">
.scrollbar {
padding: 10px;
padding-right: 15px;
border: 1px solid rgb(239, 239, 245);
border-radius: 5px;
position: relative;
.message-title {
color: slategrey;
}
.message-text {
position: relative;
background-color: rgb(239, 239, 245);
padding: 2px 7px;
// padding-right: 5px;
width: fit-content;
max-width: calc(100% - 28px);
border-radius: 5px;
white-space: pre-wrap;
.message-text-copy {
cursor: pointer;
position: absolute;
bottom: 8px;
right: -10px;
height: 7px;
width: 7px;
opacity: 0;
}
}
.message-text:hover .message-text-copy {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.message-text:not(:hover) .message-text-copy {
opacity: 0;
transition: opacity 0.3s ease-in-out;
transition-delay: 0.5s;
}
.message-file {
background-color: rgb(239, 239, 245);
padding: 2px 7px;
width: fit-content;
border-radius: 5px;
.message-file-content {
justify-content: center;
align-items: center;
user-select: none;
cursor: pointer;
}
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
import aesjs from 'aes-js'
import aes_worker from '@/worker/aes_ctr?worker'
export default (key, data) => {
key = aesjs.utils.utf8.toBytes(key)
return new Promise((resolve) => {
const aesWorker = new aes_worker()
aesWorker.onmessage = (event) => {
resolve(event.data)
}
aesWorker.postMessage({ data, key })
})
}
+42
View File
@@ -0,0 +1,42 @@
import workerpool from 'workerpool'
function randName(){
return `randName${Math.ceil(Math.random()*100000)}`
}
class MiaoWorkerPool{
constructor(){
this.workers = {}
this.pool = workerpool.pool()
}
/**
* Description 添加function
* @param {function} func 待添加function
* @returns {String} 返回添加的function名
*/
init(func){
let funcName = func.name.length > 0 ? func.name : randName();
this[funcName] = async (...args)=>{
try{
let result = await this.pool.exec(func,args)
return {
isSuccess:true,
result
}
}catch(e){
throw {
isSuccess:false,
result:e
}
}
}
return funcName
}
}
export default MiaoWorkerPool
+27
View File
@@ -0,0 +1,27 @@
import sm4_encrypt_worker from '@/worker/sm4_encrypt?worker'
import sm4_decrypt_worker from '@/worker/sm4_decrypt?worker'
const SM4 = {
encrypt: async (key, data) => {
const encryptedBytes = await new Promise((resolve) => {
const sm4Worker = new sm4_encrypt_worker()
sm4Worker.onmessage = (event) => {
resolve(event.data)
}
sm4Worker.postMessage({ data, key })
})
return encryptedBytes
},
decrypt: async (key, data) => {
const decryptedBytes = await new Promise((resolve) => {
const sm4Worker = new sm4_decrypt_worker()
sm4Worker.onmessage = (event) => {
resolve(event.data)
}
sm4Worker.postMessage({ data, key })
})
return decryptedBytes
}
}
export default SM4
+34 -14
View File
@@ -2,6 +2,8 @@ import aesjs from "aes-js";
import { Base64 } from "js-base64";
import { saveAs } from "file-saver";
import { sm4 } from "sm-crypto";
import AES from './AES'
import SM4 from './SM4'
const blob2Uint8Array = function (blob) {
return new Promise((resolve, reject) => {
@@ -19,7 +21,7 @@ const blob2Uint8Array = function (blob) {
};
const decrypt = (key, data) => {
key = key.trim()
return blob2Uint8Array(data).then(data => {
return blob2Uint8Array(data).then(async data => {
// 无加密
if (key.length === 0) {
return data;
@@ -28,20 +30,23 @@ const decrypt = (key, data) => {
if (key.length === 19) {
key = [...aesjs.utils.utf8.toBytes(key.substring(3))];
// console.log([...data])
var decryptedBytes = new Uint8Array(sm4.decrypt([...data], key, { output: "array" }));
// var decryptedBytes = new Uint8Array(sm4.decrypt([...data], key, { output: "array" }));
let decryptedBytes = await SM4.decrypt(key, data)
return decryptedBytes;
} else {
// AES
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var decryptedBytes = aesCtr.decrypt(data);
return decryptedBytes;
// key = aesjs.utils.utf8.toBytes(key);
// var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
// var decryptedBytes = aesCtr.decrypt(data);
// return decryptedBytes;
const decryptedBytes = await AES(key, data)
return decryptedBytes
}
});
};
const decryptString = (key, str) => {
key = key.trim()
console.log(key.length)
// console.log(key.length)
// 无加密
if (key.length === 0) {
// return sm4.decrypt(str, "miaospringmiaospringmiaospringao");
@@ -54,23 +59,38 @@ const decryptString = (key, str) => {
return sm4.decrypt(str, key);
}
// AES
var data = new Uint8Array(
Base64.decode(str)
.split(",")
.map(n => parseInt(n))
);
// var data = new Uint8Array(
// Base64.decode(str)
// .split(",")
// .map(n => parseInt(n))
// );
var data = Base64.toUint8Array(str)
// console.log(data)
// data = new TextDecoder().decode(data)
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var decryptedBytes = aesCtr.decrypt(data);
var decryptedStr = aesjs.utils.utf8.fromBytes(decryptedBytes);
// var decryptedStr = aesjs.utils.utf8.fromBytes(decryptedBytes);
var decryptedStr = new TextDecoder().decode(decryptedBytes)
return decryptedStr;
};
const _saveAs = (blob,fileName)=>{
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.download = fileName;
a.href = blobUrl;
a.click();
}
const downloadFromUint8Array = (data, fileName) => {
let uint8Array = data;
let blob = new Blob([uint8Array], { type: "text/plain" });
let blob = new Blob([uint8Array], { type: "application/octet-stream" });
// const blobUrl = window.URL.createObjectURL(blob);
// 使用file-saver.js
saveAs(blob, fileName);
// _saveAs(blob, fileName);
};
export { blob2Uint8Array, decrypt, decryptString, downloadFromUint8Array };
+14
View File
@@ -0,0 +1,14 @@
export default function filePicker({ fn, accept, multiple } = {}) {
const inpEle = document.createElement("input");
inpEle.id = `__file_${parseInt(Math.random() * 100000000)}`;
inpEle.type = "file";
inpEle.style.display = "none";
// 文件类型限制
// inpEle.accept="image/*"
accept && (inpEle.accept = accept);
// 多选限制
multiple && (inpEle.multiple = multiple);
// inpEle.addEventListener("change", event => fn.call(inpEle, event, inpEle.files), { once: true });
inpEle.addEventListener("change", e => fn(inpEle.files), { once: true });
inpEle.click();
}
+12
View File
@@ -0,0 +1,12 @@
function pow1024(num) {
return Math.pow(1024, num)
}
export default function(size){
if (!size) return '';
return size < 1024 ? size + ' B' :
size < pow1024(2) ? (size / 1024).toFixed(2) + ' KB' :
size < pow1024(3) ? (size / pow1024(2)).toFixed(2) + ' MB' :
size < pow1024(4) ? (size / pow1024(3)).toFixed(2) + ' GB' :
(size / pow1024(4)).toFixed(2) + ' TB'
}
+8
View File
@@ -0,0 +1,8 @@
export default function getQueryString(key) {
var reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substring(1).match(reg);
if (r != null) {
return decodeURI(r[2]);
}
return null;
}
+121 -94
View File
@@ -1,111 +1,138 @@
import aesjs from "aes-js";
import { Base64 } from "js-base64";
import { sm4 } from 'sm-crypto'
import { sm4 } from "sm-crypto";
import AES from "./AES";
import SM4 from "./SM4";
// const setting = useSettingStore();
// const message = useMessage();
// 生成密钥
const generateKey = function(setting) {
if(setting.activeFuction === "不加密"){
return ''
}
var charMap = ""
for(let i of setting.activeKeyMap){
if(i === 'A-Z'){
charMap = charMap + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
}else if(i === "a-z"){
charMap = charMap + 'abcdefghijklmnopqrstuvwxyz'
}else if(i === '0-9'){
charMap = charMap + '0123456789'
}
}
var returnStr = "",
length = setting.activeFuction !== "AES256" ? 16 : 32
// charMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const generateKey = function (setting) {
if (setting.activeFuction === "不加密") {
return "";
}
var charMap = "";
for (let i of setting.activeKeyMap) {
if (i === "A-Z") {
charMap = charMap + "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
} else if (i === "a-z") {
charMap = charMap + "abcdefghijklmnopqrstuvwxyz";
} else if (i === "0-9") {
charMap = charMap + "0123456789";
}
}
var returnStr = "",
length = setting.activeFuction !== "AES256" ? 16 : 32;
// charMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (var i = 0; i < length; i++) {
var index = parseInt(Math.random() * charMap.length);
returnStr += charMap.substring(index, index + 1);
}
if(setting.activeFuction === "SM4"){
returnStr = 'SM4' + returnStr
}
// console.log(returnStr,returnStr.length)
return returnStr;
for (var i = 0; i < length; i++) {
var index = parseInt(Math.random() * charMap.length);
returnStr += charMap.substring(index, index + 1);
}
if (setting.activeFuction === "SM4") {
returnStr = "SM4" + returnStr;
}
// console.log(returnStr,returnStr.length)
return returnStr;
};
const blob2Uint8Array = function(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function() {
let buffer = reader.result;
let uint8array = new Uint8Array(buffer);
resolve(uint8array);
};
reader.onerror = function() {
reject(reader.error);
};
});
const blob2Uint8Array = function (blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function () {
let buffer = reader.result;
let uint8array = new Uint8Array(buffer);
resolve(uint8array);
};
reader.onerror = function () {
reject(reader.error);
};
});
};
const AES = (key,data)=>{
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(data);
return encryptedBytes;
}
const SM4 = (key,data)=>{
// console.log(key)
var encryptedBytes = sm4.encrypt(data,key,{padding: 'pkcs#7',output:'array'});
// console.log(encryptedBytes, sm4.decrypt(encryptedBytes, key,{output:'array'}))
return new Uint8Array(encryptedBytes);
}
// const AES = (key, data) => {
// key = aesjs.utils.utf8.toBytes(key)
// // var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5))
// // var encryptedBytes = aesCtr.encrypt(data)
// // return encryptedBytes
// return new Promise((resolve) => {
// const aesWorker = new aes_worker()
// aesWorker.onmessage = (event) => {
// resolve(event.data)
// }
// aesWorker.postMessage({ data, key })
// })
// }
// const SM4 = (key, data) => {
// // console.log(key)
// var encryptedBytes = sm4.encrypt(data, key, {
// padding: 'pkcs#7',
// output: 'array'
// })
// // console.log(encryptedBytes, sm4.decrypt(encryptedBytes, key,{output:'array'}))
// return new Uint8Array(encryptedBytes)
// }
const encrypt = (key, data, fileName, setting, message) => {
// if(!encryptFunction){
// encryptFunction = setting.activeFuction
// }
let encryptFunction = {0:'不加密',16:'AES128',19:'SM4',32:'AES256'}[key.length]
return blob2Uint8Array(data).then(data => {
let encrypted_data = null
if(encryptFunction === "不加密"|| key === ''){
encrypted_data = data
// AES
}else if(encryptFunction != "SM4"){
message.info(`开始加密${fileName}`);
encrypted_data = AES(key,data)
message.info(`${fileName}加密完成`);
// SM4
}else{
message.info(`开始加密${fileName}`);
key = [...aesjs.utils.utf8.toBytes(key.substring(3))];
encrypted_data = SM4(key,data)
message.info(`${fileName}加密完成`);
}
return encrypted_data
});
// if(!encryptFunction){
// encryptFunction = setting.activeFuction
// }
let encryptFunction = {
0: "不加密",
16: "AES128",
19: "SM4",
32: "AES256"
}[key.length];
return blob2Uint8Array(data).then(async data => {
let encrypted_data = null;
if (encryptFunction === "不加密" || key === "") {
encrypted_data = data;
// AES
} else if (encryptFunction != "SM4") {
if (message) message.info(`开始加密${fileName}`);
encrypted_data = await AES(key, data);
if (message) message.info(`${fileName}加密完成`);
// SM4
} else {
if (message) message.info(`开始加密${fileName}`);
key = [...aesjs.utils.utf8.toBytes(key.substring(3))];
encrypted_data = await SM4.encrypt(key, data);
if (message) message.info(`${fileName}加密完成`);
}
console.log(encrypted_data);
return encrypted_data;
});
};
// setting为pinia存储的setting。。。
const encryptString = (key, str, setting) => {
// if(!encryptFunction){
// encryptFunction = setting.activeFuction
// }
// console.log(key,key.length)
let encryptFunction = {0:'不加密',16:'AES128',19:'SM4',32:'AES256'}[key.length]
if(encryptFunction === "不加密" || key === ''){
// return sm4.encrypt(str, 'miaospringmiaospringmiaospringao')
return Base64.encode(str)
}else if(encryptFunction !== "SM4"){
key = aesjs.utils.utf8.toBytes(key);
var data = aesjs.utils.utf8.toBytes(str);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(data);
return Base64.encode(encryptedBytes);
}else{
key = [...aesjs.utils.utf8.toBytes(key.substring(3))];
// console.log(key)
return sm4.encrypt(str, key)
}
// if(!encryptFunction){
// encryptFunction = setting.activeFuction
// }
// console.log(key,key.length)
let encryptFunction = {
0: "不加密",
16: "AES128",
19: "SM4",
32: "AES256"
}[key.length];
if (encryptFunction === "不加密" || key === "") {
// return sm4.encrypt(str, 'miaospringmiaospringmiaospringao')
return Base64.encode(str);
} else if (encryptFunction !== "SM4") {
key = aesjs.utils.utf8.toBytes(key);
// var data = aesjs.utils.utf8.toBytes(str);
var data = new TextEncoder().encode(str);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(data);
var res = Base64.fromUint8Array(encryptedBytes, true);
// console.log(encryptedBytes,res)
return res;
} else {
key = [...aesjs.utils.utf8.toBytes(key.substring(3))];
// console.log(key)
return sm4.encrypt(str, key);
}
};
export {encrypt,encryptString,generateKey}
export { encrypt, encryptString, generateKey };
+73 -21
View File
@@ -1,31 +1,83 @@
import { defineStore } from "pinia";
import config from "../../miaoShareConfig.json"
import { defineStore } from 'pinia'
import config from '../../miaoShareConfig.json'
console.log(location)
const userID = localStorage.getItem('userID')
const userGroup = localStorage.getItem('userGroup')
const tokenExpiresIn = config.tokenExpiresIn
if (
localStorage.getItem('tokenExpiredTime') == null ||
parseInt(localStorage.getItem('tokenExpiredTime')) < parseInt(Date.now())
) {
localStorage.removeItem('token')
}
const token = localStorage.getItem('token')
var activeFuctionLocal = localStorage.getItem('activeFuction')
if (activeFuctionLocal == null){
if (activeFuctionLocal == null) {
activeFuctionLocal = 'AES128'
localStorage.setItem('activeFuction',activeFuctionLocal)
localStorage.setItem('activeFuction', activeFuctionLocal)
}
// localStorage.removeItem('activeKeyMap')
var activeKeyMap = localStorage.getItem('activeKeyMap')
if (activeKeyMap == null ){
activeKeyMap = JSON.stringify(['A-Z','0-9'])
localStorage.setItem('activeKeyMap',activeKeyMap)
if (activeKeyMap == null) {
activeKeyMap = JSON.stringify(['A-Z', '0-9'])
localStorage.setItem('activeKeyMap', activeKeyMap)
}
export const useSettingStore = defineStore("setting", {
state: () => ({
// 开发环境下服务器端口使用设置的端口,否则使用与页面相同的端口
const serverPort =
process.env.NODE_ENV === 'development' ? config.PORT : location.port
const baseUrl =
location.protocol + '//' + location.hostname + ':' + serverPort + '/'
const wsBaseUrl = 'ws' + '://' + location.hostname + ':' + serverPort
// console.log(process.env.NODE_ENV,config.DEVPORT,location.port,serverPort)
export const useSettingStore = defineStore('setting', {
state: () => ({
activeFuction: activeFuctionLocal,
activeKeyMap:JSON.parse(activeKeyMap),
serverPort:config.PORT
}),
getters: {
// double: state => state.count * 2
},
actions: {
// increment() {
// this.count++;
// }
}
});
activeKeyMap: JSON.parse(activeKeyMap),
enableGuest: config.enableGuest,
serverPort,
token,
userID,
userGroup,
baseUrl,
wsBaseUrl
}),
getters: {
// double: state => state.count * 2
},
actions: {
setJWT(token, keepTokenLocal) {
this.token = token
if (keepTokenLocal) {
localStorage.setItem('token', token)
localStorage.setItem(
'tokenExpiredTime',
parseInt(Date.now()) + parseInt(tokenExpiresIn) * 3600000
)
}
},
setUserID(id, keepTokenLocal) {
this.userID = id
if (keepTokenLocal) localStorage.setItem('userID', id)
},
setUserGroup(g, keepTokenLocal) {
this.userGroup = g
if (keepTokenLocal) localStorage.setItem('userGroup', g)
},
getToken() {
return localStorage.getItem('token')
}
// increment() {
// this.count++;
// }
}
})
+18
View File
@@ -0,0 +1,18 @@
<template>
<n-button @click="signOut" type="error">
退出登录
</n-button>
</template>
<script setup>
import { NCheckbox, NInputGroupLabel, NInputGroup, NP, NFlex, NDataTable, NButton, NInput, NCard } from 'naive-ui'
function signOut(){
const items = ['token']
items.forEach(v=>{
localStorage.removeItem(v)
})
location.reload();
}
</script>
+297 -83
View File
@@ -1,52 +1,144 @@
<template>
<n-button @click="createGroup">
createTest
</n-button>
<n-text> createTest gID:{{ gID }}<br />key:{{ key }}<br />code:{{ gID+key }}</n-text>
<n-input
v-model:value="codeInput"
type="textarea"
placeholder="测试加入群组"
/>
<n-button @click="joinGroup(codeInput)">
joinTest
</n-button>
<n-input
v-model:value="msg"
type="textarea"
placeholder="测试发送信息"
/>
<n-button @click="sendMsg(codeInput.substring(0,8),msg)">
sendMsgTest
</n-button>
<n-button @click="data.length = 0">
clearMsg
</n-button>
<n-data-table
:columns="columns"
:data="data"
:row-props="null"
:max-height="250"
<n-flex vertical>
<n-flex style="padding-top: 10px;">
<n-input v-model:value="groupCode" type="text" placeholder="输入邀请码" style="flex:1">
<template #suffix>
<n-icon :component="CopyOutline" style="cursor: pointer;" @click='_toClipboard(groupCode)' />
</template>
</n-input>
<n-flex>
<n-button @click="createGroup" :disabled="groupCode !== ''" :type="groupCode !== '' ? 'default' : 'primary'"
ghost>
创建群组
</n-button>
<n-button @click="joinGroup(groupCode)" :disabled="groupCode === ''"
:type="groupCode === '' ? 'default' : 'primary'" ghost>
加入群组
</n-button>
<n-button @click="shareWithQR()" :disabled="activeGID === null" v-show="activeGID !== null"
:type="activeGID === null ? 'default' : 'primary'">
分享群组
</n-button>
<n-select v-model:value="activeGID" :options="joinedGroup.map(gid => { return { label: gid, value: gid } })" v-show="activeGID !== null"
placeholder="未加入群组" style="flex:1;user-select: none;" />
</n-flex>
</n-flex>
<!-- <n-flex style="padding-top: 10px;padding-bottom: 10px;">
<n-select v-model:value="activeGID" :options="joinedGroup.map(gid => { return { label: gid, value: gid } })"
placeholder="选择当前查看群组" style="flex:1" />
<n-button @click="shareWithQR()" :disabled="activeGID === null"
:type="activeGID === null ? 'default' : 'primary'">
分享群组
</n-button>
</n-flex> -->
<!-- <n-data-table :columns="columns"
:data="activeGID ? Messages[activeGID].map(v => { v.timestampString = new Date(v.timestamp).toLocaleString(); return v }).reverse() : []"
:row-props="null" :bordered="false" :single-line="false" :max-height="400" /> -->
<div style="flex:1; position: relative;min-height: none;">
<chatui style="margin-bottom: 10px; height: 65vh"
:messages="activeGID ? Messages[activeGID].map(v => { v.timestampString = new Date(v.timestamp).toLocaleString(); return v }).reverse() : []"/>
<n-input v-model:value="msgInput" type="textarea" placeholder="发送信息" :autosize="{
minRows: 1,
maxRows: 2
}" @keydown="handleKeyDown" style="margin-top: 0px;">
<template #suffix>
<n-icon :component="DocumentsOutline" style="cursor: pointer;margin-right: 5px;" @click='filePicker({fn:sendFiles,multiple:true})'/>
<n-icon :component="ImagesOutline" style="cursor: pointer;margin-right: 5px;" @click='filePicker({fn:sendImages,accept:"image/*",multiple:true})'/>
<n-icon :component="SendOutline" style="cursor: pointer;" @click='sendMsg(activeGID, msgInput)'/>
</template>
</n-input>
<!-- <n-button @click="sendMsg(activeGID, msg)" :disabled="msg === '' || activeGID === null"
:type="msg === '' || activeGID === null ? 'default' : 'primary'">
发送消息
</n-button> -->
</div>
</n-flex>
<!-- {{ dataTest }} -->
<!-- {{ Messages }} -->
<n-modal v-model:show="showModal">
<n-card
style="width: fit-content;"
title=""
:bordered="false"
:single-line="false"
/>
size="huge"
role="dialog"
aria-modal="true"
>
<n-qr-code :value="QRCodeData" style="padding: 0;" :size="256" error-correction-level="Q" :v-if="showModal"
:icon-src='0?QRIconUrl:null'
:icon-size="128"/>
</n-card>
</n-modal>
</template>
<script setup>
import {ref} from 'vue'
import {NText,NDataTable,NButton,NInput} from 'naive-ui'
import { ref,onMounted } from 'vue'
// import { useModal } from "naive-ui";
import useClipboard from "vue-clipboard3";
import { NIcon, NFlex, NDataTable, NButton, NInput, NSelect,NModal,NCard , NQrCode} from 'naive-ui'
import { useSettingStore } from "@/pinia/store";
import { generateKey,encryptString } from '@/func/upload';
import { generateKey, encryptString, encrypt } from '@/func/upload';
import { decryptString } from '@/func/download';
import WebsocketHeartbeatJs from 'websocket-heartbeat-js';
import { CopyOutline,SendOutline,ImagesOutline,DocumentsOutline } from '@vicons/ionicons5'
import getQueryString from '@/func/getQuery'
import chatui from "@/components/chatui.vue"
import { Base64 } from 'js-base64';
import { useMessage, useNotification } from "naive-ui";
import filePicker from "@/func/filePicker"
const message = useMessage()
// const modal = useModal();
const { toClipboard } = useClipboard();
const _toClipboard = async (text) => {
await toClipboard(text)
message.info(`已复制 ${text.length < 10 ? text : text.substring(0,7) + '...'}`)
}
const setting = useSettingStore();
const serverPort = setting.serverPort;
const baseUrl = 'ws' + "://" + location.hostname + ":" + serverPort;
// const wsBaseUrl = 'ws' + "://" + location.hostname + ":" + serverPort;
const wsBaseUrl = setting.wsBaseUrl
// const baseUrl = location.protocol + "//" + location.hostname + ":" + serverPort + "/";
const baseUrl = setting.baseUrl
const QRIconUrl = ref(baseUrl + "favicon.ico")
const columns = [
{
"title": "type",
"key": "type",
width: 80
},
{
"title": "from",
"key": "from",
width: 80
},
{
"title": "timestamp",
"key": "timestampString",
width: 150,
// sorter: {
// compare: (a, b) => {
// // console.log(a,b,new Date(a),parseInt(new Date(a)) - parseInt(new Date(b)))
// return parseInt(new Date(a.timestamp)) - parseInt(new Date(b.timestamp))
// },
// }
},
{
"title": "msg",
"key": "msg"
}
]
const columnsTest = [
{
title: "from",
key: "from"
@@ -56,22 +148,52 @@ const columns = [
key: "msg"
},
];
const data = ref([])
const gID = ref('')
const codeInput = ref('')
function handleKeyDown(e){
// console.log(e,e.ctrlKey,e.code)
if(e.ctrlKey === true&&e.code === 'Enter'){
sendMsg(activeGID.value, msgInput.value)
}
}
// 用于二维码分享
const showModal = ref(false)
const QRCodeData = ref('')
function shareWithQR(){
let code = activeGID.value+gID2key[activeGID.value].key
QRCodeData.value = location.protocol + "//" + location.hostname + ":" + serverPort + `/?aP=chat&cG=${code}`
showModal.value = true
}
// 凭此code进入群组
const groupCode = ref('')
// 当前活跃的群组ID
const activeGID = ref(null)
// 临时生成的key,之后与返回的code结合形成groupCode
const key = ref('')
const msg = ref('')
const joinedGroup = []
// 存储所有消息,通过gID查询一个群组的消息,{msg,from,timestamp}
const Messages = ref({})
const dataTest = ref([])
const gID = ref('')
// const codeInput = ref('')
const msgInput = ref('')
const joinedGroup = ref([])
// 由server随机分配uuid
const uuid = ref('')
const gID2key = {}
const options = {
url: baseUrl,
pingTimeout: 20000,
pongTimeout: 10000,
url: wsBaseUrl,
pingTimeout: 20000,
pongTimeout: 10000,
reconnectTimeout: 3000,
pingMsg: JSON.stringify({type:'heartbeat'})
pingMsg: JSON.stringify({ type: 'heartbeat' })
}
let socket = new WebsocketHeartbeatJs(options);
@@ -79,55 +201,66 @@ let socket = new WebsocketHeartbeatJs(options);
// socket.addEventListener("open", (event) => {
// // socket.send("");
// });
// setInterval(()=>console.log(activeGID.value),1000)
socket.onmessage= (event) => {
socket.onmessage = (event) => {
const msg = JSON.parse(event.data)
data.value.push({from:'server',msg:event.data})
if(msg.type){
dataTest.value.push({ from: 'server', msg: event.data })
if (msg.type) {
let type = msg.type
if(type === 'sMsg'){
if (type === 'sMsg') {
let type1 = msg.type1
if(type1 === 'create'){
gID.value=msg.gID
}else if(type1 === 'joinVerify'){
if (type1 === 'create') {
gID.value = msg.gID
groupCode.value = msg.gID + key.value
joinGroup(groupCode.value)
} else if (type1 === 'joinVerify') {
let gID = msg.gID
let key = gID2key[gID].key
// console.log(gID2key,gID,key)
let res = decryptString(key,msg.verify)
console.log(res,key,msg.verify)
if(res === 'miaoSpring'){
socket.send(JSON.stringify({type:'join',type1:'success',gID}))
joinedGroup.push(msg.gID)
let res = decryptString(key, msg.verify)
// console.log(res, key, msg.verify)
// 验证成功,正式加入群组
if (res === 'miaoSpring') {
socket.send(JSON.stringify({ type: 'join', type1: 'success', gID }))
joinedGroup.value.push(msg.gID)
Messages.value[msg.gID] = []
// if(activeGID.value === null){
activeGID.value = msg.gID
// }
}
}else if(type1 === 'history'){
} else if (type1 === 'history') {
let gID = msg.gID
let key = gID2key[gID].key
for(let m of JSON.parse(msg.history)){
data.value.push({from:'decrypt',msg:decryptString(key,m.msg)})
Messages.value[msg.gID].length = 0
for (let m of JSON.parse(msg.history)) {
Messages.value[msg.gID].push({ type: m.type,type1:m.type1, msg: decryptString(key, m.msg), from: m.from, timestamp: m.timestamp })
dataTest.value.push({ from: 'decrypt', msg: decryptString(key, m.msg) })
}
}else if(type1 === 'uuid'){
} else if (type1 === 'uuid') {
uuid.value = msg.uuid
}
}else if(type === 'gMsg'){
} else if (type === 'gMsg') {
let gID = msg.gID
let key = gID2key[gID].key
data.value.push({from:'decrypt',msg:decryptString(key,msg.msg)})
Messages.value[msg.gID].push({ type: msg.type,type1:msg.type1, msg: decryptString(key, msg.msg), from: msg.from, timestamp: msg.timestamp })
dataTest.value.push({ from: 'decrypt', msg: decryptString(key, msg.msg) })
}
}
};
socket.onopen = ()=>{
if(uuid.value){
socket.send(JSON.stringify({type:'uuid',type1:'set',uuid:uuid.value}))
}else{
socket.send(JSON.stringify({type:'uuid',type1:'get'}))
socket.onopen = () => {
if (uuid.value) {
socket.send(JSON.stringify({ type: 'uuid', type1: 'set', uuid: uuid.value }))
} else {
socket.send(JSON.stringify({ type: 'uuid', type1: 'get' }))
}
console.log(joinedGroup)
let toJoin = [...joinedGroup]
joinedGroup.length = 0
for(let gID of toJoin){
let code = gID+gID2key[gID].key
// console.log(joinedGroup)
let toJoin = [...joinedGroup.value]
joinedGroup.value.length = 0
for (let gID of toJoin) {
let code = gID + gID2key[gID].key
console.log(code)
joinGroup(code)
}
@@ -137,16 +270,16 @@ socket.onreconnect = (e) => {
console.log('reconnecting...');
}
function createGroup(){
function createGroup() {
let keyt = generateKey(setting)
key.value = keyt
socket.send(JSON.stringify({type:'create',verify:encryptString(keyt,'miaoSpring',setting)}))
socket.send(JSON.stringify({ type: 'create', verify: encryptString(keyt, 'miaoSpring', setting) }))
}
function joinGroup(code){
function joinGroup(code) {
var gID = code.substring(0, 8);
var key = code.substring(8);
if(joinedGroup.includes(gID)){
if (joinedGroup.value.includes(gID)) {
return
}
gID2key[gID] = {}
@@ -154,18 +287,99 @@ function joinGroup(code){
joinGroup_Verify(gID)
}
function joinGroup_Verify(gID){
socket.send(JSON.stringify({type:'join',type1:'verify',gID}))
function joinGroup_Verify(gID) {
socket.send(JSON.stringify({ type: 'join', type1: 'verify', gID }))
}
function sendMsg(gID,msg){
function sendMsg(gID, msg) {
if(msg === '' || msg === null || activeGID.value === null)return
let key = gID2key[gID].key
msg = encryptString(key,msg,setting)
socket.send(JSON.stringify({type:'message',msg,gID}))
msg = encryptString(key, msg, setting)
socket.send(JSON.stringify({ type: 'message',type1:'text', msg, gID }))
msgInput.value = null
}
// async function encryptImg(file){
// var f = new Blob([file]);
// let key = gID2key[gID].key
// let eU8A = await encrypt(key, f, fileName, setting)
// let eb = Base64.fromUint8Array(eU8A)
// return eb
// }
async function uploadFile(file){
if(activeGID.value === null)return
var fileName = file.name;
let key = gID2key[activeGID.value].key
const encryptedFileName = encryptString(key, fileName, setting);
var f = new Blob([file]);
let data = await encrypt(key, f, fileName, setting)
const params = {
fileName: encryptedFileName,
downloadCredit:-1
};
message.info(`开始上传${fileName}`)
const xhr = new XMLHttpRequest();
// onProgress({ percent: 1 });
xhr.open(
"POST",
baseUrl + "file/upload?" + new URLSearchParams(params)
);
// console.log(setting.token)
xhr.setRequestHeader('Authorization', `Bearer ${setting.token}`);
xhr.send(data);
const code = await new Promise((resolve,reject)=>{
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
let id = xhr.responseText;
let code = id + key
message.success(`上传${fileName}成功`)
resolve(code)
} else {
if(xhr.statusText !== 'OK'){
console.error(xhr.statusText);
message.error(`上传${fileName}失败`)
}
}
};
})
console.log(code)
return code
}
async function sendImages(files){
if(activeGID.value === null || files.length === 0)return
for(let file of files){
const code = await uploadFile(file)
let key = gID2key[activeGID.value].key
socket.send(JSON.stringify({ type: 'message',type1:'img', msg: encryptString(key, code), gID:activeGID.value }))
};
}
async function sendFiles(files){
if(activeGID.value === null || files.length === 0)return
for(let file of files){
let fileName = file.name;
const code = await uploadFile(file)
let key = gID2key[activeGID.value].key
socket.send(JSON.stringify({ type: 'message',type1:'file', msg: encryptString(key, code), gID:activeGID.value }))
};
}
onMounted(()=>{
let aP = getQueryString('aP')
let cG = getQueryString('cG')
if(aP === 'chat' && cG){
groupCode.value = cG
var st = setInterval(()=>{
if(joinedGroup.value.includes(cG.substring(0,8))){
clearInterval(st)
return
}
joinGroup(cG)
},2000)
}
})
</script>
<style>
</style>
<style></style>
+8 -4
View File
@@ -28,7 +28,7 @@
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted,computed } from "vue";
import { useSettingStore } from "@/pinia/store";
import {decrypt, decryptString, downloadFromUint8Array} from '../func/download'
import {NInput,NFlex,NDataTable,NButton} from 'naive-ui'
@@ -40,8 +40,8 @@ const setting = useSettingStore();
// 服务端端口
const serverPort = setting.serverPort;
var baseUrl =
location.protocol + "//" + location.hostname + ":" + serverPort + "/";
// const baseUrl = location.protocol + "//" + location.hostname + ":" + serverPort + "/";
const baseUrl = setting.baseUrl
// console.log(baseUrl);
// const blob2Uint8Array = function(blob) {
@@ -158,7 +158,11 @@ const downloadFile = code => {
fileName: id
};
const options = {
method: "GET"
method: "GET",
headers: {
'Authorization': `Bearer ${setting.token}`
// 'Content-Type':'application/json'
},
};
fetch(baseUrl + "file/download?" + new URLSearchParams(params), options)
.then(res => {
+90
View File
@@ -0,0 +1,90 @@
<template>
<n-card style="box-shadow: 10px 10px 5px #888888;margin-top: 100px; padding-left: 25px;padding-right: 25px; padding-bottom: 30px" size="small">
<n-flex vertical justify="center">
<n-flex justify="center">
<h1 style="letter-spacing:25px;font-size: 40px;">登录</h1>
</n-flex>
<n-input-group>
<n-input-group-label>账号</n-input-group-label>
<n-input placeholder="userID" v-model:value="userIDInput" :loading="isLogining" :disabled="isLogining"></n-input>
</n-input-group>
<n-input-group>
<n-input-group-label>密码</n-input-group-label>
<n-input placeholder="userPassword" v-model:value="userPasswordInput" type="password"
show-password-on="click" :loading="isLogining" :disabled="isLogining"></n-input>
</n-input-group>
<n-flex justify="end">
<n-flex style="flex:1;align-items: center;">
<n-checkbox size="large" label="保持登录状态" v-model:checked="keepTokenLocal" />
</n-flex>
<n-flex justify="end">
<n-button @click="useGuestAccount" v-if="settingData.enableGuest">使用访客账号登录</n-button>
<n-button @click="login">登录</n-button>
</n-flex>
</n-flex>
</n-flex>
</n-card>
</template>
<script setup>
import { computed, ref } from 'vue'
import { NCheckbox, NInputGroupLabel, NInputGroup, NP, NFlex, NDataTable, NButton, NInput, NCard } from 'naive-ui'
import { useSettingStore } from "@/pinia/store";
import { useMessage, useNotification } from "naive-ui";
import sha256 from "crypto-js/sha256.js";
const message = useMessage()
const settingData = useSettingStore();
const serverPort = settingData.serverPort;
const userIDInput = ref('')
const userPasswordInput = ref('')
const isLogining = ref(false)
// const baseUrl = location.protocol + "//" + location.hostname + ":" + serverPort + "/";
const baseUrl = settingData.baseUrl
const Token = computed(() => settingData.token)
const keepTokenLocal = ref(true)
function useGuestAccount() {
userIDInput.value = 'guest'
userPasswordInput.value = 'guest'
login()
}
async function login() {
try {
isLogining.value = true
const response = await fetch(`${baseUrl}user/login`, {
method: 'POST',
headers: {
// 'Authorization': `Bearer ${Token}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: userIDInput.value,
pwd: sha256(userPasswordInput.value).toString()
})
})
let data = await response.json()
if (data.status === 'Error') {
console.log('error')
throw ('failed')
}
console.log(data.token, data)
settingData.setJWT(data.token,keepTokenLocal)
settingData.setUserID(userIDInput.value,keepTokenLocal)
settingData.setUserGroup(data.userGroup,keepTokenLocal)
message.success(`登录成功`);
} catch (e) {
isLogining.value = false
message.error(`登录失败`);
console.log('failed')
console.log(e)
}
}
</script>
+8 -5
View File
@@ -14,12 +14,12 @@
<n-text style="font-size: 16px;user-select: none;">点击此处或者拖动文件到该区域来上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0;user-select: none;">
最大支持200MB大小文件,其实只是懒得做分片上传而已
<br />虽然理论上没有人有能力审查你上传的东西
<!-- <br />虽然理论上没有人有能力审查你上传的东西 -->
<br />反正没人用写这么多干什么。。。
</n-p>
</n-upload-dragger>
</n-upload>
<n-flex justify="end" style="margin: 10px 0;position:relative">
<n-input-number v-model:value="downloadCredit" placeholder="可下载次数,默认无限" clearable :min="1" :max="2147483647" style="width:max-content"/>
<n-button type="primary" @click="clickUpload">开始上传</n-button>
@@ -37,7 +37,7 @@
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted,computed } from "vue";
// import aesjs from "aes-js";
// import { Base64 } from "js-base64";
import { useMessage, useNotification } from "naive-ui";
@@ -94,8 +94,9 @@ var file_Code_Map = ref([]);
var uploadProcess = ref(0);
const baseUrl =
location.protocol + "//" + location.hostname + ":" + serverPort + "/";
// const baseUrl = location.protocol + "//" + location.hostname + ":" + serverPort + "/";
const baseUrl = setting.baseUrl
console.log(baseUrl)
// const blob2Uint8Array = function(blob) {
@@ -204,6 +205,8 @@ const customRequest = ({
"POST",
baseUrl + "file/upload?" + new URLSearchParams(params)
);
console.log(setting.token)
xhr.setRequestHeader('Authorization', `Bearer ${setting.token}`);
xhr.send(data);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
+9
View File
@@ -0,0 +1,9 @@
import aesjs from 'aes-js'
self.onmessage = async function (event) {
const data = event.data.data
const key = event.data.key
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(7058))
const encryptData = aesCtr.encrypt(data)
self.postMessage(encryptData)
}
+9
View File
@@ -0,0 +1,9 @@
import { sm4 } from 'sm-crypto'
self.onmessage = async function (event) {
const data = event.data.data
const key = event.data.key
self.postMessage(
new Uint8Array(sm4.decrypt([...data], key, { output: 'array' }))
)
}
+11
View File
@@ -0,0 +1,11 @@
import { sm4 } from 'sm-crypto'
self.onmessage = async function (event) {
const data = event.data.data
const key = event.data.key
const encryptData = sm4.encrypt(data, key, {
padding: 'pkcs#7',
output: 'array'
})
self.postMessage(new Uint8Array(encryptData))
}
+17
View File
@@ -0,0 +1,17 @@
import jwt from 'jsonwebtoken'
import sha256 from 'crypto-js/sha256.js';
import fs from 'fs'
const JWT_SK = fs.readFileSync('JWT_SK.key').toString()
let login = {id:123456,pwd:sha256('nmsl').toString()}
console.log(JWT_SK,login)
let token = jwt.sign(login,JWT_SK,{ expiresIn: '1h' })
let fakeToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJwd2QiOiIwNmYxYTIzOTE2MGZhNTQ2ZTAyOWM5YWQxNTU4OGYwYTI0YWVjYWNhN2M1ZjkyZTE4YmFlNjMzMDk1ZmRjM2JlIiwiaWF0IjoxNzExNTEzMjIwLCJleHAiOjE3MTE1MTY4MjB9.O4-0DkT8Nn0jwZUj09DFK-dOhOfG0n1Mmx3QvsfU8x1`
// error
let verify = jwt.verify(token,JWT_SK)
console.log(token,verify,parseInt(Date.now()/1000))
+43 -17
View File
@@ -1,21 +1,47 @@
import { fileURLToPath, URL } from 'node:url'
import { fileURLToPath, URL } from "node:url";
import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
// host:'0.0.0.0',
port:'17058',
open:true,
},
})
plugins: [
vue(),
visualizer({
open: true,
filename: "visualizer.html" //分析图生成的文件名
})
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
},
server: {
// host:'0.0.0.0',
port: "17058",
open: true
},
esbuild: {
drop: ["console", "debugger"]
},
build: {
minify: true,
rollupOptions: {
output: {
// 打包输出的配置
manualChunks: id => {
// 这个ID,就是所有文件的绝对路径
if (id.includes("node_modules")) {
// 因为 node_modules 中的依赖通常是不会改变的
// 所以直接单独打包出去
// 这个return 的值就是打包的名称
// return id.split('node_modules')[1]
return "modules";
}
}
}
}
}
});