Compare commits

51 Commits

Author SHA1 Message Date
91b69a2150 设置了Docker构建方式 2023-12-22 11:58:13 +00:00
369a29b824 在服务端添加了原始请求种类校验,并修改了后端对浮点数的校验规则 2023-12-20 07:26:21 +00:00
df54f1bca6 修改了一下设计文档 2023-12-19 03:05:09 +00:00
b4a58ea162 小修改了一下安装手册 2023-12-19 02:40:58 +00:00
0bf79b5045 添加了桌面端的手册 2023-12-18 14:46:43 +00:00
a580e27ba4 修复了查询页面的样式问题 2023-12-18 08:57:39 +00:00
d747b430ae 设置搜索字段为必填 2023-12-18 08:44:59 +00:00
b08f08a91f 优化了管理后台的左侧导航栏 2023-12-18 08:35:50 +00:00
8e7edecb77 添加了demo 2023-12-18 07:48:57 +00:00
fa6b256031 编辑了手册 2023-12-18 07:42:48 +00:00
876e1aa03b 删除了不该出现的Identifier 2023-12-18 07:28:55 +00:00
b31e74d0d3 手动同步了客户端中的更改 2023-12-18 06:57:24 +00:00
59229eb1b5 修复了css的计算错误 2023-12-18 06:53:56 +00:00
5a8e5d093a 优化了样式和图标 2023-12-18 06:31:59 +00:00
b7675248c3 完整的添加了对原始请求的token数检查 2023-12-18 01:45:53 +00:00
458c085ac2 增加了对token数不正确的原始请求的拦截 2023-12-18 01:34:21 +00:00
824604870b 修改了服务端,防止非法原始请求卡爆后端 2023-12-18 01:19:06 +00:00
24b9a9fedb 修改了检测模式 2023-12-18 00:18:27 +00:00
4ed14be05f 修改了链接 2023-12-17 23:58:14 +00:00
6031d79d52 修复了添加用户界面id冲突导致的问题 2023-12-17 23:56:11 +00:00
98052932a2 添加了用户手册 2023-12-17 17:04:20 +00:00
e15a4765eb 修复了交易简报排版问题 2023-12-17 16:48:20 +00:00
f7dd1ba171 提高了事件循环的频率 2023-12-17 16:40:48 +00:00
a482b55ec4 修复了财务报表页面的小bug 2023-12-17 16:31:22 +00:00
0272b8efb8 完成了图书管理系统 2023-12-17 16:20:34 +00:00
99f4ca6fa6 添加了用户管理,并且支持了部分免密 2023-12-17 16:06:06 +00:00
092c4b8a35 添加了日志页面相关功能,并且试图解决过滤器超时的问题 2023-12-17 15:39:42 +00:00
ff6a9a38a1 完成管理面板首页 2023-12-17 13:59:39 +00:00
d5da269cdc 修复了文本长度过大的问题 2023-12-17 05:52:04 +00:00
3b4ae7d013 给网页端的RawRequest上了个锁,防止发生神奇的事情 2023-12-17 05:03:51 +00:00
c696fa1396 修复了查询页面页面鉴权的问题 2023-12-17 04:45:37 +00:00
8d75e8bc86 完成基本的搜索功能 2023-12-17 04:43:10 +00:00
ca50995b3c 添加了指向主页的链接 2023-12-17 04:02:22 +00:00
122ddb9e2c 添加了购买图书的功能 2023-12-17 03:55:12 +00:00
8574b188f8 修复了无法普通模式启动的bug 2023-12-17 03:16:07 +00:00
9283940259 添加了内容审核支持防止被ban掉交不了作业 2023-12-17 02:59:53 +00:00
eceace3785 把基本的javscript代码也复用了 2023-12-17 01:16:30 +00:00
5d120e7439 修改并复用了样式 2023-12-17 01:08:32 +00:00
09edd8de81 完成Action导航栏的链接搭建,修复了登出的一些小bug 2023-12-17 00:41:59 +00:00
0d6943214e 修改了安装方式 2023-12-16 12:13:04 +00:00
e7d85352bb docs: 指明了Node版本 2023-12-16 11:48:26 +00:00
4203d0e786 不再支持关闭会话,并修复了修改密码页面无法自动重置无效旧会话的bug 2023-12-16 09:45:46 +00:00
eedf717f10 修复了有旧会话信息时,无法自动重置会话的bug 2023-12-16 09:28:50 +00:00
d23d0986d6 完成修改密码界面并修复了大量bug 2023-12-16 09:16:17 +00:00
f1c95ea987 完成注册页面 2023-12-16 08:40:03 +00:00
69e91157df 完成登录页面 2023-12-16 08:29:56 +00:00
303a6877d3 添加了退出登录和关闭会话功能 2023-12-16 07:59:32 +00:00
658ac383a2 修复了下拉框元素宽度不对的问题 2023-12-16 04:58:15 +00:00
76952cc393 完成UI的基本框架设计 2023-12-16 04:38:43 +00:00
2add648b32 添加了demo 2023-12-15 13:25:14 +00:00
acfb393e8d 增加了有关端口号的说明 2023-12-15 13:21:09 +00:00
29 changed files with 4539 additions and 50 deletions

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:20.10
LABEL maintainer="ZhuangYumin <zhuangyumin@sjtu.edu.cn>"
COPY . /bookstore/
RUN apt-get update && apt-get install -y g++ && apt-get install -y cmake
WORKDIR /bookstore/
RUN mkdir data
RUN mkdir build && cd build && cmake -B . -S .. && cmake --build . && cd ../frontend/Web && npm install
WORKDIR /bookstore/frontend/Web
EXPOSE 3000
CMD ["node","index.js","/bookstore/data/"]

View File

@ -11,4 +11,9 @@
- [开发文档](docs/develop/开发文档.md) - [开发文档](docs/develop/开发文档.md)
## User Docs ## User Docs
- [系统安装手册](docs/user/系统安装手册.md) - [系统安装手册](docs/user/系统安装手册.md)
- [用户手册](docs/user/用户手册.md)
demo(2024年1月5日之前在线)
- [网页端](http://bookstore.zymsite.ink/)
- [客户端](https://jbox.sjtu.edu.cn/v/link/view/a4843cf3920a44ff95c0bfc92aafb41a)

View File

@ -336,7 +336,7 @@ bool CommandModifyLexer(const std::string &command, std::string &ISBN,
std::string &name, std::string &author, std::string &name, std::string &author,
std::string &keyword, double &price) { std::string &keyword, double &price) {
static std::basic_regex main_pattern( static std::basic_regex main_pattern(
R"(^ *modify(?: +-ISBN=(?:[!-~]{1,20})| +-name=\"(?:[!#-~]{1,60})\"| +-author=\"(?:[!#-~]{1,60})\"| +-keyword=\"((?:[!#-{}~]{1,60}\|)*(?:[!#-{}~]{1,60}))\"| +-price=[0-9]{1,10}(?:\.[0-9]+)?)+ *$)", R"(^ *modify(?: +-ISBN=(?:[!-~]{1,20})| +-name=\"(?:[!#-~]{1,60})\"| +-author=\"(?:[!#-~]{1,60})\"| +-keyword=\"((?:[!#-{}~]{1,60}\|)*(?:[!#-{}~]{1,60}))\"| +-price=[0-9]{1,13}(?:\.[0-9]+)?)+ *$)",
std::regex_constants::optimize); std::regex_constants::optimize);
if (std::regex_match(command, main_pattern)) { if (std::regex_match(command, main_pattern)) {
std::stringstream ss(command); std::stringstream ss(command);
@ -373,6 +373,7 @@ bool CommandModifyLexer(const std::string &command, std::string &ISBN,
if (keyword.length() > 60) return false; if (keyword.length() > 60) return false;
} else if (token[1] == 'p') { } else if (token[1] == 'p') {
if (has_price) return false; if (has_price) return false;
if (token.substr(7).length() > 13) return false;
has_price = true; has_price = true;
price = std::stod(token.substr(7)); price = std::stod(token.substr(7));
} else } else
@ -400,7 +401,7 @@ bool CommandModifyLexer(const std::string &command, std::string &ISBN,
bool CommandImportLexer(const std::string &command, int &quantity, bool CommandImportLexer(const std::string &command, int &quantity,
double &total_cost) { double &total_cost) {
static std::basic_regex main_pattern( static std::basic_regex main_pattern(
R"(^ *import +[0-9]{1,10} +[0-9]{1,10}(?:\.[0-9]+)? *$)", R"(^ *import +[0-9]{1,10} +[0-9]{1,13}(?:\.[0-9]+)? *$)",
std::regex_constants::optimize); std::regex_constants::optimize);
if (std::regex_match(command, main_pattern)) { if (std::regex_match(command, main_pattern)) {
std::stringstream ss(command); std::stringstream ss(command);
@ -412,7 +413,10 @@ bool CommandImportLexer(const std::string &command, int &quantity,
ss >> quantity_tmp; ss >> quantity_tmp;
if (quantity_tmp > 2147483647) return false; if (quantity_tmp > 2147483647) return false;
quantity = quantity_tmp; quantity = quantity_tmp;
ss >> total_cost; std::string total_cost_tmp;
ss >> total_cost_tmp;
if(total_cost_tmp.length() > 13) return false;
total_cost = std::stod(total_cost_tmp);
return true; return true;
} else } else
return false; return false;

View File

@ -1,7 +1,7 @@
计划实现的bonus 计划实现的bonus
- 缓存 - 【实现】缓存MemoryRiver层的基于简单贪心的缓存
- 并行(数据库保证并发安全,但由于锁和条件变量的过高开销,不打算使用;后端不支持操作并发,逻辑并发由服务端维护) - 【不完全实现】并行:服务端可并发地响应请求,但后端是串行处理的
- GUI前端和完整部署方案 - 【实现】GUI前端和完整部署方案为WebUI有安装手册和用户手册和一个套了个壳的简单Windows桌面端。单会话支持操作频次约5~15次每秒系统整体支持操作频次约1e2次每秒。历史最高连续稳定运行时长25小时。支持多会话同时进行不支持响应式设计但是在桌面版浏览器上小范围缩放不会影响页面的美观程度。
有时间打算实现的bonus按优先级次序排序 有时间打算实现的bonus按优先级次序排序
1. UTF-8中文支持 1. UTF-8中文支持

View File

@ -0,0 +1,30 @@
以下是快速入门:
# 网页端
## 客户
### 账户相关操作
鼠标悬停在右上角`Action`按钮上方弹出的下拉菜单中有相关的登录、登出、注册、修改密码等功能。用户名、密码、昵称的最大长度为30用户名和密码只能由数字、字母或下划线构成昵称则自由度较高支持**除空格外**的全部ASCII字符。
### 图书搜索
点击左侧导航栏中的Search Books可以根据ISBN、标题、作者或关键词限一个查询图书。或者可以点击`Show All Books`查看完整图书列表。注意ISBN、标题、作者和关键词中均不能有空格。
### 图书购买
点击左侧导航栏中的Purchase Books输入待购买图书的ISBN号和欲购买数量点击`Buy`按钮购买
## 店员
鼠标悬停在右上角`Action`按钮上方,在弹出的下拉菜单中选择`Admin Panel`进入管理后台首页,随后根据弹出的导航页面选择进入用户管理系统或书籍管理系统。
注意:
1. 图书信息中不能含有空格,一本图书可以设置多个关键词,用`|`隔开。
2. 用户管理页面的“删除用户”功能不对店员开放,仅对店长开放。书籍管理系统的操作页面自带提示。
3. 如欲添加图书只需在图书编辑界面输入新书的ISBN号和相关信息并点击“Update Book Info”即可。
4. 注意虽然可供修改的图书信息是“留空表示不修改”但是不可以全部留空。ISBN、标题、作者和关键词中均不能有空格。
## 店长
在店员可访问的功能的基础上点击管理后台首页的第三个导航按钮可以进入日志查询系统在下拉框中可以选择查询所有交易简报、最近n次交易简报须额外输入次数、财务报告、员工工作报告和系统工作日志等日志信息。
# Windows桌面端
## 安装与卸载
双击安装包会自动无需管理员权限地绿色安装并在桌面生成快捷方式、在Windows程序管理面板控制面板及Win11的`设置->应用->安装的应用`中留下注册信息以供一键卸载)。如果程序文件损坏或因卸载方式不正确导致残留,请手动删除桌面的快捷方式及`C:\Users\[用户名]\AppData\Local\Programs\bookstoreclient`文件夹,然后在控制面板里点击卸载、移除不存在的程序。
## 使用
打开后自动全屏按F11可以在全屏和半屏之间切换按Esc退出。具体界面和操作方式和网页端一样。
___
详细权限与操作细节请移步[这里](/docs/homework_requirement/requirements.md)

View File

@ -1,7 +1,11 @@
# 部署 # 部署
## 从Docker构建
运行`docker run -it --name bookstore -v [your data dir]:/bookstore/data -p [your port]:3000 docker.io/happyzym/bookstore:1.0.2.0`
执行此步骤后,无需执行下列的 依赖、下载与构建、安装步骤。
## 依赖 ## 依赖
- `g++``CMake`注意要支持C++17 - `g++``CMake`注意要支持C++17
- `Node.js``npm` - `Node.js``npm`注意本项目使用NodeJs 20.10开发的apt默认源的版本过旧请移步<https://github.com/nodejs/help/wiki/Installation><https://nodejs.org/en/download/>
## 下载与构建 ## 下载与构建
1. 下载或克隆本仓库,并进入仓库根目录 1. 下载或克隆本仓库,并进入仓库根目录
@ -18,9 +22,13 @@ npm install
## 安装 ## 安装
直接把**整个项目文件夹**移动到你想移动到的位置 直接把**整个项目文件夹**移动到你想移动到的位置
## Windows桌面端生成
1. 编辑文件`frontend/client/main.js`的第9行替换成你的服务器运行的网址并编辑`frontend/client/package.json`,修改相关信息
2. 进入`frontend/client`目录,依次执行`npm install``npm install --save-dev electron``npm install -save-dev electron-builder``npm run dist`。随后会在`frontend/client/dist`目录下生成客户端安装包。此步骤可能需要管理员权限。
# 运维 # 运维
## 打开与关闭 ## 打开与关闭
启动方式:用`node`执行`frontend/Web/index.js`,务必在后面带上数据库存放位置,它应当是一个目录,并且必须以`/`结尾。执行该命令后,会一直等到服务器停止运行才退出。 启动方式:用`node`执行`frontend/Web/index.js`,务必在后面带上数据库存放位置,它应当是一个目录,并且必须以`/`结尾。执行该命令后,会一直等到服务器停止运行才退出。服务器监听3000端口。示例`node index.js /bookdata/`
关闭方式打开WebUI以超级管理员身份登录并在浏览器控制台中输入`await ShutDownWholeSystem();`,没有任何输出则表明关闭成功。 关闭方式打开WebUI以超级管理员身份登录并在浏览器控制台中输入`await ShutDownWholeSystem();`,没有任何输出则表明关闭成功。

97
frontend/Web/admin.html Normal file
View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.admin-buttons {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.admin-buttons button {
width: 80%;
padding: 20px;
margin: 10px 0;
font-size: 18px;
background-color: #3498db;
color: #fff;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
.admin-buttons button:hover {
background-color: #2980b9;
}
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
<a href="/user-management">User Management</a>
<a href="/book-management">Book Management</a>
<a href="/log-query">Log Query</a>
</nav>
<div class="content">
<div class="admin-buttons">
<button onclick="window.location.href='/user-management'">User Management System</button>
<button onclick="window.location.href='/book-management'">Book Information Management System</button>
<button onclick="window.location.href='/log-query'">Log Query System</button>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<3)
{
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in as worker or root first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

124
frontend/Web/basic.css Normal file
View File

@ -0,0 +1,124 @@
body {
margin: 0;
font-family: Arial, sans-serif;
min-height: 100vh; /* 设置 body 的最小高度为视口的高度 */
/*display: flex;*/
flex-direction: column;
}
header {
background-color: #333;
color: #fff;
padding: 10px 0;
text-align: center;
position: relative;
}
.user-bar {
position: absolute;
top: 100%;
right: 0;
transform: translateY(-100%);
padding: 10px;
background-color: #007bff;
z-index: 1000;
}
.user-bar span {
margin-right: 20px;
}
.user-bar a {
color: #fff;
margin-right: 10px;
text-decoration: none;
}
.user-bar a:hover {
text-decoration: underline;
}
.action-button-container {
display: inline-block;
}
.action-button {
padding: 10px 15px;
background-color: #007bff;
color: #fff;
border: 2px solid #0056b3;
border-radius: 5px;
cursor: pointer;
display: inline-block;
}
.action-button:hover,
.dropdown-content:hover {
background-color: #87CEFA;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #87CEFA;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
z-index: 1;
max-width: 150px;
right: 0;
top: 100%;
}
.dropdown-content a {
display: block;
padding: 10px;
text-decoration: none;
color: #333;
text-align: center;
width: 100%;
box-sizing: border-box;
}
.dropdown-content a:hover {
background-color: #ddd;
}
.dropdown:hover .dropdown-content {
display: block;
}
.main-content {
flex: 1;
display: flex;
}
nav {
width: 200px;
background-color: #f4f4f4;
padding: 20px;
--header-height: 140px; /* 默认高度,可以根据实际情况调整 */
min-height: calc(100vh - var(--header-height));
}
nav a {
display: block;
padding: 10px;
margin-bottom: 10px;
text-decoration: none;
color: #333;
border-radius: 5px;
transition: background-color 0.3s ease;
}
nav a:hover {
background-color: #ddd;
}
.content {
flex: 1;
padding: 20px;
}

36
frontend/Web/basic.js Normal file
View File

@ -0,0 +1,36 @@
function showDropdown() {
document.querySelector('.dropdown-content').style.display = 'block';
}
function hideDropdown() {
document.querySelector('.dropdown-content').style.display = 'none';
}
function ChangeUsername(newUsername) {
document.getElementById('username').textContent = newUsername;
}
async function UpdateUserInfo(){
let nm=await GetMyName();
let pri=await GetMyPrivilege();
if(nm=='')
{
await RefreshSession();
nm=await GetMyName();
pri=await GetMyPrivilege();
}
if(pri==0)
{
ChangeUsername("[Guest]");
}
else if(pri==1)
{
ChangeUsername(nm);
}
else if(pri==3)
{
ChangeUsername(nm+" [Worker]");
}
else if(pri==7)
{
ChangeUsername(nm+" [Admin]");
}
}

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BookManagement - Admin Panel - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.book-management {
margin-top: 20px;
}
.book-form,
.purchase-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
max-width: 600px;
margin: 20px auto;
}
.book-form label,
.purchase-form label {
font-weight: bold;
margin-bottom: 5px;
}
.book-form input,
.purchase-form input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.book-form button,
.purchase-form button {
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.book-form button:hover,
.purchase-form button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
<a href="/user-management">User Management</a>
<a href="/book-management">Book Management</a>
<a href="/log-query">Log Query</a>
</nav>
<div class="content">
<div class="book-management">
<h2>Book Management</h2>
<div class="book-form">
<label for="isbn">ISBN:</label>
<input type="text" id="isbn" required>
<label for="newIsbn">New ISBN:</label>
<input type="text" id="newIsbn" placeholder="Leave blank for not change">
<label for="title">Title:</label>
<input type="text" id="title" placeholder="Leave blank for not change">
<label for="author">Author:</label>
<input type="text" id="author" placeholder="Leave blank for not change">
<label for="keywords">Keywords:</label>
<input type="text" id="keywords" placeholder="Leave blank for not change">
<label for="price">Price:</label>
<input type="text" id="price" placeholder="Leave blank for not change">
<button onclick="updateBook()">Update Book Info</button>
</div>
<div class="purchase-form">
<label for="quantity">Quantity:</label>
<input type="number" id="quantity" required>
<label for="totalCost">Total Cost:</label>
<input type="text" id="totalCost" required>
<button onclick="purchaseBooks()">Import Books</button>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<3)
{
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in as worker or root first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
async function updateBook() {
let isbn = document.getElementById('isbn').value;
let newIsbn = document.getElementById('newIsbn').value;
let title = document.getElementById('title').value;
let author = document.getElementById('author').value;
let keywords = document.getElementById('keywords').value;
let price = document.getElementById('price').value;
let cmd="modify";
await Request("select "+isbn);
if(newIsbn!="") cmd+=" -ISBN="+newIsbn;
if(title!="") cmd+=" -name=\""+title+"\"";
if(author!="") cmd+=" -author=\""+author+"\"";
if(keywords!="") cmd+=" -keyword=\""+keywords+"\"";
if(price!="") cmd+=" -price="+price;
let result = await Request(cmd);
if(result == '[empty]') {
document.querySelector('.content').innerHTML='<div class="info-box"><h2>Successfully updated book info</h2><p>Auto refreshing page in 5 seconds...</p></div>';
setTimeout(function(){location.reload();},5000);
} else {
alert('Failed to update book info.');
}
}
async function purchaseBooks() {
let isbn = document.getElementById('isbn').value;
let quantity = document.getElementById('quantity').value;
let totalCost = document.getElementById('totalCost').value;
await Request("select "+isbn);
let result = await Request('import '+quantity+' '+totalCost);
if(result == '[empty]') {
document.querySelector('.content').innerHTML='<div class="info-box"><h2>Successfully purchased books</h2><p>Auto refreshing page in 5 seconds...</p></div>';
setTimeout(function(){location.reload();},5000);
} else {
alert('Failed to purchase books.');
}
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

BIN
frontend/Web/book.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

126
frontend/Web/buy.html Normal file
View File

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buy - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.buy-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: #fff;
border: none;
cursor: pointer;
}
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<div class="buy-box">
<form onsubmit="console.log('trying passwd'); TryBuy(); return false;">
<input id="isbn" type="text" placeholder="ISBN" required>
<input id="quantity" type="text" placeholder="Quantity" required>
<button type="submit">Buy</button>
</form>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<1)
{
document.querySelector('.buy-box').style.display = 'none';
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
function isFloat(str) {
// 使用parseFloat尝试将字符串转换为浮点数
var floatValue = parseFloat(str);
// 使用isNaN检查是否成功转换并确保不是Infinity或NaN
return !isNaN(floatValue) && isFinite(floatValue);
}
async function TryBuy()
{
console.log("TryBuy called");
var isbn = document.getElementById("isbn").value;
var quantity = document.getElementById("quantity").value;
var ret=await Request("buy "+isbn+" "+quantity);
if(isFloat(ret))
{
document.getElementById("isbn").value="";
document.getElementById("quantity").value="";
document.querySelector('.buy-box').style.display = 'none';
document.querySelector('.content').innerHTML = '<div class="info-box"><h3>Successfully bought book with ISBN='+isbn+'</h3><h3>Used '+ret+'RMB</h3><p>Auto refreshing page in 10 seconds...</p></div>';
setTimeout(function(){location.reload();},10000);
}
else alert("Invalid bookname or quantity");
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

View File

@ -20,16 +20,42 @@ socket.on('response', (msg) => {
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
async function RawRequest(raw_request){ let raw_request_lock = false;
socket.emit('request', raw_request);
while(true) async function acquireRawRequestLock() {
{ return new Promise((resolve) => {
if(__raw_response!=""){ const attemptAcquire = () => {
let response=__raw_response; if (!raw_request_lock) {
__raw_response=""; raw_request_lock = true;
return response; resolve();
} else {
setTimeout(attemptAcquire, 3);
}
};
attemptAcquire();
});
}
function releaseRawRequestLock() {
raw_request_lock = false;
}
async function RawRequest(raw_request) {
await acquireRawRequestLock();
try {
socket.emit('request', raw_request);
while (true) {
if (__raw_response !== "") {
let response = __raw_response;
__raw_response = "";
return response;
}
await sleep(10);
} }
await sleep(100); } finally {
releaseRawRequestLock();
} }
} }
async function ShutDownWholeSystem() async function ShutDownWholeSystem()
@ -69,27 +95,19 @@ async function Request(req)
localStorage.setItem("operation_count", operation_count); localStorage.setItem("operation_count", operation_count);
return response; return response;
} }
(async () => {
if(session_token==null||outhentication_key==null||operation_count==null){
let tmp_channel=generateRandomString(10);
let ret=await RawRequest('#OpenSession '+tmp_channel);
operation_count=0;
session_token=ret.split('\n')[1].split(' ')[0];
outhentication_key=ret.split('\n')[1].split(' ')[1];
localStorage.setItem("session_token", session_token);
localStorage.setItem("outhentication_key", outhentication_key);
localStorage.setItem("operation_count", operation_count);
}
})();
async function GetMyName() async function GetMyName()
{ {
session_token=localStorage.getItem("session_token");
outhentication_key=localStorage.getItem("outhentication_key");
let ret = await RawRequest("#Who "+session_token+" InfoQuery "+outhentication_key); let ret = await RawRequest("#Who "+session_token+" InfoQuery "+outhentication_key);
return ret.split('\n')[1].split(' ')[0]; return ret.split('\n')[1].split(' ')[0];
} }
async function GetMyPrivilege() async function GetMyPrivilege()
{ {
session_token=localStorage.getItem("session_token");
outhentication_key=localStorage.getItem("outhentication_key");
let ret = await RawRequest("#Who "+session_token+" InfoQuery "+outhentication_key); let ret = await RawRequest("#Who "+session_token+" InfoQuery "+outhentication_key);
return parseInt(ret.split('\n')[1].split(' ')[1]); return parseInt(ret.split('\n')[1].split(' ')[1]);
} }

View File

@ -1,13 +1,53 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZYM's Book Store</title> <title>ZYM's Book Store</title>
<style> <link rel="stylesheet" href="basic.css">
</style> </head>
</head> <body>
<body>
<script src="/socket.io/socket.io.js"></script> <header>
<script src="/communication.js"></script> <h1>ZYM's Book Store</h1>
</body> <div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<!-- Your content goes here -->
<p>FBI warning: This is a homework demo, not a real bookstore system. Data privacy and safety is not guaranteed as any bug could happen.</p>
<p>User mannual can be found at <a href="https://github.com/happyZYM/BH-Bookstore-2023/" target="_blank">here</a></p>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
console.log("receive event session ready");
await UpdateUserInfo();
});
</script>
<script src="/sessioninit.js"></script>
</body>
</html> </html>

View File

@ -2,6 +2,8 @@ const express = require('express');
const { createServer } = require('node:http'); const { createServer } = require('node:http');
const { join } = require('node:path'); const { join } = require('node:path');
const { Server } = require('socket.io'); const { Server } = require('socket.io');
const IsValid=require('./validator.js');
const Validing=process.env['VALIDING'];
const app = express(); const app = express();
const server = createServer(app); const server = createServer(app);
@ -44,12 +46,18 @@ async function GetResult(session_token,operation_token) {
{ {
if(message_map.get(session_token).has(operation_token)) if(message_map.get(session_token).has(operation_token))
{ {
const ret=message_map.get(session_token).get(operation_token); let ret=message_map.get(session_token).get(operation_token);
message_map.get(session_token).delete(operation_token); message_map.get(session_token).delete(operation_token);
if(Validing=='True'){
if(!(await IsValid(ret)))
{
ret="Invalid Content";
}
}
return ret; return ret;
} }
} }
await sleep(100); await sleep(10);
} }
} }
@ -103,9 +111,22 @@ backend.stdout.on('data', (data) => {
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html')); res.sendFile(join(__dirname, 'index.html'));
}); });
app.get('/favicon.ico', (req, res) => {
res.sendFile(join(__dirname, 'book.ico'));
});
app.get('/communication.js', (req, res) => { app.get('/communication.js', (req, res) => {
res.sendFile(join(__dirname, 'communication.js')); res.sendFile(join(__dirname, 'communication.js'));
}); });
app.get('/sessioninit.js', (req, res) => {
res.sendFile(join(__dirname, 'sessioninit.js'));
});
app.get('/basic.js', (req, res) => {
res.sendFile(join(__dirname, 'basic.js'));
});
app.get('/basic.css', (req, res) => {
res.sendFile(join(__dirname, 'basic.css'));
});
io.on('connection', async (socket) => { io.on('connection', async (socket) => {
console.log('a user connected'); console.log('a user connected');
@ -114,17 +135,66 @@ io.on('connection', async (socket) => {
}); });
socket.on('request', async (msg) => { socket.on('request', async (msg) => {
console.log('message: ' + msg); console.log('message: ' + msg);
if(Validing=='True'){
if(!(await IsValid(msg)))
{
socket.emit('response', "Invalid Input");
return;
}
}
const substrings = msg.trim().split('\n')[0].split(' '); const substrings = msg.trim().split('\n')[0].split(' ');
if(substrings.length<2)
{
console.log("input has "+substrings.length+" words");
socket.emit('response', "Invalid Input");
return;
}
const head=substrings[0]; const head=substrings[0];
const session_token=substrings[1]; const session_token=substrings[1];
if(head[1]=='O') if(head[1]=='O')
{ {
if(msg.split('\n').length!=1)
{
console.log("O: input has "+msg.split('\n').length+" lines");
socket.emit('response', "Invalid Input");
return;
}
SendRequest(msg); SendRequest(msg);
ret=await GetResult(session_token,"Init"); ret=await GetResult(session_token,"Init");
console.log("ret: "+ret); console.log("ret: "+ret);
socket.emit('response', ret); socket.emit('response', ret);
} }
else{ else{
if(head[1]!='S'&&head[1]!='C'&&head[1]!='W'&&head[1]!='R')
{
console.log("input has invalid head");
socket.emit('response', "Invalid Input");
return;
}
if(substrings.length!=4)
{
console.log("input has "+substrings.length+" words");
socket.emit('response', "Invalid Input");
return;
}
if(head[1]=='R')
{
if(msg.split('\n').length!=2)
{
console.log("R: input has "+msg.split('\n').length+" lines");
socket.emit('response', "Invalid Input");
return;
}
}
else
{
if(msg.split('\n').length!=1)
{
console.log("other: input has "+msg.split('\n').length+" lines");
socket.emit('response', "Invalid Input");
return;
}
}
const operation_token=substrings[2]; const operation_token=substrings[2];
const outhentication_key=substrings[3]; const outhentication_key=substrings[3];
const command=msg.trim().split('\n')[1]; const command=msg.trim().split('\n')[1];
@ -154,6 +224,42 @@ backend.on('exit', (code, signal) => {
process.exit(0); process.exit(0);
}); });
app.get('/login', (req, res) => {
res.sendFile(join(__dirname, 'login.html'));
});
app.get('/register', (req, res) => {
res.sendFile(join(__dirname, 'register.html'));
});
app.get('/passwd', (req, res) => {
res.sendFile(join(__dirname, 'passwd.html'));
});
app.get('/admin', (req, res) => {
res.sendFile(join(__dirname, 'admin.html'));
});
app.get('/buy', (req, res) => {
res.sendFile(join(__dirname, 'buy.html'));
});
app.get('/show', (req, res) => {
res.sendFile(join(__dirname, 'show.html'));
});
app.get('/user-management', (req, res) => {
res.sendFile(join(__dirname, 'user-management.html'));
});
app.get('/book-management', (req, res) => {
res.sendFile(join(__dirname, 'book-management.html'));
});
app.get('/log-query', (req, res) => {
res.sendFile(join(__dirname, 'log-query.html'));
});
server.listen(3000, () => { server.listen(3000, () => {
console.log('server running at http://localhost:3000'); console.log('server running at http://localhost:3000');
}); });

507
frontend/Web/log-query.html Normal file
View File

@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log - Admin Panel - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.query-section {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.custom-select {
position: relative;
display: inline-block;
width: 100%;
}
select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
background-color: #f8f8f8;
}
select:focus {
outline: none;
border-color: #007BFF;
}
select:after {
content: '\25BC'; /* Unicode character for down arrow */
font-size: 14px;
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
color: #555;
}
button {
padding: 10px 15px;
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
.log-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.log-table th, .log-table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
.log-table th {
background-color: #007BFF;
color: #fff;
}
.log-table td:first-child {
width: 50px; /* Adjust the width for the ID column */
}
.log-table td:nth-child(2) {
width: 150px; /* Adjust the width for the User column */
}
.log-table td:nth-child(3) {
width: 70%; /* Adjust the width for the Command column */
}
.log-table tr:hover {
background-color: #f5f5f5;
}
.finance-report-table {
width: 45%; /* 设置表格宽度,根据需要调整 */
border-collapse: collapse;
margin-right: 5%; /* 设置表格之间的间距 */
float: left; /* 设置浮动,使两个表格并排显示 */
}
.finance-report-table th, .finance-report-table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
.finance-report-table th {
background-color: #007BFF;
color: #fff;
}
.finance-report-table tr:hover {
background-color: #f5f5f5;
}
.finance-report-table caption {
caption-side: top;
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
}
/* Style for the worker log table */
.workerlog-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
/* Style for table caption */
.workerlog-table caption {
font-weight: bold;
margin-bottom: 10px;
}
/* Style for table header cells */
.workerlog-table th {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
background-color: #f2f2f2;
}
/* Style for table data cells */
.workerlog-table td {
border: 1px solid #ddd;
padding: 8px;
}
/* Set a specific percentage width for each column */
.workerlog-table th:nth-child(1),
.workerlog-table td:nth-child(1) {
width: 15%;
}
.workerlog-table th:nth-child(2),
.workerlog-table td:nth-child(2) {
width: 20%;
}
.workerlog-table th:nth-child(3),
.workerlog-table td:nth-child(3) {
width: 65%;
}
/* Responsive styling for smaller screens */
@media (max-width: 600px) {
.workerlog-table {
font-size: 12px;
}
.workerlog-table th, .workerlog-table td {
padding: 6px;
}
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
<a href="/user-management">User Management</a>
<a href="/book-management">Book Management</a>
<a href="/log-query">Log Query</a>
</nav>
<div class="content">
<div class="query-section">
<label for="queryType">Select Query Type:</label>
<select id="queryType">
<option value="financial">Financial Summary</option>
<option value="recentTransactions">Recent Transactions</option>
<option value="financialReport">Financial Report</option>
<option value="employeeReport">Employee Work Report</option>
<option value="systemLog">System Work Log</option>
</select>
<div id="recentTransactionsSection" style="display:none;">
<label for="transactionCount">Enter Number of Transactions:</label>
<input type="number" id="transactionCount" min="1" value="5">
</div>
<button onclick="handleQuery()">Query</button>
</div>
<div class="result-section" id="resultSection"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<7)
{
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in as root first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
async function handleQuery() {
const resultSection = document.getElementById('resultSection');
resultSection.innerHTML="";
const queryType = document.getElementById('queryType').value;
let queryParam = '';
if (queryType === 'recentTransactions') {
const transactionCount = document.getElementById('transactionCount').value;
let ret=await Request("show finance "+transactionCount);
if(ret=='') ret='+ 0.00 - 0.00';
if(ret!='Invalid'){
resultSection.innerHTML='<div class="info-box"><h2>Recent '+transactionCount+' Transactions</h2><h1>'+'+ '+ret.split(' ')[1]+'</h1><h1>'+'- '+ret.split(' ')[3]+'</h1></div>';
}
else{
resultSection.innerHTML='<div class="info-box"><h1>Invalid Input</h1></div>';
}
return;
}
else if(queryType=='financial')
{
let ret=await Request("show finance");
if(ret=='') ret='+ 0.00 - 0.00';
if(ret!='Invalid'){
resultSection.innerHTML='<div class="info-box"><h2>Financial Summary</h2><h1>'+'+ '+ret.split(' ')[1]+'</h1><h1>'+'- '+ret.split(' ')[3]+'</h1></div>';
}
else{
resultSection.innerHTML='<div class="info-box"><h1>Invalid Input</h1></div>';
}
return;
}
else if(queryType=='financialReport')
{
let ret=await Request("report finance");
if(ret=='Invalid')
{
resultSection.innerHTML='<div class="info-box"><h1>Invalid Input</h1></div>';
return;
}
/* This result is in the following format
Sale Ranking:
[ISBN]\t[Total Sale]\t[Total Cost]\n
--------------------
Cost Ranking:
[ISBN]\t[Total Cost]\t[Total Sale]\n
Display it in two tables side by side
*/
let ret_list=ret.split("\n");
let table1=document.createElement("table");
let table2=document.createElement("table");
// ad type for the table to suite css
table1.setAttribute("class","finance-report-table");
table2.setAttribute("class","finance-report-table");
table1.innerHTML = "<caption>Sale Ranking</caption>";
table2.innerHTML = "<caption>Cost Ranking</caption>";
let tr1=document.createElement("tr");
let tr2=document.createElement("tr");
let th1=document.createElement("th");
let th2=document.createElement("th");
let th3=document.createElement("th");
th1.innerHTML="ISBN";
th2.innerHTML="Total Sale";
th3.innerHTML="Total Cost";
tr1.appendChild(th1);
tr1.appendChild(th2);
tr1.appendChild(th3);
table1.appendChild(tr1);
th1=document.createElement("th");
th2=document.createElement("th");
th3=document.createElement("th");
th1.innerHTML="ISBN";
th2.innerHTML="Total Sale";
th3.innerHTML="Total Cost";
tr2.appendChild(th1);
tr2.appendChild(th2);
tr2.appendChild(th3);
table2.appendChild(tr2);
let flag=0;
console.log(ret_list);
for(let i=0;i<ret_list.length;i++)
{
if(ret_list[i]=="--------------------")
{
flag=1;
continue;
}
let ret=ret_list[i].split("\t");
if(ret.length<3)
continue;
let tr=document.createElement("tr");
let td1=document.createElement("td");
let td2=document.createElement("td");
let td3=document.createElement("td");
td1.innerHTML=ret[0];
td2.innerHTML=ret[2];
td3.innerHTML=ret[3];
if(flag==0)
{
tr.appendChild(td1);
tr.appendChild(td2);
tr.appendChild(td3);
table1.appendChild(tr);
}
else
{
tr.appendChild(td1);
tr.appendChild(td3);
tr.appendChild(td2);
table2.appendChild(tr);
}
}
resultSection.appendChild(table1);
resultSection.appendChild(table2);
return;
}
else if(queryType=='employeeReport')
{
let ret=await Request("report employee");
if(ret=='Invalid')
{
resultSection.innerHTML='<div class="info-box"><h1>Invalid Input</h1></div>';
return;
}
if(ret=='[empty]')
{
resultSection.innerHTML='<div class="info-box"><h1>No data</h1></div>';
return;
}
/*The return value is in the following format
---------- [name] ----------
[optid] [name] [command]\n
---------- [name] ----------
[optid] [name] [command]\n
Show these data in multiple tables. Each table is for one worker with caption of his name.
Note that there may be blanks in command.
*/
let ret_list=ret.split("\n");
// warning: there are multiple tables
let table=document.createElement("table");
// ad type for the table to suite css
table.setAttribute("class","workerlog-table");
let tr=document.createElement("tr");
let th1=document.createElement("th");
let th2=document.createElement("th");
let th3=document.createElement("th");
th1.innerHTML="ID";
th2.innerHTML="User";
th3.innerHTML="Command";
tr.appendChild(th1);
tr.appendChild(th2);
tr.appendChild(th3);
table.appendChild(tr);
let flag=0;
for(let i=0;i<ret_list.length;i++)
{
if(ret_list[i].startsWith("----------"))
{
if(flag==1)
resultSection.appendChild(table);
flag=1;
table=document.createElement("table");
// ad type for the table to suite css
table.setAttribute("class","workerlog-table");
let tr=document.createElement("tr");
let th1=document.createElement("th");
let th2=document.createElement("th");
let th3=document.createElement("th");
th1.innerHTML="ID";
th2.innerHTML="User";
th3.innerHTML="Command";
tr.appendChild(th1);
tr.appendChild(th2);
tr.appendChild(th3);
table.appendChild(tr);
let caption=document.createElement("caption");
caption.innerHTML=ret_list[i].split(" ")[1];
table.appendChild(caption);
continue;
}
let log=ret_list[i].split(" ");
if(log.length<3)
continue;
let tr=document.createElement("tr");
let td1=document.createElement("td");
let td2=document.createElement("td");
let td3=document.createElement("td");
td1.innerHTML=log[0];
td2.innerHTML=log[1];
td3.innerHTML=log.slice(2).join(" ");
tr.appendChild(td1);
tr.appendChild(td2);
tr.appendChild(td3);
table.appendChild(tr);
}
if(flag==1)
resultSection.appendChild(table);
return;
}
else if(queryType=='systemLog')
{
let ret=await Request("log");
let log_list=ret.split("\n");
// note that each line is in the format of [id] [user] [command]
// warning: their may be blanks in command
// show the three items in a table
let table=document.createElement("table");
// ad type for the table to suite css
table.setAttribute("class","log-table");
let tr=document.createElement("tr");
let th1=document.createElement("th");
let th2=document.createElement("th");
let th3=document.createElement("th");
th1.innerHTML="ID";
th2.innerHTML="User";
th3.innerHTML="Command";
tr.appendChild(th1);
tr.appendChild(th2);
tr.appendChild(th3);
table.appendChild(tr);
for(let i=0;i<log_list.length;i++)
{
let log=log_list[i].split(" ");
if(log.length<3)
continue;
let tr=document.createElement("tr");
let td1=document.createElement("td");
let td2=document.createElement("td");
let td3=document.createElement("td");
td1.innerHTML=log[0];
td2.innerHTML=log[1];
td3.innerHTML=log.slice(2).join(" ");
tr.appendChild(td1);
tr.appendChild(td2);
tr.appendChild(td3);
table.appendChild(tr);
}
resultSection.appendChild(table);
return;
}
// Call a function or make an AJAX request to handle the query with the selected parameters
// For example: Request('query?type=' + queryType + queryParam, handleQueryResponse);
}
// Show/hide the 'Enter Number of Transactions' section based on the selected query type
document.getElementById('queryType').addEventListener('change', function() {
const recentTransactionsSection = document.getElementById('recentTransactionsSection');
recentTransactionsSection.style.display = this.value === 'recentTransactions' ? 'block' : 'none';
});
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

110
frontend/Web/login.html Normal file
View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.login-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: #fff;
border: none;
cursor: pointer;
}
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<div class="login-box">
<form onsubmit="console.log('trying login'); TryLogin(); return false;">
<input id="user_name" type="text" placeholder="Username" required>
<input id="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
});
async function TryLogin()
{
console.log("TryLogin called");
var username = document.getElementById("user_name").value;
var password = document.getElementById("password").value;
var ret=await Request("su "+username+" "+password);
if(ret=="[empty]")
{
await UpdateUserInfo();
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Login Success</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
else alert("Invalid username or password");
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

View File

@ -9,6 +9,7 @@
"version": "0.0.0.1", "version": "0.0.0.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@alicloud/pop-core": "^1.7.13",
"async-lock": "^1.4.0", "async-lock": "^1.4.0",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"express": "^4.18.2", "express": "^4.18.2",
@ -16,6 +17,34 @@
"socket.io": "^4.7.2" "socket.io": "^4.7.2"
} }
}, },
"node_modules/@alicloud/pop-core": {
"version": "1.7.13",
"resolved": "https://registry.npmjs.org/@alicloud/pop-core/-/pop-core-1.7.13.tgz",
"integrity": "sha512-agzr4DU+aAGW7/2mp2hP1JcNJkn/zBS0jUGQt5etIASN0MVq1tMdudVqvWbExUG0mUouo/n2VgdnjOHjswvrlA==",
"dependencies": {
"debug": "^3.1.0",
"httpx": "^2.1.2",
"json-bigint": "^1.0.0",
"kitx": "^1.2.1",
"xml2js": "^0.5.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@alicloud/pop-core/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/@alicloud/pop-core/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/@socket.io/component-emitter": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@ -80,6 +109,14 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"engines": {
"node": "*"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@ -455,6 +492,36 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/httpx": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.1.tgz",
"integrity": "sha512-l5rcAoKP8A9XOIlcIA87Wt9A7AX2fgOslHOF4WB5Q24y/1+aeH8b7c7NKfm+Bcf+h0u4FHNtLCriC4mAFmCYgg==",
"dependencies": {
"@types/node": "^20",
"debug": "^4.1.1"
}
},
"node_modules/httpx/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/httpx/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -479,6 +546,19 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/kitx": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/kitx/-/kitx-1.3.0.tgz",
"integrity": "sha512-fhBqFlXd0GkKTB+8ayLfpzPUw+LHxZlPAukPNBD1Om7JMeInT+/PxCAf1yLagvD+VKoyWhXtJR68xQkX/a0wOQ=="
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -660,6 +740,11 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
},
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
@ -894,6 +979,26 @@
"optional": true "optional": true
} }
} }
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"engines": {
"node": ">=4.0"
}
} }
} }
} }

View File

@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"@alicloud/pop-core": "^1.7.13",
"async-lock": "^1.4.0", "async-lock": "^1.4.0",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"express": "^4.18.2", "express": "^4.18.2",

120
frontend/Web/passwd.html Normal file
View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>change password - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.passwd-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: #fff;
border: none;
cursor: pointer;
}
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<div class="passwd-box">
<form onsubmit="console.log('trying passwd'); TryPasswd(); return false;">
<input id="user_name" type="text" placeholder="Username" required>
<input id="old_password" type="password" placeholder="Old Password">
<input id="new_password" type="password" placeholder="New Password" required>
<button type="submit">Reset Password</button>
</form>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<1)
{
document.querySelector('.passwd-box').style.display = 'none';
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
async function TryPasswd()
{
console.log("TryPasswd called");
var username = document.getElementById("user_name").value;
var old_password = document.getElementById("old_password").value;
var new_password = document.getElementById("new_password").value;
var ret=await Request("passwd "+username+" "+old_password+" "+new_password);
if(ret=="[empty]")
{
// 删除注册框,在中间显示“注册成功”,三秒后自动跳转到首页
document.querySelector('.passwd-box').style.display = 'none';
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Successfully changed password</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
else alert("Invalid username or old password");
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

114
frontend/Web/register.html Normal file
View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>register - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.register-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: #fff;
border: none;
cursor: pointer;
}
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<div class="register-box">
<form onsubmit="console.log('trying register'); TryRegister(); return false;">
<input id="user_name" type="text" placeholder="Username" required>
<input id="password" type="password" placeholder="Password" required>
<input id="nickname" type="text" placeholder="Nickname" required>
<button type="submit">Register</button>
</form>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
});
async function TryRegister()
{
console.log("TryRegister called");
var username = document.getElementById("user_name").value;
var password = document.getElementById("password").value;
var nick_name = document.getElementById("nickname").value;
var ret=await Request("register "+username+" "+password+" "+nick_name);
if(ret=="[empty]")
{
// 删除注册框,在中间显示“注册成功”,三秒后自动跳转到首页
document.querySelector('.register-box').style.display = 'none';
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Register Success</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
else alert("Invalid username or password");
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

View File

@ -0,0 +1,15 @@
var SessionReadyEvent = new Event('SessionReady');
(async () => {
if(session_token==null||outhentication_key==null||operation_count==null){
let tmp_channel=generateRandomString(10);
let ret=await RawRequest('#OpenSession '+tmp_channel);
operation_count=0;
session_token=ret.split('\n')[1].split(' ')[0];
outhentication_key=ret.split('\n')[1].split(' ')[1];
localStorage.setItem("session_token", session_token);
localStorage.setItem("outhentication_key", outhentication_key);
localStorage.setItem("operation_count", operation_count);
}
document.dispatchEvent(SessionReadyEvent);
console.log("Sent event Session Ready");
})();

252
frontend/Web/show.html Normal file
View File

@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.search-panel {
margin-bottom: 20px;
text-align: center;
}
.search-panel button,
.search-panel form {
display: inline-block; /* 或者使用 display: inline-flex; */
}
.search-panel button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.search-panel label {
margin-right: 10px;
}
.search-panel select,
.search-panel input {
padding: 8px;
margin-right: 10px;
}
#searchResults {
margin-top: 20px;
}
#searchResults table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
#searchResults th, #searchResults td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
#searchResults th {
background-color: #4CAF50;
color: white;
}
#searchResults td {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
</nav>
<div class="content">
<div class="search-panel">
<button onclick="ShowAll()">Show All Books</button>
<form onsubmit="console.log('trying passwd'); performSearch(); return false;">
<label for="searchType">Or Search by:</label>
<select id="searchType">
<option value="isbn">ISBN</option>
<option value="title">Title</option>
<option value="author">Author</option>
<option value="keyword">Keyword</option>
</select>
<input type="text" id="searchInput" placeholder="Enter search term" required>
<button type="submit">Search</button>
</form>
</div>
<div id="searchResults"></div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<1)
{
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
function performSearch() {
// Get the selected search type and the search input value
const searchType = document.getElementById('searchType').value;
const searchInput = document.getElementById('searchInput').value;
// Call a function to handle the search logic and display results
handleSearch(searchType, searchInput);
}
async function handleSearch(searchType, searchInput) {
let ret;
if(searchType=='isbn')
{
ret=await Request("show -ISBN="+searchInput);
}
else if(searchType=='title')
{
ret=await Request("show -name=\""+searchInput+"\"");
}
else if(searchType=='author')
{
ret=await Request("show -author=\""+searchInput+"\"");
}
else if(searchType=='keyword')
{
ret=await Request("show -keyword=\""+searchInput+"\"");
}
if(ret=="Invalid")
{
alert("Invalid input");
return;
}
// Implement your logic for handling the search here
// You can use AJAX, fetch, or any other method to send a request to the server
// and get the search results
// For now, let's just display a sample result in a table
const resultsContainer = document.getElementById('searchResults');
if(ret=='')
{
resultsContainer.innerHTML = '<h2>No results found</h2>';
return;
}
const table = document.createElement('table');
table.innerHTML = `
<tr>
<th>ISBN</th>
<th>Title</th>
<th>Author</th>
<th>Keywords</th>
<th>Price</th>
<th>Stock</th>
</tr>`;
const results=ret.split('\n');
for(let i=0;i<results.length;i++)
{
const row=document.createElement('tr');
const cols=results[i].split('\t');
for(let j=0;j<cols.length;j++)
{
const col=document.createElement('td');
col.innerHTML=cols[j];
row.appendChild(col);
}
table.appendChild(row);
}
// Clear previous search results and append the new table
resultsContainer.innerHTML = '';
resultsContainer.appendChild(table);
}
async function ShowAll() {
let ret;
ret=await Request("show");
if(ret=="Invalid")
{
alert("Invalid input");
return;
}
// Implement your logic for handling the search here
// You can use AJAX, fetch, or any other method to send a request to the server
// and get the search results
// For now, let's just display a sample result in a table
const resultsContainer = document.getElementById('searchResults');
if(ret=='')
{
resultsContainer.innerHTML = '<h2>No results found</h2>';
return;
}
const table = document.createElement('table');
table.innerHTML = `
<tr>
<th>ISBN</th>
<th>Title</th>
<th>Author</th>
<th>Keywords</th>
<th>Price</th>
<th>Stock</th>
</tr>`;
const results=ret.split('\n');
for(let i=0;i<results.length;i++)
{
const row=document.createElement('tr');
const cols=results[i].split('\t');
for(let j=0;j<cols.length;j++)
{
const col=document.createElement('td');
col.innerHTML=cols[j];
row.appendChild(col);
}
table.appendChild(row);
}
// Clear previous search results and append the new table
resultsContainer.innerHTML = '';
resultsContainer.appendChild(table);
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

View File

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UserManagement - Admin Panel - ZYM's Book Store</title>
<link rel="stylesheet" href="basic.css">
<style>
.info-box {
width: 300px;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.user-management {
margin-top: 20px;
}
.user-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
max-width: 600px;
margin: 20px auto;
}
.user-form label {
font-weight: bold;
margin-bottom: 5px;
}
.user-form input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.user-form button {
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.user-form button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<header>
<h1>ZYM's Book Store</h1>
<div class="user-bar">
<span>Welcome, <span id="username">[Guest]</span></span>
<div class="action-button-container" onmouseover="showDropdown()" onmouseout="hideDropdown()">
<div class="action-button">Actions</div>
<div class="dropdown">
<div class="dropdown-content">
<a href="/login">Login</a>
<a href="#" onclick="(async () => { await Request('logout'); await UpdateUserInfo(); location.reload();})(); return false;">Logout</a>
<a href="/register">Register</a>
<a href="/passwd">Change Password</a>
<a href="/admin">Admin Panel</a>
</div>
</div>
</div>
</div>
</header>
<div class="main-content">
<nav>
<a href="/">Home Page</a>
<a href="/show">Search Books</a>
<a href="/buy">Purchase Books</a>
<a href="/user-management">User Management</a>
<a href="/book-management">Book Management</a>
<a href="/log-query">Log Query</a>
</nav>
<div class="content">
<div class="user-management">
<h2>User Management</h2>
<div class="user-form">
<label for="userId">User ID:</label>
<input type="text" id="userId" required>
<label for="password">Password:</label>
<input type="password" id="password">
<label for="privilege">Privilege:</label>
<input type="number" id="privilege">
<label for="username_input">Username:</label>
<input type="text" id="username_input">
<button onclick="addUser()">Add User</button>
<button onclick="deleteUser()">Delete User</button>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/communication.js"></script>
<script src="/basic.js"></script>
<script>
document.addEventListener('SessionReady', async () => {
await UpdateUserInfo();
if((await GetMyPrivilege())<3)
{
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Please log in as worker or root first.</h2><p>Redirecting to home page in 3 seconds...</p></div>';
setTimeout(function(){window.location.href="/";},3000);
}
});
async function addUser()
{
var userId = document.getElementById("userId").value;
var password = document.getElementById("password").value;
var privilege = document.getElementById("privilege").value;
var username = document.getElementById("username_input").value;
if(userId=="" || password=="" || privilege=="" || username=="")
{
alert("Please fill in all fields.");
return;
}
if(privilege!=1&&privilege!=3&&privilege!=7)
{
alert("Invalid privilege.");
return;
}
if(!confirm("Are you sure to add this user?"))
return;
var result = await Request('useradd '+userId+' '+password+' '+privilege+' '+username);
if(result=="[empty]")
{
document.getElementById("userId").value = "";
document.getElementById("password").value = "";
document.getElementById("privilege").value = "";
document.getElementById("username_input").value = "";
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Successfully added user</h2><p>Auto refreshing page in 5 seconds...</p></div>';
setTimeout(function(){location.reload();},5000);
}
else
alert("Failed to add user");
}
async function deleteUser()
{
var userId = document.getElementById("userId").value;
if(userId=="")
{
alert("Please fill in user ID.");
return;
}
if(!confirm("Are you sure to delete this user?"))
return;
// confirm again
if(!confirm("Are you REALLY sure to delete this user?"))
return;
var result = await Request('delete '+userId);
if(result=="[empty]")
{
document.getElementById("userId").value = "";
document.getElementById("password").value = "";
document.getElementById("privilege").value = "";
document.getElementById("username_input").value = "";
document.querySelector('.content').innerHTML = '<div class="info-box"><h2>Successfully deleted user</h2><p>Auto refreshing page in 5 seconds...</p></div>';
setTimeout(function(){location.reload();},5000);
}
else
alert("Failed to delete user");
}
</script>
<script src="/sessioninit.js"></script>
</body>
</html>

96
frontend/Web/validator.js Normal file
View File

@ -0,0 +1,96 @@
const RPCClient = require("@alicloud/pop-core");
console.log(process.env['ALIBABA_CLOUD_ACCESS_KEY_ID'])
console.log(process.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET'])
// 注意此处实例化的client请尽可能重复使用避免重复建立连接提升检测性能。
let client;
if(process.env['VALIDING']=='True'){
client=new RPCClient({
/**
* 阿里云账号AccessKey拥有所有API的访问权限建议您使用RAM用户进行API访问或日常运维。
* 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里否则可能导致AccessKey泄露威胁您账号下所有资源的安全。
* 常见获取环境变量方式:
* 获取RAM用户AccessKey IDprocess.env['ALIBABA_CLOUD_ACCESS_KEY_ID']
* 获取RAM用户AccessKey Secretprocess.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET']
*/
accessKeyId: process.env['ALIBABA_CLOUD_ACCESS_KEY_ID'],
accessKeySecret: process.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
// 接入区域和地址请根据实际情况修改
endpoint: "https://green-cip.cn-beijing.aliyuncs.com",
apiVersion: '2022-03-02',
// 设置http代理
// httpProxy: "http://xx.xx.xx.xx:xxxx",
// 设置https代理
// httpsProxy: "https://username:password@xxx.xxx.xxx.xxx:9999",
});
}
async function IsValid(text) {
console.log("IsValid called");
console.log("cheching text: "+text);
// 去除text中的空格、tab、无意义特殊符号!@#$%^&*()_+_=~`{}|[]\;':",./<>?)等。
//text = text.replace(/\s+/g, "");
text = text.replace(/[!@#$%^&*()_+_=~`{}|[\]\\;':",./<>?]+/g, "");
// 再去除换行和回车
//text = text.replace(/[\r\n]/g, "");
console.log("after replace: "+text);
if(text.length==0||text.length>590) return true;
// 通过以下代码创建API请求并设置参数。
const params = {
// 文本检测service内容安全控制台文本增强版规则配置的serviceCode示例comment_detection
"Service": "comment_detection",
"ServiceParameters": JSON.stringify({
//待检测文本内容。
"content": text
}),
};
const requestOption = {
method: 'POST',
formatParams: false,
};
let response;
try {
// 使用Promise.race设置超时时间为3000毫秒。
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 3000-100)
);
// 调用接口获取检测结果同时等待timeoutPromise。
response = await Promise.race([
client.request('TextModeration', params, requestOption),
timeoutPromise
]);
// 自动路由。
if (response.Code === 500) {
// 区域切换到cn-beijing。
client.endpoint = "https://green-cip.cn-shanghai.aliyuncs.com";
// 重新调用接口并再次等待timeoutPromise。
response = await Promise.race([
client.request('TextModeration', params, requestOption),
timeoutPromise
]);
}
// 处理正常响应的逻辑,例如返回结果等。
// ...
} catch (error) {
return true;
// 处理超时错误或其他错误。
console.error(error.message);
// 进行适当的错误处理,例如重试、记录日志等。
// ...
}
if(response['Message']=='OK')
{
console.log(response['Data']['reason']);
return response['Data']['reason']=="";
}
return false;
}
module.exports=IsValid;

BIN
frontend/client/book.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,8 +1,19 @@
const { app, BrowserWindow } = require('electron'); const { app, BrowserWindow } = require('electron');
function createWindow() { function createWindow() {
const win = new BrowserWindow(); const win = new BrowserWindow({
win.loadURL('http://localhost:4000/'); fullscreen: true, // 设置默认全屏
frame: false, // 隐藏导航栏
});
win.loadURL('http://bookstore.zymsite.ink/');
// 监听按键事件
win.webContents.on('before-input-event', (event, input) => {
if (input.key === 'Escape') {
app.quit(); // 按下Esc键时退出程序
}
});
} }
app.whenReady().then(createWindow); app.whenReady().then(createWindow);

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,42 @@
{ {
"name": "bookstoreclient", "name": "bookstoreclient",
"version": "1.0.0", "version": "1.1.3",
"description": "Book Store Client", "description": "Book Store Client",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ." "start": "electron .",
"pack": "electron-builder --dir",
"dist": "electron-builder"
}, },
"author": "ZhuangYumin", "author": "ZhuangYumin",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"electron": "^27.1.2"
},
"devDependencies": {
"electron-builder": "^24.9.1"
},
"build": {
"appId": "ZhuangYumin.homework.bookstoreclient",
"mac": {
"target": "dmg"
},
"win": {
"icon":"book.ico",
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}
]
},
"linux": {
"target": ["AppImage", "deb"],
"category": "Utility"
}
} }
} }