添加 Vue 组件、插件和配置文件,包含用户认证、对话框和样式设置
This commit is contained in:
parent
58e93e295b
commit
7d94b2fa66
128
src/main.c
128
src/main.c
@ -1,21 +1,141 @@
|
|||||||
|
#include "server/auth.h"
|
||||||
|
#include "server/config.h"
|
||||||
|
#include "server/types.h"
|
||||||
|
|
||||||
|
#include "jwt/jwt.h"
|
||||||
|
|
||||||
|
#include "db/auth.h"
|
||||||
|
#include "db/db.h"
|
||||||
|
|
||||||
|
|
||||||
#include <civetweb.h>
|
#include <civetweb.h>
|
||||||
|
|
||||||
#include <kqm/defs.h>
|
#include <signal.h>
|
||||||
#include <kqm/types.h>
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
leveldb_t* db = NULL;
|
||||||
|
|
||||||
|
void signal_handler(int signal)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "Signal %d received, cleaning up...\n", signal);
|
||||||
|
close_db(db);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
const char* options[] = {"document_root", "./www", "listening_ports", "8080", NULL};
|
open_user_db();
|
||||||
|
|
||||||
|
// while (1) {
|
||||||
|
// printf("> ");
|
||||||
|
// char op[16];
|
||||||
|
// scanf("%15s", op);
|
||||||
|
// if (strcmp(op, "exit") == 0) break;
|
||||||
|
// if (strcmp(op, "login") == 0) {
|
||||||
|
// char user_id[64];
|
||||||
|
// char password[64];
|
||||||
|
// scanf("%63s %63s", user_id, password);
|
||||||
|
// int result;
|
||||||
|
// int flag = login(user_id, password, &result);
|
||||||
|
// if (!flag) {
|
||||||
|
// printf("Failed to login\n");
|
||||||
|
// } else {
|
||||||
|
// printf("Result: %s\n", result ? "login success" : "login failed");
|
||||||
|
// }
|
||||||
|
// }else if(strcmp(op, "logout") == 0){
|
||||||
|
// char user_id[64];
|
||||||
|
// scanf("%63s", user_id);
|
||||||
|
// int flag = logout(user_id);
|
||||||
|
// if (!flag) {
|
||||||
|
// printf("Failed to logout\n");
|
||||||
|
// } else {
|
||||||
|
// printf("Logout success\n");
|
||||||
|
// }
|
||||||
|
// } else if (strcmp(op, "register") == 0) {
|
||||||
|
// char user_id[64];
|
||||||
|
// char password[64];
|
||||||
|
// scanf("%63s %63s", user_id, password);
|
||||||
|
// int result;
|
||||||
|
// int flag = check_user_exists(user_id, &result);
|
||||||
|
// if(!flag){
|
||||||
|
// printf("Failed to check user existence\n");
|
||||||
|
// continue;
|
||||||
|
// }else if (result) {
|
||||||
|
// printf("User already exists\n");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// flag = set_user_password(user_id, password);
|
||||||
|
// if (!flag) {
|
||||||
|
// printf("Failed to register\n");
|
||||||
|
// } else {
|
||||||
|
// printf("Registration success\n");
|
||||||
|
// }
|
||||||
|
// } else if (strcmp(op, "check") == 0) {
|
||||||
|
// char user_id[64];
|
||||||
|
// scanf("%63s", user_id);
|
||||||
|
// int result;
|
||||||
|
// int flag = check_user_exists(user_id, &result);
|
||||||
|
// if (!flag) {
|
||||||
|
// printf("Failed to check user existence\n");
|
||||||
|
// } else {
|
||||||
|
// printf("Result: %s\n", result ? "user exists" : "user does not exist");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getchar();
|
||||||
|
|
||||||
|
// char* jwt = create_token("Hello", "world");
|
||||||
|
// printf("JWT: %s\n", jwt);
|
||||||
|
// int flag = verify_token(jwt, "world");
|
||||||
|
// printf("Flag: %d\n", flag);
|
||||||
|
// char* id = get_payload(jwt);
|
||||||
|
// printf("ID: %s\n", id);
|
||||||
|
// free(id);
|
||||||
|
// free(jwt);
|
||||||
|
|
||||||
|
// if (!(db = open_db("db/main"))) return 1;
|
||||||
|
|
||||||
|
// signal(SIGINT, signal_handler);
|
||||||
|
// signal(SIGTERM, signal_handler);
|
||||||
|
// signal(SIGSEGV, signal_handler);
|
||||||
|
|
||||||
|
config_t config;
|
||||||
|
int stat = config_read(&config, "configs/config.json");
|
||||||
|
if (!stat) {
|
||||||
|
printf("config_read() returned %d\n", stat);
|
||||||
|
config_dtor(&config);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (config.server_port > 65535 || config.server_port <= 0) {
|
||||||
|
printf("Invalid server_port: %d\n", config.server_port);
|
||||||
|
config_dtor(&config);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
secret = config.secret;
|
||||||
|
|
||||||
|
char server_port_str[6];
|
||||||
|
snprintf(server_port_str, sizeof(server_port_str), "%d", config.server_port);
|
||||||
|
|
||||||
|
const char* options[] =
|
||||||
|
{"document_root", "./www", "listening_ports", server_port_str, "error_log_file", "logs/civetweb.log", NULL};
|
||||||
mg_callbacks callbacks;
|
mg_callbacks callbacks;
|
||||||
mg_context* ctx;
|
mg_context* ctx;
|
||||||
|
|
||||||
memset(&callbacks, 0, sizeof(callbacks));
|
memset(&callbacks, 0, sizeof(callbacks));
|
||||||
ctx = mg_start(&callbacks, NULL, options);
|
ctx = mg_start(&callbacks, NULL, options);
|
||||||
|
|
||||||
|
mg_set_request_handler(ctx, "/api/auth/login", login_handler, NULL);
|
||||||
|
mg_set_request_handler(ctx, "/api/auth/register", register_handler, NULL);
|
||||||
|
mg_set_request_handler(ctx, "/api/auth/delete", delete_handler, NULL);
|
||||||
|
|
||||||
printf("Server started on port(s) %s\n", mg_get_option(ctx, "listening_ports"));
|
printf("Server started on port(s) %s\n", mg_get_option(ctx, "listening_ports"));
|
||||||
getchar();
|
getchar();
|
||||||
mg_stop(ctx);
|
mg_stop(ctx);
|
||||||
|
|
||||||
|
close_user_db();
|
||||||
|
// close_db(db);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ int login_handler(mg_connection* conn, void* cbdata)
|
|||||||
"HTTP/1.1 200 OK\r\n"
|
"HTTP/1.1 200 OK\r\n"
|
||||||
"Content-Type: application/json\r\n"
|
"Content-Type: application/json\r\n"
|
||||||
"Access-Control-Allow-Origin: *\r\n\r\n"
|
"Access-Control-Allow-Origin: *\r\n\r\n"
|
||||||
"{\"token\":\"%s\"}",
|
"{\"success\":\"login success\", \"token\":\"%s\"}",
|
||||||
token);
|
token);
|
||||||
free(token);
|
free(token);
|
||||||
}
|
}
|
||||||
|
4
ui/.browserslistrc
Normal file
4
ui/.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
6
ui/.editorconfig
Normal file
6
ui/.editorconfig
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
22
ui/.gitignore
vendored
Normal file
22
ui/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
81
ui/README.md
Normal file
81
ui/README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Vuetify (Default)
|
||||||
|
|
||||||
|
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||||
|
|
||||||
|
## ❗️ Important Links
|
||||||
|
|
||||||
|
- 📄 [Docs](https://vuetifyjs.com/)
|
||||||
|
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||||
|
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||||
|
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||||
|
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||||
|
|
||||||
|
## 💿 Install
|
||||||
|
|
||||||
|
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||||
|
|
||||||
|
| Package Manager | Command |
|
||||||
|
|---------------------------------------------------------------|----------------|
|
||||||
|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||||
|
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||||
|
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||||
|
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||||
|
|
||||||
|
After completing the installation, your environment is ready for Vuetify development.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||||
|
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||||
|
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||||
|
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||||
|
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||||
|
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||||
|
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||||
|
|
||||||
|
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||||
|
|
||||||
|
## 💡 Usage
|
||||||
|
|
||||||
|
This section covers how to start the development server and build your project for production.
|
||||||
|
|
||||||
|
### Starting the Development Server
|
||||||
|
|
||||||
|
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||||
|
|
||||||
|
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
To build your project for production, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||||
|
|
||||||
|
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||||
|
|
||||||
|
## 💪 Support Vuetify Development
|
||||||
|
|
||||||
|
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||||
|
|
||||||
|
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||||
|
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||||
|
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||||
|
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||||
|
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||||
|
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||||
|
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||||
|
|
||||||
|
## 📑 License
|
||||||
|
[MIT](http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-present Vuetify, LLC
|
21
ui/components.d.ts
vendored
Normal file
21
ui/components.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
Auth: typeof import('./src/components/Auth.vue')['default']
|
||||||
|
Dialog: typeof import('./src/components/Dialog.vue')['default']
|
||||||
|
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||||
|
HelloWorld2: typeof import('./src/components/HelloWorld2.vue')['default']
|
||||||
|
LoginForm: typeof import('./src/components/LoginForm.vue')['default']
|
||||||
|
RegisterForm: typeof import('./src/components/RegisterForm.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
Toy: typeof import('./src/components/Toy.vue')['default']
|
||||||
|
UserPanel: typeof import('./src/components/UserPanel.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
2
ui/env.d.ts
vendored
Normal file
2
ui/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="unplugin-vue-router/client" />
|
36
ui/eslint.config.js
Normal file
36
ui/eslint.config.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* .eslint.js
|
||||||
|
*
|
||||||
|
* ESLint configuration file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import vueTsEslintConfig from '@vue/eslint-config-typescript'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'app/files-to-ignore',
|
||||||
|
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||||
|
},
|
||||||
|
|
||||||
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
...vueTsEslintConfig(),
|
||||||
|
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-expressions': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowShortCircuit: true,
|
||||||
|
allowTernary: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
13
ui/index.html
Normal file
13
ui/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Welcome to Vuetify 3</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4222
ui/package-lock.json
generated
Normal file
4222
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
ui/package.json
Normal file
48
ui/package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"lint": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "7.4.47",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"core-js": "^3.37.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"pinia": "^2.3.0",
|
||||||
|
"roboto-fontface": "*",
|
||||||
|
"vue": "^3.4.31",
|
||||||
|
"vuetify": "^3.6.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@tsconfig/node22": "^22.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"@vue/eslint-config-typescript": "^14.1.3",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint": "^9.14.0",
|
||||||
|
"eslint-plugin-vue": "^9.30.0",
|
||||||
|
"npm-run-all2": "^7.0.1",
|
||||||
|
"sass": "1.77.8",
|
||||||
|
"sass-embedded": "^1.77.8",
|
||||||
|
"typescript": "~5.6.3",
|
||||||
|
"unplugin-fonts": "^1.1.1",
|
||||||
|
"unplugin-vue-components": "^0.27.2",
|
||||||
|
"unplugin-vue-router": "^0.10.0",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-vuetify": "^2.0.3",
|
||||||
|
"vue-router": "^4.4.0",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
BIN
ui/public/favicon.ico
Normal file
BIN
ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
44
ui/src/App.vue
Normal file
44
ui/src/App.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-layout class="rounded rounded-md">
|
||||||
|
<v-app-bar color="surface-variant" title="Math">
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-magnify</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-heart</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-app-bar>
|
||||||
|
<v-navigation-drawer>
|
||||||
|
<v-list nav>
|
||||||
|
<v-list-item to="/" prepend-icon="mdi-email" title="Inbox" value="inbox"></v-list-item>
|
||||||
|
<v-list-item to="/auth" prepend-icon="mdi-account-supervisor-circle" title="登录" value="supervisors"></v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-clock-start" :title="token" value="clockin"></v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
|
||||||
|
<router-view></router-view>
|
||||||
|
</v-main>
|
||||||
|
</v-layout>
|
||||||
|
<!-- <v-main>
|
||||||
|
<router-view />
|
||||||
|
</v-main> -->
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useAuthStore } from './store/auth';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const token = ref(authStore.token);
|
||||||
|
|
||||||
|
</script>
|
BIN
ui/src/assets/logo.png
Normal file
BIN
ui/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
6
ui/src/assets/logo.svg
Normal file
6
ui/src/assets/logo.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
|
||||||
|
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
|
||||||
|
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
|
||||||
|
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 526 B |
40
ui/src/components/Auth.vue
Normal file
40
ui/src/components/Auth.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<v-carousel v-model="loginMode" v-if="!authStore.isAuthenticated" height="100%" hide-delimiters show-arrows>
|
||||||
|
<v-carousel-item>
|
||||||
|
<v-sheet class="v-center" height="100%">
|
||||||
|
<LoginForm id="aaaaa"></LoginForm>
|
||||||
|
</v-sheet>
|
||||||
|
</v-carousel-item>
|
||||||
|
<v-carousel-item>
|
||||||
|
<v-sheet class="v-center" height="100%">
|
||||||
|
<RegisterForm v-model="loginMode"></RegisterForm>
|
||||||
|
</v-sheet>
|
||||||
|
</v-carousel-item>
|
||||||
|
</v-carousel>
|
||||||
|
<UserInfo v-else></UserInfo>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import LoginForm from './LoginForm.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||||
|
import RegisterForm from './RegisterForm.vue';
|
||||||
|
import UserInfo from './UserPanel.vue';
|
||||||
|
|
||||||
|
const loginMode = ref(0);
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const token = ref(authStore.token);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.v-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
/* 设置容器高度为视口高度 */
|
||||||
|
}
|
||||||
|
</style>
|
29
ui/src/components/Dialog.vue
Normal file
29
ui/src/components/Dialog.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog max-width="500">
|
||||||
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
<v-btn v-bind="activatorProps" color="surface-variant" text="Open Dialog" variant="flat"></v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:default="{ isActive }">
|
||||||
|
<v-card :title="title">
|
||||||
|
<v-card-text>
|
||||||
|
<slot></slot>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn text="关闭" @click="isActive.value = false"></v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const { title = '' } = defineProps<{
|
||||||
|
title?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
7
ui/src/components/HelloWorld.vue
Normal file
7
ui/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
点左边登录。
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
157
ui/src/components/HelloWorld2.vue
Normal file
157
ui/src/components/HelloWorld2.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-responsive
|
||||||
|
class="align-centerfill-height mx-auto"
|
||||||
|
max-width="900"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
class="mb-4"
|
||||||
|
height="150"
|
||||||
|
src="@/assets/logo.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||||
|
|
||||||
|
<h1 class="text-h2 font-weight-bold">Vuetify 2</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4"></div>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||||
|
prepend-icon="mdi-rocket-launch-outline"
|
||||||
|
rounded="lg"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<v-img position="top right" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #subtitle>
|
||||||
|
<div class="text-subtitle-1">
|
||||||
|
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-overlay
|
||||||
|
opacity=".12"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/"
|
||||||
|
prepend-icon="mdi-text-box-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Learn about all things Vuetify in our documentation."
|
||||||
|
target="_blank"
|
||||||
|
title="Documentation"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||||
|
prepend-icon="mdi-star-circle-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Explore available framework Features."
|
||||||
|
target="_blank"
|
||||||
|
title="Features"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/components/all"
|
||||||
|
prepend-icon="mdi-widgets-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Discover components in the API Explorer."
|
||||||
|
target="_blank"
|
||||||
|
title="Components"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://discord.vuetifyjs.com"
|
||||||
|
prepend-icon="mdi-account-group-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Connect with Vuetify developers."
|
||||||
|
target="_blank"
|
||||||
|
title="Community"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
157
ui/src/components/LoginForm.vue
Normal file
157
ui/src/components/LoginForm.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<!-- <template>
|
||||||
|
<v-sheet class="mx-auto text-center" min-width="50%">
|
||||||
|
<v-form fast-fail @submit.prevent="submit">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h2 font-weight-bold">登录</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-text-field v-model="userId" :rules="userIdRules" label="账号"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate"
|
||||||
|
label="密码"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-btn :loading="loading" width="50%" text="注册" type="submit"></v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-form>
|
||||||
|
</v-sheet>
|
||||||
|
</template> -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-form fast-fail @submit.prevent="submit">
|
||||||
|
<v-img class="mx-auto my-6" max-width="228"
|
||||||
|
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img>
|
||||||
|
|
||||||
|
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg">
|
||||||
|
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||||
|
label="账号"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
|
||||||
|
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate" label="密码"
|
||||||
|
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'" :type="visible ? 'text' : 'password'"
|
||||||
|
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
|
||||||
|
<v-card class="mb-12" color="surface-variant" variant="tonal">
|
||||||
|
<v-card-text class="text-medium-emphasis text-caption">
|
||||||
|
警告:连续3次登录失败后,您的帐户将被暂时锁定三个小时。如果您必须立即登录,您还可以点击“忘记登录密码?”下面重置登录密码。
|
||||||
|
<br />
|
||||||
|
这只是个占位符文本,不知道为什么没有这段话整个布局就会乱成一坨,实际上我并没有做这个冻结功能。
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-btn class="mb-8" color="blue" size="large" variant="tonal" type="submit" block>
|
||||||
|
登录
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<a class="text-red text-decoration-none" @click="dialog('没做呢。', '没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。')">
|
||||||
|
忘记密码<v-icon icon="mdi-chevron-right"></v-icon>
|
||||||
|
</a>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
<v-dialog v-model="dialogShow" width="auto">
|
||||||
|
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const storedToken = ref(authStore.token);
|
||||||
|
|
||||||
|
const dialogShow = ref(false);
|
||||||
|
const dialogTitle = ref('');
|
||||||
|
const dialogText = ref('');
|
||||||
|
const dialogClose = ref(() => { });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const userId = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const passwordValidate = ref<string>("");
|
||||||
|
|
||||||
|
|
||||||
|
type LoginResponse = { success?: string, token?: string, error?: string };
|
||||||
|
|
||||||
|
const dialog = (title: string, text: string) => {
|
||||||
|
dialogTitle.value = title;
|
||||||
|
dialogText.value = text;
|
||||||
|
dialogShow.value = true;
|
||||||
|
return new Promise(res => {
|
||||||
|
dialogClose.value = res as () => void;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (userId: string, password: string) => {
|
||||||
|
console.log('login', userId, password);
|
||||||
|
try {
|
||||||
|
const formData = new FormData;
|
||||||
|
formData.append("user_id", userId);
|
||||||
|
formData.append("password", password);
|
||||||
|
let res = await axios.post('/api/auth/login', formData);
|
||||||
|
console.log(res.data);
|
||||||
|
return res.data as LoginResponse;
|
||||||
|
} catch (e) {
|
||||||
|
let ex = e as AxiosError;
|
||||||
|
return ex.response?.data as LoginResponse;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (event: SubmitEvent) => {
|
||||||
|
const results: any = await event;
|
||||||
|
if (results.valid) {
|
||||||
|
loading.value = true
|
||||||
|
console.log('valid')
|
||||||
|
let res = await login(userId.value, password.value);
|
||||||
|
if (res?.error) {
|
||||||
|
passwordValidate.value = res.error;
|
||||||
|
await dialog('错误', `登录失败:${res.error}`);
|
||||||
|
} else {
|
||||||
|
await dialog('信息', `登录成功,你好 ${(jwtDecode(res.token as string) as any).user_id}。`);
|
||||||
|
authStore.setToken(res.token as string);
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
console.log(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIdRules: any = [(value: string) => {
|
||||||
|
passwordValidate.value = '';
|
||||||
|
if (value?.length > 0) return true;
|
||||||
|
return '账号不能为空';
|
||||||
|
}];
|
||||||
|
|
||||||
|
const passwordRules: any = [(value: string) => {
|
||||||
|
passwordValidate.value = '';
|
||||||
|
if (value?.length > 0) return true;
|
||||||
|
return '密码不能为空';
|
||||||
|
}];
|
||||||
|
|
||||||
|
</script>
|
125
ui/src/components/RegisterForm.vue
Normal file
125
ui/src/components/RegisterForm.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-form fast-fail @submit.prevent="submit">
|
||||||
|
<v-img class="mx-auto my-6" max-width="228"
|
||||||
|
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img>
|
||||||
|
|
||||||
|
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg">
|
||||||
|
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||||
|
label="账号"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
|
||||||
|
<v-text-field v-model="password" :rules="passwordRules" label="密码"
|
||||||
|
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'" :type="visible ? 'text' : 'password'"
|
||||||
|
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
|
||||||
|
<v-text-field :rules="passwordRules2" label="重复输入密码" :append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'"
|
||||||
|
:type="visible ? 'text' : 'password'" @click:append-inner="visible = !visible"
|
||||||
|
prepend-inner-icon="mdi-lock-outline"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
|
||||||
|
<v-card class="mb-12" color="surface-variant" variant="tonal">
|
||||||
|
<v-card-text class="text-medium-emphasis text-caption">
|
||||||
|
这只是个占位符文本,不知道为什么没有这段话整个布局就会乱成一坨,我还在研究怎么让它表现得正常一点,这个组件库的文档太抽象了,看不懂,唉。
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-btn :loading="loading" class="mb-8" color="blue" size="large" type="submit" variant="tonal" block>
|
||||||
|
注册
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
</v-card>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
<v-dialog v-model="dialogShow" width="auto">
|
||||||
|
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
const model = defineModel();
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const storedToken = ref(authStore.token);
|
||||||
|
|
||||||
|
const dialogShow = ref(false);
|
||||||
|
const dialogTitle = ref('');
|
||||||
|
const dialogText = ref('');
|
||||||
|
const dialogClose = ref(() => { });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const userId = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
|
||||||
|
type RegisterResponse = { success?: string, error?: string };
|
||||||
|
|
||||||
|
const dialog = (title: string, text: string) => {
|
||||||
|
dialogTitle.value = title;
|
||||||
|
dialogText.value = text;
|
||||||
|
dialogShow.value = true;
|
||||||
|
return new Promise(res => {
|
||||||
|
dialogClose.value = res as () => void;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (userId: string, password: string) => {
|
||||||
|
console.log('login', userId, password);
|
||||||
|
try {
|
||||||
|
const formData = new FormData;
|
||||||
|
formData.append("user_id", userId);
|
||||||
|
formData.append("password", password);
|
||||||
|
let res = await axios.post('/api/auth/register', formData);
|
||||||
|
console.log(res.data);
|
||||||
|
return res.data as RegisterResponse;
|
||||||
|
} catch (e) {
|
||||||
|
let ex = e as AxiosError;
|
||||||
|
return ex.response?.data as RegisterResponse;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (event: SubmitEvent) => {
|
||||||
|
const results: any = await event;
|
||||||
|
if (results.valid) {
|
||||||
|
loading.value = true
|
||||||
|
console.log('valid')
|
||||||
|
let res = await register(userId.value, password.value);
|
||||||
|
if (res?.error) {
|
||||||
|
dialog('错误', `注册失败:${res.error}`);
|
||||||
|
} else {
|
||||||
|
await dialog('提示', '注册成功,请前往登录。');
|
||||||
|
model.value = 0;
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
console.log(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIdRules: any = [(value: string) => {
|
||||||
|
if (value?.length > 0) return true;
|
||||||
|
return '账号不能为空';
|
||||||
|
}];
|
||||||
|
|
||||||
|
const passwordRules: any = [(value: string) => {
|
||||||
|
if (value?.length > 0) return true;
|
||||||
|
return '密码不能为空';
|
||||||
|
}];
|
||||||
|
|
||||||
|
const passwordRules2: any = [(value: string) => {
|
||||||
|
if (value == password.value) return true;
|
||||||
|
return '两次输入需保持一致';
|
||||||
|
}];
|
||||||
|
|
||||||
|
</script>
|
80
ui/src/components/Toy.vue
Normal file
80
ui/src/components/Toy.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<v-row align="center" justify="center" dense>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card append-icon="mdi-check" class="mx-auto" prepend-icon="mdi-account" subtitle="占位符。占位符。" title="占位符">
|
||||||
|
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary" icon="mdi-account"></v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon color="success" icon="mdi-check"></v-icon>
|
||||||
|
</template>
|
||||||
|
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card append-avatar="https://cdn.vuetifyjs.com/images/john.jpg" class="mx-auto"
|
||||||
|
prepend-avatar="https://cdn.vuetifyjs.com/images/logos/v-alt.svg" subtitle="占位符。占位符。" title="占位符">
|
||||||
|
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar color="blue-darken-2">
|
||||||
|
<v-icon icon="mdi-alarm"></v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-avatar size="24">
|
||||||
|
<v-img alt="John" src="https://file.aiquickdraw.com/inpaint/1735136797_9819bf1a-7f1d-4a32-be6a-f5f7f3977b71.png"></v-img>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const labels = ref([
|
||||||
|
'12am',
|
||||||
|
'3am',
|
||||||
|
'6am',
|
||||||
|
'9am',
|
||||||
|
'12pm',
|
||||||
|
'3pm',
|
||||||
|
'6pm',
|
||||||
|
'9pm',
|
||||||
|
])
|
||||||
|
|
||||||
|
const value = ref([
|
||||||
|
200,
|
||||||
|
675,
|
||||||
|
410,
|
||||||
|
390,
|
||||||
|
310,
|
||||||
|
460,
|
||||||
|
250,
|
||||||
|
240,
|
||||||
|
]);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-sheet--offset {
|
||||||
|
top: -24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
46
ui/src/components/UserPanel.vue
Normal file
46
ui/src/components/UserPanel.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<v-card subtitle="占位符占位符占位符" title="用户信息面板" max-width="80%">
|
||||||
|
<v-card-item>
|
||||||
|
用户 ID: {{ userId }}
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-item>
|
||||||
|
<v-chip @click="authStore.clearToken">
|
||||||
|
退出登录
|
||||||
|
</v-chip>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-item>
|
||||||
|
<v-expansion-panels>
|
||||||
|
<v-expansion-panel title="Token (放出来玩玩,之后会换个机制)" :text="token">
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-item>
|
||||||
|
<Toy></Toy>
|
||||||
|
</v-card-item>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||||
|
import Toy from './Toy.vue';
|
||||||
|
|
||||||
|
const reveal = ref(false);
|
||||||
|
|
||||||
|
interface KqmJwt extends JwtPayload {
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const token = ref(authStore.token);
|
||||||
|
|
||||||
|
const userId = computed(() => {
|
||||||
|
if (authStore.isAuthenticated) {
|
||||||
|
let data: KqmJwt = jwtDecode(token.value);
|
||||||
|
return data.user_id;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
25
ui/src/main.ts
Normal file
25
ui/src/main.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* main.ts
|
||||||
|
*
|
||||||
|
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import { registerPlugins } from '@/plugins'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
app.use(pinia);
|
||||||
|
|
||||||
|
registerPlugins(app)
|
||||||
|
|
||||||
|
app.mount('#app')
|
18
ui/src/plugins/index.ts
Normal file
18
ui/src/plugins/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* plugins/index.ts
|
||||||
|
*
|
||||||
|
* Automatically included in `./src/main.ts`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import vuetify from './vuetify'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { App } from 'vue'
|
||||||
|
|
||||||
|
export function registerPlugins (app: App) {
|
||||||
|
app
|
||||||
|
.use(vuetify)
|
||||||
|
.use(router)
|
||||||
|
}
|
19
ui/src/plugins/vuetify.ts
Normal file
19
ui/src/plugins/vuetify.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* plugins/vuetify.ts
|
||||||
|
*
|
||||||
|
* Framework documentation: https://vuetifyjs.com`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
|
export default createVuetify({
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light',
|
||||||
|
},
|
||||||
|
})
|
66
ui/src/router/index.ts
Normal file
66
ui/src/router/index.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* router/index.ts
|
||||||
|
*
|
||||||
|
* Automatic routes for `./src/pages/*.vue`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import Auth from '@/components/Auth.vue'
|
||||||
|
import HelloWorld from '@/components/HelloWorld.vue'
|
||||||
|
import HelloWorld2 from '@/components/HelloWorld2.vue'
|
||||||
|
import Login from '@/components/RegisterForm.vue'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HelloWorld
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth',
|
||||||
|
name: 'home2',
|
||||||
|
component: Auth
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// const routes = [
|
||||||
|
// {
|
||||||
|
// path: '/',
|
||||||
|
// name: 'home',
|
||||||
|
// componen
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// const router = createRouter({
|
||||||
|
// history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
// routes,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Workaround for https://github.com/vitejs/vite/issues/11804
|
||||||
|
router.onError((err, to) => {
|
||||||
|
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
||||||
|
if (!localStorage.getItem('vuetify:dynamic-reload')) {
|
||||||
|
console.log('Reloading page to fix dynamic import error')
|
||||||
|
localStorage.setItem('vuetify:dynamic-reload', 'true')
|
||||||
|
location.assign(to.fullPath)
|
||||||
|
} else {
|
||||||
|
console.error('Dynamic import error, reloading page did not fix it', err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.isReady().then(() => {
|
||||||
|
localStorage.removeItem('vuetify:dynamic-reload')
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
20
ui/src/store/auth.ts
Normal file
20
ui/src/store/auth.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore("auth", {
|
||||||
|
state: () => ({
|
||||||
|
token: localStorage.getItem('token') || '',
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setToken(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
},
|
||||||
|
clearToken() {
|
||||||
|
this.token = '';
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state) => !!state.token,
|
||||||
|
},
|
||||||
|
});
|
10
ui/src/styles/settings.scss
Normal file
10
ui/src/styles/settings.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* src/styles/settings.scss
|
||||||
|
*
|
||||||
|
* Configures SASS variables and Vuetify overwrites
|
||||||
|
*/
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/features/sass-variables/`
|
||||||
|
// @use 'vuetify/settings' with (
|
||||||
|
// $color-pack: false
|
||||||
|
// );
|
14
ui/tsconfig.app.json
Normal file
14
ui/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
ui/tsconfig.json
Normal file
14
ui/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "es2020",
|
||||||
|
}
|
||||||
|
}
|
19
ui/tsconfig.node.json
Normal file
19
ui/tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
22
ui/typed-router.d.ts
vendored
Normal file
22
ui/typed-router.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||||
|
// It's recommended to commit this file.
|
||||||
|
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||||
|
|
||||||
|
declare module 'vue-router/auto-routes' {
|
||||||
|
import type {
|
||||||
|
RouteRecordInfo,
|
||||||
|
ParamValue,
|
||||||
|
ParamValueOneOrMore,
|
||||||
|
ParamValueZeroOrMore,
|
||||||
|
ParamValueZeroOrOne,
|
||||||
|
} from 'vue-router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route name map generated by unplugin-vue-router
|
||||||
|
*/
|
||||||
|
export interface RouteNamedMap {
|
||||||
|
}
|
||||||
|
}
|
61
ui/vite.config.mts
Normal file
61
ui/vite.config.mts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Plugins
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||||
|
import ViteFonts from 'unplugin-fonts/vite'
|
||||||
|
import VueRouter from 'unplugin-vue-router/vite'
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
VueRouter(),
|
||||||
|
Vue({
|
||||||
|
template: { transformAssetUrls },
|
||||||
|
}),
|
||||||
|
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||||
|
Vuetify({
|
||||||
|
autoImport: true,
|
||||||
|
styles: {
|
||||||
|
configFile: 'src/styles/settings.scss',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Components(),
|
||||||
|
ViteFonts({
|
||||||
|
google: {
|
||||||
|
families: [ {
|
||||||
|
name: 'Roboto',
|
||||||
|
styles: 'wght@100;300;400;500;700;900',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
define: { 'process.env': {} },
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
'.js',
|
||||||
|
'.json',
|
||||||
|
'.jsx',
|
||||||
|
'.mjs',
|
||||||
|
'.ts',
|
||||||
|
'.tsx',
|
||||||
|
'.vue',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
sass: {
|
||||||
|
api: 'modern-compiler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
75
xmake.lua
75
xmake.lua
@ -1,27 +1,70 @@
|
|||||||
add_rules("mode.debug", "mode.release")
|
add_rules("mode.debug", "mode.release")
|
||||||
add_rules("plugin.compile_commands.autoupdate")
|
add_rules("plugin.compile_commands.autoupdate")
|
||||||
|
|
||||||
set_languages("c23")
|
|
||||||
|
|
||||||
set_warnings("all")
|
set_warnings("all")
|
||||||
set_warnings("error")
|
set_warnings("error")
|
||||||
|
|
||||||
add_requires("civetweb")
|
add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp")
|
||||||
|
|
||||||
target("math")
|
local npm = "npm"
|
||||||
set_kind("binary")
|
if is_host("windows") then
|
||||||
add_packages("civetweb")
|
npm = "npm.cmd"
|
||||||
add_includedirs("include")
|
end
|
||||||
add_files("src/main.c")
|
|
||||||
|
target("jwt")
|
||||||
|
set_languages("c++23")
|
||||||
|
set_kind("static")
|
||||||
|
add_packages("jwt-cpp")
|
||||||
|
add_includedirs("include/jwt")
|
||||||
|
add_files("src/jwt/**.cpp")
|
||||||
|
|
||||||
|
target("hash")
|
||||||
|
set_languages("c++23")
|
||||||
|
set_kind("static")
|
||||||
|
add_packages("cryptopp")
|
||||||
|
add_includedirs("include/hash")
|
||||||
|
add_files("src/hash/**.cpp")
|
||||||
|
|
||||||
|
target("db")
|
||||||
|
set_languages("c++23")
|
||||||
|
set_kind("static")
|
||||||
|
add_packages("leveldb")
|
||||||
|
add_deps("hash")
|
||||||
|
add_includedirs("include/db", "include")
|
||||||
|
add_files("src/db/**.cpp")
|
||||||
|
|
||||||
after_build(function (target)
|
after_build(function (target)
|
||||||
local npm = "npm"
|
local target_dir = target:targetdir()
|
||||||
if is_host("windows") then
|
os.cp("configs", target_dir)
|
||||||
npm = "npm.cmd"
|
if not os.exists(target_dir .. "/db") then
|
||||||
|
os.mkdir(target_dir .. "/db")
|
||||||
end
|
end
|
||||||
os.execv(npm, {"--prefix", "ui", "run", "build"})
|
end)
|
||||||
|
|
||||||
local vue_dist_dir = "ui/dist"
|
target("server")
|
||||||
local target_dir = target:targetdir() .. "/www"
|
set_languages("c23")
|
||||||
os.cp(vue_dist_dir, target_dir)
|
set_kind("binary")
|
||||||
|
add_packages("civetweb", "cjson")
|
||||||
|
add_deps("jwt", "db")
|
||||||
|
add_includedirs("include")
|
||||||
|
add_files("src/server/**.c")
|
||||||
|
add_files("src/main.c")
|
||||||
|
|
||||||
|
target("ui")
|
||||||
|
on_build(function (target)
|
||||||
|
os.execv(npm, {"--prefix", "ui", "run", "build"})
|
||||||
|
local target_dir = target:targetdir()
|
||||||
|
os.cp("ui/dist", target_dir)
|
||||||
|
os.mv(target_dir .. "/dist", target_dir .. "/www")
|
||||||
|
end)
|
||||||
|
on_run(function (target)
|
||||||
|
os.execv(npm, {"--prefix", "ui", "run", "dev"})
|
||||||
|
end)
|
||||||
|
|
||||||
|
target("math")
|
||||||
|
add_deps("ui")
|
||||||
|
add_deps("server")
|
||||||
|
on_build(function (target) end)
|
||||||
|
on_run(function (target)
|
||||||
|
os.exec("xmake run server")
|
||||||
end)
|
end)
|
||||||
|
Loading…
Reference in New Issue
Block a user