├── DockerBuild.md ├── Dockerfile ├── LICENSE ├── README.md ├── admin ├── db │ ├── password │ │ └── PasswordNext.sqlite │ └── user │ │ ├── user.md │ │ ├── user.sqlite │ │ └── user_backup.sqlite ├── index.js ├── package.json ├── routers │ ├── main.js │ └── password │ │ ├── addPassword.js │ │ ├── deletePassword.js │ │ ├── editPassword.js │ │ ├── get2faSecret.js │ │ ├── get2faToken.js │ │ ├── getPassword.js │ │ ├── getRandomPassword.js │ │ ├── login.js │ │ ├── main.js │ │ ├── public │ │ └── checkToken.js │ │ ├── register.js │ │ └── router.js └── utils │ ├── database │ ├── changeSqliteTableColumn.js │ ├── changeSqliteTableData.js │ ├── createSqlite.js │ ├── createSqliteTable.js │ ├── deleteSqliteTableColumn.js │ ├── deleteSqliteTableData.js │ ├── getSqlite.js │ ├── getSqliteAllTable.js │ ├── insertSqliteTableColumn.js │ ├── insertSqliteTableData.js │ ├── searchSqliteTableColumn.js │ └── searchSqliteTableData.js │ ├── get2faSecret.js │ ├── nlog.js │ ├── randomInt.js │ ├── randomPassword.js │ └── randomString.js ├── example ├── .DS_Store ├── 2fa.png ├── add.png ├── edit.png ├── handle.png ├── list.png ├── login.png └── register.png └── web ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── public ├── images │ ├── cancelSetting.png │ └── setting.png ├── next.svg └── vercel.svg ├── src ├── apis │ ├── addPassword.ts │ ├── deletePassword.ts │ ├── editNewPassword.ts │ ├── get2faToken.ts │ ├── getPassword.ts │ ├── login.ts │ └── register.ts ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ └── page.tsx ├── components │ └── loading │ │ ├── page.module.css │ │ └── page.tsx ├── config │ └── config.ts └── utils │ └── request.ts └── tsconfig.json /DockerBuild.md: -------------------------------------------------------------------------------- 1 | ## 指定打包架构 2 | 3 | `docker buildx build --platform linux/amd64 -t password_next .` 4 | 5 | `docker buildx build --platform linux/arm64 -t password_next .` 6 | 7 | `docker buildx build --platform linux/arm64,linux/amd64 -t password_next .` 8 | 9 | ## 镜像标签 10 | 11 | `docker tag password_next:latest nocn/password_next:latest` 12 | 13 | `docker tag password_next:latest nocn/password_next:AMD64` 14 | 15 | `docker tag password_next:latest nocn/password_next:ARM64` 16 | 17 | ## 推送镜像到仓库 18 | 19 | `docker push nocn/password_next:latest` 20 | 21 | `docker push nocn/password_next:AMD64` 22 | 23 | `docker push nocn/password_next:ARM64` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方的Node.js镜像作为基础镜像 2 | FROM node:18.17.0 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制前端代码并构建 8 | COPY web /app/web 9 | WORKDIR /app/web 10 | RUN npm install 11 | 12 | # 复制后端代码并构建 13 | COPY admin /app/admin 14 | WORKDIR /app/admin 15 | RUN npm install 16 | 17 | # 重新构建 sqlite3 模块 18 | RUN npm rebuild sqlite3 19 | 20 | # 暴露端口 21 | EXPOSE 3000 22 | EXPOSE 19899 23 | 24 | # 启动应用 25 | CMD ["sh", "-c", "cd /app/web && npm run build && npm start & cd /app/admin && node index.js"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HChaoHui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Password-Next 2 | 3 | Password-Next is a minimalist password manager designed to securely store your passwords with ease. 4 | 5 | ### Technologies Used 6 | 7 | - **Frontend**: Built using NextJS. 8 | - **Backend**: Utilizes Koa for API development and Sqlite for data storage. 9 | 10 | ### Getting Started 11 | 12 | #### Local Setup 13 | 14 | 1. **Clone the repository:** 15 | ```bash 16 | git clone https://github.com/HChaoHui/Password-Next.git 17 | ``` 18 | 2. **Navigate to the web directory:** 19 | ```bash 20 | cd Password-Next/web 21 | ``` 22 | 3. **Install dependencies:** 23 | ```bash 24 | npm install 25 | ``` 26 | 4. **Run the development server:** 27 | ```bash 28 | npm run dev 29 | ``` 30 | 5. **Open your browser and visit:** 31 | ``` 32 | http://localhost:3000 33 | ``` 34 | 6. **Navigate to the admin directory:** 35 | ```bash 36 | cd ../admin 37 | ``` 38 | 7. **Install dependencies:** 39 | ```bash 40 | npm install 41 | ``` 42 | 8. **Start the backend server:** 43 | ```bash 44 | npm run start 45 | ``` 46 | 9. **API will be available at:** 47 | ``` 48 | http://localhost:19899 49 | ``` 50 | 10. **Default Token:** 51 | ``` 52 | PasswordNext 53 | ``` 54 | 55 | #### Docker Deployment 56 | 57 | 1. **Docker Address:** 58 | [nocn/password_next](https://hub.docker.com/r/nocn/password_next) 59 | 2. **Run the Docker container:** 60 | ```bash 61 | docker run -p 3000:3000 -p 19899:19899 -e NEXT_PUBLIC_API_URL=http://127.0.0.1:19899 nocn/password_next 62 | ``` 63 | replace "http://127.0.0.1:19899" with your API address 64 | Map the data persistence to the 'db' directory within the admin folder. 65 | 66 | ### Demo 67 | 68 | Check out the live demo [here](https://password.next.688828.xyz/). 69 | The demo site may periodically delete data, so please do not store important information there. 70 | 71 | ### Example Screenshots 72 | 73 | ![Login](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/login.png) 74 | ![Register](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/register.png) 75 | ![List](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/list.png) 76 | ![Add](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/add.png) 77 | ![Handle](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/handle.png) 78 | ![Edit](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/edit.png) 79 | ![2FA](https://raw.githubusercontent.com/HChaoHui/Password-Next/main/example/2fa.png) 80 | 81 | ### Star History 82 | 83 | [![Star History Chart](https://api.star-history.com/svg?repos=HChaoHui/Password-Next&type=Date)](https://star-history.com/#HChaoHui/Password-Next&Date) 84 | 85 | ### Contributing 86 | 87 | Feel free to contribute to the project by opening issues or submitting pull requests. Your feedback and contributions are highly appreciated! 88 | 89 | ### License 90 | 91 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /admin/db/password/PasswordNext.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/admin/db/password/PasswordNext.sqlite -------------------------------------------------------------------------------- /admin/db/user/user.md: -------------------------------------------------------------------------------- 1 | ## userToken 2 | 3 | id: "INTEGER PRIMARY KEY" 4 | token: "TEXT" 5 | user_id: "INTEGER", 6 | user: "TEXT" -------------------------------------------------------------------------------- /admin/db/user/user.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/admin/db/user/user.sqlite -------------------------------------------------------------------------------- /admin/db/user/user_backup.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/admin/db/user/user_backup.sqlite -------------------------------------------------------------------------------- /admin/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const cors = require('koa2-cors'); 3 | const bodyParser = require('koa-bodyparser'); 4 | const nlogController = require('./utils/nlog'); 5 | const app = new Koa(); 6 | 7 | require('dotenv').config(); 8 | 9 | let corsConfig = { 10 | origin: "*", 11 | allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 12 | allowHeaders: ['Content-Type', 'Authorization', 'Accept'], 13 | exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], 14 | maxAge: 5, 15 | credentials: true, 16 | } 17 | 18 | app.use(cors(corsConfig)) 19 | app.use(bodyParser()); 20 | app.use(nlogController()); 21 | 22 | const routers = require('./routers/main'); 23 | 24 | routers.forEach((router) => { 25 | app.use(router.routes()).use(router.allowedMethods()); 26 | }); 27 | 28 | console.log('Server Is Start, listening On Port 19899'); 29 | app.listen(19899); -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node ./index.js" 4 | }, 5 | "dependencies": { 6 | "dotenv": "^16.4.5", 7 | "koa": "^2.15.3", 8 | "koa-bodyparser": "^4.4.1", 9 | "koa-router": "^12.0.1", 10 | "koa2-cors": "^2.0.6", 11 | "node-fetch": "^3.3.2", 12 | "nodemailer": "^6.9.14", 13 | "qrcode": "^1.5.3", 14 | "speakeasy": "^2.0.0", 15 | "sqlite3": "^5.1.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /admin/routers/main.js: -------------------------------------------------------------------------------- 1 | const password_router = require('./password/main'); 2 | 3 | const routers = [ 4 | ...password_router 5 | ] 6 | 7 | module.exports = routers; -------------------------------------------------------------------------------- /admin/routers/password/addPassword.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const checkToken = require('./public/checkToken'); 3 | const getSqlite = require('../../utils/database/getSqlite'); 4 | const createSqlite = require('../../utils/database/createSqlite'); 5 | const createSqliteTable = require('../../utils/database/createSqliteTable'); 6 | const insertSqliteTableData = require('../../utils/database/insertSqliteTableData'); 7 | 8 | const checkBody = async (ctx, next) => { 9 | const code400 = { 10 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 11 | }; 12 | const { account, password, appName, webSite, twofa } = ctx.request.body; 13 | 14 | if (!account || !password || !appName || !webSite) { 15 | ctx.status = 400; 16 | ctx.body = code400; 17 | return; 18 | } 19 | 20 | const createSqliteInfo = await createSqlite(ctx.state.userData.user, 'password'); 21 | 22 | ctx.state.nlog(createSqliteInfo); 23 | 24 | const db = getSqlite(ctx.state.userData.user, 'password'); 25 | 26 | const createTableInfo = await createSqliteTable(db, 'password', { 27 | id: 'INTEGER PRIMARY KEY AUTOINCREMENT', 28 | account: 'TEXT', 29 | password: 'TEXT', 30 | appName: 'TEXT', 31 | webSite: 'TEXT', 32 | createTime: 'TEXT', 33 | updateTime: 'TEXT', 34 | twofa: "TEXT" 35 | }); 36 | 37 | ctx.state.nlog(createTableInfo); 38 | 39 | const insertDataInfo = await insertSqliteTableData(db, 'password', { 40 | account: account, 41 | password: password, 42 | appName: appName, 43 | webSite: webSite, 44 | createTime: new Date().toISOString(), 45 | updateTime: new Date().toISOString(), 46 | twofa: twofa ? twofa : '' 47 | }); 48 | 49 | ctx.state.nlog(insertDataInfo); 50 | 51 | db.close(); 52 | 53 | ctx.state.responseData = { 54 | status: 200, 55 | message: 'success', 56 | } 57 | 58 | await next(); 59 | }; 60 | const addPassword = async (ctx) => { 61 | ctx.body = ctx.state.responseData 62 | }; 63 | 64 | router.post('/addPassword', checkToken, checkBody, addPassword); 65 | 66 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/deletePassword.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const checkToken = require('./public/checkToken'); 3 | const getSqlite = require('../../utils/database/getSqlite'); 4 | const deleteSqliteTableData = require('../../utils/database/deleteSqliteTableData'); 5 | 6 | const checkBody = async (ctx, next) => { 7 | const code400 = { 8 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 9 | }; 10 | const { id } = ctx.request.body; 11 | 12 | if (!id) { 13 | ctx.status = 400; 14 | ctx.body = code400; 15 | return; 16 | } 17 | 18 | const db = getSqlite(ctx.state.userData.user, 'password'); 19 | 20 | const deleteDataInfo = await deleteSqliteTableData(db,'password',{id}) 21 | 22 | ctx.state.nlog(deleteDataInfo); 23 | 24 | db.close(); 25 | 26 | ctx.state.responseData = { 27 | status: 200, 28 | message: 'success', 29 | } 30 | 31 | await next(); 32 | }; 33 | const deletePassword = async (ctx) => { 34 | ctx.body = ctx.state.responseData 35 | }; 36 | 37 | router.post('/deletePassword', checkToken, checkBody, deletePassword); 38 | 39 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/editPassword.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const checkToken = require('./public/checkToken'); 3 | const getSqlite = require('../../utils/database/getSqlite'); 4 | const changeSqliteTableData = require('../../utils/database/changeSqliteTableData'); 5 | 6 | const checkBody = async (ctx, next) => { 7 | const code400 = { 8 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 9 | }; 10 | const { id, account, password, appName, webSite, twofa } = ctx.request.body; 11 | 12 | if (!id || !account || !password || !appName || !webSite) { 13 | ctx.status = 400; 14 | ctx.body = code400; 15 | return; 16 | } 17 | 18 | const db = getSqlite(ctx.state.userData.user, 'password'); 19 | 20 | const updateData = { 21 | account: account, 22 | password: password, 23 | appName: appName, 24 | webSite: webSite, 25 | updateTime: new Date().toISOString(), 26 | twofa: twofa ? twofa : '' 27 | } 28 | 29 | const conditions = { 30 | id: id 31 | } 32 | 33 | const updateDataInfo = await changeSqliteTableData(db, 'password', updateData, conditions); 34 | 35 | ctx.state.nlog(updateDataInfo); 36 | 37 | db.close(); 38 | 39 | ctx.state.responseData = { 40 | status: 200, 41 | message: 'success', 42 | } 43 | 44 | await next(); 45 | }; 46 | const editPassword = async (ctx) => { 47 | ctx.body = ctx.state.responseData 48 | }; 49 | 50 | router.post('/editPassword', checkToken, checkBody, editPassword); 51 | 52 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/get2faSecret.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const getSecretQRCode = require('../../utils/get2faSecret'); 3 | 4 | const get2faSecret = async (ctx) => { 5 | 6 | 7 | const data = await getSecretQRCode() 8 | 9 | ctx.body = { 10 | data, 11 | status: 200, 12 | message: 'success', 13 | } 14 | 15 | }; 16 | 17 | router.get('/get2faSecret', get2faSecret); 18 | 19 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/get2faToken.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const speakeasy = require('speakeasy'); 3 | const checkToken = require('./public/checkToken'); 4 | 5 | const checkBody = async (ctx, next) => { 6 | 7 | const code400 = { 8 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 9 | }; 10 | 11 | const { secret } = ctx.request.body; 12 | 13 | if (!secret) { 14 | ctx.status = 400; 15 | ctx.body = code400; 16 | return; 17 | } 18 | 19 | await next(); 20 | }; 21 | 22 | const get2faToken = async (ctx) => { 23 | 24 | const { secret } = ctx.request.body; 25 | 26 | const token = speakeasy.totp({ 27 | secret: secret, 28 | encoding: 'base32' 29 | }); 30 | 31 | // 计算剩余时间 32 | const timeStep = 30; // 30秒为一个周期 33 | const currentTime = Math.floor(Date.now() / 1000); 34 | const remainingTime = timeStep - (currentTime % timeStep); 35 | 36 | ctx.body = { 37 | data: { 38 | token: token, 39 | remainingTime: remainingTime, 40 | }, 41 | status: 200, 42 | message: 'success', 43 | } 44 | 45 | }; 46 | 47 | router.post('/get2faToken', checkToken, checkBody, get2faToken); 48 | 49 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/getPassword.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const checkToken = require('./public/checkToken'); 3 | const getSqlite = require('../../utils/database/getSqlite'); 4 | const searchSqliteTableData = require('../../utils/database/searchSqliteTableData'); 5 | 6 | const getPassword = async (ctx) => { 7 | 8 | try { 9 | const db = getSqlite(ctx.state.userData.user, 'password') 10 | const result = await searchSqliteTableData(db, 'password', {}); 11 | db.close(); 12 | ctx.body = { 13 | data: result, 14 | status: 200, 15 | message: 'success', 16 | } 17 | } catch (error) { 18 | ctx.body = { 19 | data: [], 20 | status: 200, 21 | message: 'success', 22 | } 23 | } 24 | 25 | }; 26 | 27 | router.get('/getPassword', checkToken, getPassword); 28 | 29 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/getRandomPassword.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const randomPassword = require('../../utils/randomPassword'); 3 | 4 | const getRandomPassword = async (ctx) => { 5 | const password = randomPassword(20); 6 | ctx.state.nlog('PasswordGenerateSuccess: ' + password); 7 | ctx.body = { 8 | data: randomPassword(20), 9 | status: 200, 10 | message: 'success', 11 | } 12 | }; 13 | 14 | router.get('/getRandomPassword', getRandomPassword); 15 | 16 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/login.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const searchSqliteTableData = require('../../utils/database/searchSqliteTableData'); 3 | const getSqlite = require('../../utils/database/getSqlite'); 4 | const createSqlite = require('../../utils/database/createSqlite'); 5 | const createSqliteTable = require('../../utils/database/createSqliteTable'); 6 | const insertSqliteTableData = require('../../utils/database/insertSqliteTableData'); 7 | 8 | const isNonEmptyStringWithoutSpaces = (param) => { 9 | return typeof param === 'string' && param.replace(/\s+/g, '').length > 0; 10 | } 11 | 12 | const checkBody = async (ctx, next) => { 13 | const code400 = { 14 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 15 | }; 16 | const { token } = ctx.request.body; 17 | 18 | 19 | 20 | if (!isNonEmptyStringWithoutSpaces(token)) { 21 | ctx.status = 400; 22 | ctx.body = code400; 23 | return; 24 | } 25 | 26 | ctx.state.token = token; 27 | 28 | await next(); 29 | }; 30 | 31 | const authToken = async (ctx, next) => { 32 | 33 | 34 | const code401 = { 35 | msg: 'Access denied. Please verify your identity to proceed.' 36 | }; 37 | 38 | const db = getSqlite('user', 'user') 39 | 40 | const result = await searchSqliteTableData(db, 'userToken', { token: ctx.state.token }); 41 | 42 | if (result.length <= 0) { 43 | ctx.status = 400; 44 | ctx.body = code401; 45 | return; 46 | } 47 | 48 | ctx.state.responseData = result[0]; 49 | 50 | await next(); 51 | 52 | }; 53 | 54 | const login = async (ctx) => { 55 | ctx.body = { 56 | data: ctx.state.responseData, 57 | status: 200, 58 | message: 'success', 59 | } 60 | }; 61 | 62 | router.post('/login', checkBody, authToken, login); 63 | 64 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/main.js: -------------------------------------------------------------------------------- 1 | const addpassword_router = require('./addPassword'); 2 | const getrandompassword_router = require('./getRandomPassword'); 3 | const login_router = require('./login'); 4 | const get2fasecret_router = require('./get2faSecret'); 5 | const get2fatoken_router = require('./get2faToken'); 6 | const getpassword_router = require('./getPassword'); 7 | const deletepassword_router = require('./deletePassword'); 8 | const editpassword_router = require('./editPassword'); 9 | const register_router = require('./register'); 10 | 11 | const routers = [ 12 | addpassword_router, 13 | getrandompassword_router, 14 | login_router, 15 | get2fasecret_router, 16 | get2fatoken_router, 17 | getpassword_router, 18 | deletepassword_router, 19 | editpassword_router, 20 | register_router 21 | ]; 22 | 23 | module.exports = routers; -------------------------------------------------------------------------------- /admin/routers/password/public/checkToken.js: -------------------------------------------------------------------------------- 1 | const getSqlite = require('../../../utils/database/getSqlite'); 2 | const searchSqliteTableData = require('../../../utils/database/searchSqliteTableData'); 3 | 4 | const checkToken = async (ctx, next) => { 5 | 6 | const code401 = { 7 | msg: 'Access denied. Please verify your identity to proceed.' 8 | }; 9 | const authHeader = ctx.headers['authorization']; 10 | 11 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 12 | ctx.status = 401; 13 | ctx.body = code401; 14 | return; 15 | } 16 | 17 | ctx.state.token = authHeader.substring(7); 18 | 19 | const db = getSqlite('user', 'user') 20 | 21 | const result = await searchSqliteTableData(db, 'userToken', { token: ctx.state.token }); 22 | 23 | db.close(); 24 | 25 | if (result.length <= 0) { 26 | ctx.status = 401; 27 | ctx.body = code401; 28 | return; 29 | } 30 | 31 | ctx.state.userData = result[0]; 32 | 33 | ctx.state.nlog('LoginUser: ' + ctx.state.userData.user); 34 | 35 | await next(); 36 | }; 37 | 38 | module.exports = checkToken; -------------------------------------------------------------------------------- /admin/routers/password/register.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | const searchSqliteTableData = require('../../utils/database/searchSqliteTableData'); 3 | const insertSqliteTableData = require('../../utils/database/insertSqliteTableData'); 4 | const getSqlite = require('../../utils/database/getSqlite'); 5 | const getRandomPassword = require('../../utils/randomPassword'); 6 | 7 | const isNonEmptyStringWithoutSpaces = (param) => { 8 | return typeof param === 'string' && param.replace(/\s+/g, '').length > 0; 9 | } 10 | 11 | const checkBody = async (ctx, next) => { 12 | const code400 = { 13 | msg: 'The request was unsuccessful. Please ensure that all data is correct and error-free.' 14 | }; 15 | const { user } = ctx.request.body; 16 | 17 | if (!isNonEmptyStringWithoutSpaces(user)) { 18 | ctx.status = 400; 19 | ctx.body = code400; 20 | return; 21 | } 22 | 23 | ctx.state.user = user; 24 | 25 | await next(); 26 | }; 27 | 28 | const authUser = async (ctx, next) => { 29 | 30 | 31 | const code500 = { 32 | msg: 'The username already exists. Please try another one.' 33 | }; 34 | 35 | const db = getSqlite('user', 'user') 36 | 37 | const result = await searchSqliteTableData(db, 'userToken', { user: ctx.state.user }); 38 | 39 | if (result.length > 0) { 40 | ctx.status = 500; 41 | ctx.body = code500; 42 | return; 43 | } 44 | 45 | const token = getRandomPassword(30); 46 | 47 | const insertInfo = await insertSqliteTableData(db, 'userToken', { user: ctx.state.user, token }); 48 | 49 | ctx.state.nlog(insertInfo) 50 | 51 | ctx.state.responseData = { 52 | token, 53 | user: ctx.state.user, 54 | }; 55 | 56 | await next(); 57 | 58 | }; 59 | 60 | const register = async (ctx) => { 61 | ctx.body = { 62 | data: ctx.state.responseData, 63 | status: 200, 64 | message: 'success', 65 | } 66 | }; 67 | 68 | router.post('/register', checkBody, authUser, register); 69 | 70 | module.exports = router; -------------------------------------------------------------------------------- /admin/routers/password/router.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const path = require('path'); 3 | const router = new Router(); 4 | const currentDirectory = path.basename(__dirname); 5 | router.prefix('/' + currentDirectory) 6 | 7 | module.exports = router -------------------------------------------------------------------------------- /admin/utils/database/changeSqliteTableColumn.js: -------------------------------------------------------------------------------- 1 | const changeSqliteTableColumn = (db, tableName, oldColumn, newColumn, newColumnType) => { 2 | return new Promise((resolve, reject) => { 3 | db.serialize(async () => { 4 | try { 5 | const tableInfo = await new Promise((resolve, reject) => { 6 | db.all(`PRAGMA table_info(${tableName})`, (err, rows) => { 7 | if (err) { 8 | reject(err); 9 | } else { 10 | resolve(rows); 11 | } 12 | }); 13 | }); 14 | 15 | const columns = tableInfo.map(info => info.name); 16 | 17 | if (!columns.includes(oldColumn)) { 18 | throw new Error(`Column ${oldColumn} does not exist in table ${tableName}`); 19 | } 20 | 21 | const newColumnsWithTypes = tableInfo.map(info => { 22 | if (info.name === oldColumn) { 23 | return `${newColumn} ${newColumnType}`; 24 | } 25 | return `${info.name} ${info.type}`; 26 | }).join(', '); 27 | 28 | const newColumns = tableInfo.map(info => { 29 | if (info.name === oldColumn) { 30 | return `${newColumn}`; 31 | } 32 | return `${info.name}`; 33 | }).join(', '); 34 | 35 | const tempTable = `${tableName}_temp`; 36 | await new Promise((resolve, reject) => { 37 | db.run(`ALTER TABLE ${tableName} RENAME TO ${tempTable}`, (err) => { 38 | if (err) { 39 | reject(err); 40 | } else { 41 | resolve(); 42 | } 43 | }); 44 | }); 45 | 46 | await new Promise((resolve, reject) => { 47 | db.run(`CREATE TABLE ${tableName} (${newColumnsWithTypes})`, (err) => { 48 | if (err) { 49 | reject(err); 50 | } else { 51 | resolve(); 52 | } 53 | }); 54 | }); 55 | 56 | await new Promise((resolve, reject) => { 57 | db.run(`INSERT INTO ${tableName} (${newColumns}) SELECT ${newColumns} FROM ${tempTable}`, (err) => { 58 | if (err) { 59 | reject(err); 60 | } else { 61 | resolve(); 62 | } 63 | }); 64 | }); 65 | 66 | await new Promise((resolve, reject) => { 67 | db.run(`DROP TABLE ${tempTable}`, (err) => { 68 | if (err) { 69 | reject(err); 70 | } else { 71 | resolve(); 72 | } 73 | }); 74 | }); 75 | 76 | resolve(`Column ${oldColumn} modified to ${newColumn} in table ${tableName}`); 77 | } catch (err) { 78 | reject(err); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | // 修改Admin数据库password的表website字段为webSite 85 | // (async () => { 86 | // const db = require('./getSqlite')('Admin', 'password'); 87 | // console.log(await changeSqliteTableColumn(db, 'password','website','webSite','TEXT')); 88 | // })() 89 | 90 | module.exports = changeSqliteTableColumn; 91 | -------------------------------------------------------------------------------- /admin/utils/database/changeSqliteTableData.js: -------------------------------------------------------------------------------- 1 | const changeSqliteTableData = (db, tableName, updates, conditions) => { 2 | return new Promise((resolve, reject) => { 3 | const setClause = Object.keys(updates).map(key => `${key} = ?`).join(', '); 4 | const whereClause = Object.keys(conditions).map(key => `${key} = ?`).join(' AND '); 5 | const values = [...Object.values(updates), ...Object.values(conditions)]; 6 | const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`; 7 | db.run(sql, values, function (err) { 8 | if (err) { 9 | reject(err); 10 | db.close(); 11 | } else { 12 | resolve(this.changes); 13 | } 14 | }); 15 | }); 16 | } 17 | 18 | module.exports = changeSqliteTableData; 19 | -------------------------------------------------------------------------------- /admin/utils/database/createSqlite.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const sqlite3 = require('sqlite3').verbose(); 3 | const path = require('path'); 4 | const dbDir = path.join(__dirname, '../../db/') 5 | 6 | const createSqlite = (baseName, baseDir) => { 7 | 8 | return new Promise((resolve) => { 9 | const dbPath = path.join(dbDir, baseDir); 10 | 11 | if (!fs.existsSync(dbPath)) { 12 | fs.mkdirSync(dbPath); 13 | } 14 | 15 | if (fs.existsSync(path.join(dbPath, baseName + '.sqlite'))) { 16 | resolve('Database Already Exists') 17 | } 18 | 19 | new sqlite3.Database(path.join(dbPath, baseName + '.sqlite')); 20 | 21 | // 用于延迟执行 不然创建完成后立即获取不到Sqlite文件 22 | setTimeout(() => { 23 | resolve('Database Created') 24 | }, 1000) 25 | }) 26 | 27 | } 28 | 29 | module.exports = createSqlite; 30 | -------------------------------------------------------------------------------- /admin/utils/database/createSqliteTable.js: -------------------------------------------------------------------------------- 1 | const createSqliteTable = (db, tableName, columns) => { 2 | return new Promise((resolve, reject) => { 3 | let columnsDef = Object.entries(columns).map(([key, value]) => `${key} ${value}`).join(", "); 4 | const createTableSQL = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnsDef})`; 5 | db.run(createTableSQL, (err) => { 6 | if (err) { 7 | reject("Error creating table:", err.message); 8 | } 9 | resolve(`${tableName} table created successfully.`); 10 | }); 11 | }); 12 | } 13 | 14 | // 创建user数据库userToken表的数据 15 | // (async () => { 16 | // const db = require('./getSqlite')('user', 'user'); 17 | // console.log(await createSqliteTable(db, 'userToken', { 18 | // id: 'INTEGER PRIMARY KEY AUTOINCREMENT', 19 | // token: 'TEXT', 20 | // user_id: "INTEGER", 21 | // user: "TEXT", 22 | // })); 23 | // })() 24 | 25 | module.exports = createSqliteTable; 26 | -------------------------------------------------------------------------------- /admin/utils/database/deleteSqliteTableColumn.js: -------------------------------------------------------------------------------- 1 | const deleteSqliteTableColumn = (db, tableName, column) => { 2 | return new Promise((resolve, reject) => { 3 | db.all(`PRAGMA table_info(${tableName});`, [], (err, columns) => { 4 | if (err) { 5 | return reject(err); 6 | } 7 | 8 | if (!columns.some(col => col.name === column)) { 9 | return reject(new Error(`Column ${column} does not exist in table ${tableName}`)); 10 | } 11 | 12 | const newColumns = columns.filter(col => col.name !== column).map(col => `${col.name} ${col.type}`).join(', '); 13 | 14 | const newColumnNames = columns.filter(col => col.name !== column).map(col => col.name).join(', '); 15 | 16 | const createTempTableSQL = `CREATE TABLE ${tableName}_temp (${newColumns});`; 17 | const copyDataSQL = `INSERT INTO ${tableName}_temp SELECT ${newColumnNames} FROM ${tableName};`; 18 | const dropOldTableSQL = `DROP TABLE ${tableName};`; 19 | const renameTempTableSQL = `ALTER TABLE ${tableName}_temp RENAME TO ${tableName};`; 20 | 21 | db.serialize(() => { 22 | db.run("BEGIN TRANSACTION;"); 23 | db.run(createTempTableSQL); 24 | db.run(copyDataSQL); 25 | db.run(dropOldTableSQL); 26 | db.run(renameTempTableSQL); 27 | db.run("COMMIT;", (err) => { 28 | if (err) { 29 | db.run("ROLLBACK;"); 30 | return reject(err); 31 | } 32 | resolve(`Column ${column} removed from ${tableName} table`); 33 | }); 34 | }); 35 | }); 36 | }); 37 | } 38 | 39 | module.exports = deleteSqliteTableColumn; 40 | -------------------------------------------------------------------------------- /admin/utils/database/deleteSqliteTableData.js: -------------------------------------------------------------------------------- 1 | const deleteSqliteTableData = (db, tableName, conditions) => { 2 | return new Promise((resolve, reject) => { 3 | const whereClause = Object.keys(conditions).map(key => `${key} = ?`).join(' AND '); 4 | const values = Object.values(conditions); 5 | const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`; 6 | db.run(sql, values, function (err) { 7 | if (err) { 8 | reject(err); 9 | } else { 10 | resolve(this.changes); 11 | } 12 | }); 13 | }); 14 | } 15 | 16 | module.exports = deleteSqliteTableData; 17 | -------------------------------------------------------------------------------- /admin/utils/database/getSqlite.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const sqlite3 = require('sqlite3').verbose(); 3 | const path = require('path'); 4 | const dbDir = path.join(__dirname, '../../db/') 5 | 6 | const getSqlite = (baseName, baseDir) => { 7 | 8 | const dbPath = path.join(dbDir, baseDir); 9 | const basePath = path.join(dbPath, baseName + '.sqlite'); 10 | 11 | if (!fs.existsSync(basePath)) { 12 | throw new Error('Database Not Found.'); 13 | } 14 | 15 | const db = new sqlite3.Database(basePath); 16 | 17 | return db 18 | 19 | } 20 | 21 | module.exports = getSqlite; 22 | -------------------------------------------------------------------------------- /admin/utils/database/getSqliteAllTable.js: -------------------------------------------------------------------------------- 1 | const getSqliteAllTable = async (db) => { 2 | return new Promise((resolve, reject) => { 3 | const getSqliteAllTableSQL = `SELECT name FROM sqlite_master WHERE type='table'`; 4 | db.all(getSqliteAllTableSQL, (err, rows) => { 5 | if (err) { 6 | reject(err); 7 | } 8 | 9 | resolve(rows); 10 | }); 11 | }) 12 | } 13 | 14 | // 查询Admin数据库的表 15 | // (async () => { 16 | // const db = require('./getSqlite')('Admin', 'password'); 17 | // console.log(await getSqliteAllTable(db)); 18 | // })() 19 | 20 | module.exports = getSqliteAllTable; 21 | -------------------------------------------------------------------------------- /admin/utils/database/insertSqliteTableColumn.js: -------------------------------------------------------------------------------- 1 | const insertSqliteTableColumn = (db, tableName, column, columnType) => { 2 | return new Promise((resolve, reject) => { 3 | const sql = `ALTER TABLE ${tableName} ADD COLUMN ${column} ${columnType}`; 4 | db.run(sql, function (err) { 5 | if (err) { 6 | reject(err); 7 | } else { 8 | resolve(`Column ${column} added to ${tableName} table`); 9 | } 10 | }); 11 | }); 12 | } 13 | 14 | // 向Admin数据库password的表中插入twofa字段类型为TEXT 15 | // (async () => { 16 | // const db = require('./getSqlite')('Admin', 'password'); 17 | // console.log(await insertSqliteTableColumn(db, 'password','twofa','TEXT')); 18 | // })() 19 | 20 | module.exports = insertSqliteTableColumn; 21 | -------------------------------------------------------------------------------- /admin/utils/database/insertSqliteTableData.js: -------------------------------------------------------------------------------- 1 | const insertSqliteTableData = (db, tableName, data) => { 2 | return new Promise((resolve, reject) => { 3 | const columns = Object.keys(data).join(', '); 4 | const placeholders = Object.keys(data).map(key => '?').join(', '); 5 | const values = Object.values(data); 6 | const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`; 7 | db.run(sql, values, function (err) { 8 | if (err) { 9 | reject(err); 10 | } else { 11 | resolve("Insert Data Success, LastID: " + this.lastID); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | // (async () => { 18 | // const db = require('./getSqlite')('user', 'user'); 19 | // console.log(await insertSqliteTableData(db, 'userToken', { 20 | // token: 'PasswordNext', 21 | // user_id: "1", 22 | // user: "PasswordNext", 23 | // })); 24 | // })() 25 | 26 | module.exports = insertSqliteTableData; 27 | -------------------------------------------------------------------------------- /admin/utils/database/searchSqliteTableColumn.js: -------------------------------------------------------------------------------- 1 | const searchSqliteTableColumn = (db, tableName) => { 2 | return new Promise((resolve, reject) => { 3 | const sql = `PRAGMA table_info(${tableName})`; 4 | db.all(sql, [], (err, rows) => { 5 | if (err) { 6 | reject(err); 7 | } else { 8 | resolve(rows); 9 | } 10 | }); 11 | }); 12 | } 13 | 14 | module.exports = searchSqliteTableColumn; 15 | -------------------------------------------------------------------------------- /admin/utils/database/searchSqliteTableData.js: -------------------------------------------------------------------------------- 1 | const searchSqliteTableData = (db, tableName, conditions = {}, useFuzzySearch = false) => { 2 | return new Promise((resolve, reject) => { 3 | let whereClause = ''; 4 | let values = []; 5 | 6 | if (useFuzzySearch) { 7 | whereClause = Object.keys(conditions).map(key => `${key} LIKE ?`).join(' AND '); 8 | values = Object.values(conditions).map(value => `%${value}%`); 9 | } else { 10 | whereClause = Object.keys(conditions).map(key => `${key} = ?`).join(' AND '); 11 | values = Object.values(conditions); 12 | } 13 | 14 | const sql = `SELECT * FROM ${tableName}` + (whereClause ? ` WHERE ${whereClause}` : ''); 15 | db.all(sql, values, (err, rows) => { 16 | if (err) { 17 | reject(err); 18 | } else { 19 | resolve(rows); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | // 查询user数据库userToken表的数据 26 | // (async () => { 27 | // const db = require('./getSqlite')('user', 'user'); 28 | // console.log(await searchSqliteTableData(db, 'userToken', { })); 29 | // })() 30 | 31 | // 查询Admin数据库password表的数据 32 | // (async () => { 33 | // const db = require('./getSqlite')('Admin', 'password'); 34 | // console.log(await searchSqliteTableData(db, 'password', { })); 35 | // })() 36 | 37 | module.exports = searchSqliteTableData; 38 | -------------------------------------------------------------------------------- /admin/utils/get2faSecret.js: -------------------------------------------------------------------------------- 1 | const speakeasy = require('speakeasy'); 2 | const QRCode = require('qrcode'); 3 | 4 | const get2faSecretQRCode = (otpauth_url) => { 5 | 6 | return new Promise((resolve, reject) => { 7 | QRCode.toDataURL(otpauth_url, (err, data_url) => { 8 | if (err) { 9 | reject(err); 10 | } 11 | resolve(data_url); 12 | }); 13 | }); 14 | 15 | } 16 | 17 | const getSecretQRCode = async () => { 18 | 19 | const secret = speakeasy.generateSecret({ length: 20 }); 20 | 21 | const QRCode = await get2faSecretQRCode(secret.otpauth_url); 22 | 23 | return { 24 | secret: secret.base32, 25 | QRCode: QRCode, 26 | } 27 | 28 | } 29 | 30 | 31 | module.exports = getSecretQRCode; -------------------------------------------------------------------------------- /admin/utils/nlog.js: -------------------------------------------------------------------------------- 1 | const nlog = (message) => { 2 | const cstTimestamp = new Date(new Date().getTime() + 8 * 60 * 60 * 1000); 3 | console.log(`[${cstTimestamp.toISOString()}] [INFO] ${message}`); 4 | } 5 | 6 | const nlogController = () => async (ctx, next) => { ctx.state.nlog = nlog; await next(); } 7 | 8 | module.exports = nlogController; -------------------------------------------------------------------------------- /admin/utils/randomInt.js: -------------------------------------------------------------------------------- 1 | const getRandomInt = (max) => { 2 | return Math.floor(Math.random() * max); 3 | } 4 | 5 | module.exports = getRandomInt -------------------------------------------------------------------------------- /admin/utils/randomPassword.js: -------------------------------------------------------------------------------- 1 | const getRandomPassword = (length) => { 2 | 3 | if (!length || length - 6 < 2) { 4 | throw new Error("Length must be greater than 8"); 5 | } 6 | 7 | const numLowercase = length - 6; 8 | const lowerCharset = "abcdefghijklmnopqrstuvwxyz"; 9 | const upperCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 10 | const numberCharset = "0123456789"; 11 | const specialCharset = "!@#$%^&*()_+[]{}|;:,.<>?"; 12 | const allCharset = lowerCharset + upperCharset + numberCharset + specialCharset; 13 | 14 | let password = ""; 15 | 16 | for (let i = 0; i < 2; i++) { 17 | password += upperCharset[Math.floor(Math.random() * upperCharset.length)]; 18 | } 19 | 20 | for (let i = 0; i < numLowercase; i++) { 21 | password += lowerCharset[Math.floor(Math.random() * lowerCharset.length)]; 22 | } 23 | 24 | for (let i = 0; i < 2; i++) { 25 | password += numberCharset[Math.floor(Math.random() * numberCharset.length)]; 26 | } 27 | 28 | for (let i = 0; i < 2; i++) { 29 | password += specialCharset[Math.floor(Math.random() * specialCharset.length)]; 30 | } 31 | 32 | for (let i = password.length; i < length; i++) { 33 | password += allCharset[Math.floor(Math.random() * allCharset.length)]; 34 | } 35 | 36 | password = password.split('').sort(() => 0.5 - Math.random()).join(''); 37 | 38 | return password; 39 | } 40 | 41 | 42 | module.exports = getRandomPassword; -------------------------------------------------------------------------------- /admin/utils/randomString.js: -------------------------------------------------------------------------------- 1 | const getRandomString = (length) => { 2 | let result = ''; 3 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | const charactersLength = characters.length; 5 | for (let i = 0; i < length; i++) { 6 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 7 | if (i === 0) { 8 | result += getRandomInt(10); 9 | } 10 | } 11 | return result; 12 | } 13 | 14 | module.exports = getRandomString; -------------------------------------------------------------------------------- /example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/.DS_Store -------------------------------------------------------------------------------- /example/2fa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/2fa.png -------------------------------------------------------------------------------- /example/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/add.png -------------------------------------------------------------------------------- /example/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/edit.png -------------------------------------------------------------------------------- /example/handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/handle.png -------------------------------------------------------------------------------- /example/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/list.png -------------------------------------------------------------------------------- /example/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/login.png -------------------------------------------------------------------------------- /example/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/example/register.png -------------------------------------------------------------------------------- /web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | env: { 4 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:19899', 5 | } 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "password_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.7.2", 13 | "next": "14.2.4", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "typescript": "^5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/public/images/cancelSetting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/web/public/images/cancelSetting.png -------------------------------------------------------------------------------- /web/public/images/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/web/public/images/setting.png -------------------------------------------------------------------------------- /web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/apis/addPassword.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface AddPasswordRequest { 4 | account: string; 5 | password: string; 6 | appName: string; 7 | webSite: string; 8 | twofa: string; 9 | } 10 | 11 | const addPassword = async (requestData: AddPasswordRequest) => { 12 | 13 | const response = await Post("/password/addPassword", requestData); 14 | 15 | if (response.status !== 200) { 16 | throw new Error("AddPassword Fail, Please Contact Author!"); 17 | } 18 | 19 | const data = response.data; 20 | 21 | return data 22 | 23 | }; 24 | 25 | export { addPassword }; -------------------------------------------------------------------------------- /web/src/apis/deletePassword.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface DeletePasswordRequest { 4 | id: string; 5 | } 6 | 7 | const deletePassword = async (requestData: DeletePasswordRequest) => { 8 | 9 | const response = await Post("/password/deletePassword", requestData); 10 | 11 | if (response.status !== 200) { 12 | throw new Error("DeletePassword Fail, Please Contact Author!"); 13 | } 14 | 15 | const data = response.data; 16 | 17 | return data 18 | 19 | }; 20 | 21 | export { deletePassword }; -------------------------------------------------------------------------------- /web/src/apis/editNewPassword.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface EditNewPasswordRequest { 4 | id: string; 5 | account: string; 6 | password: string; 7 | appName: string; 8 | webSite: string; 9 | twofa: string; 10 | } 11 | 12 | const editNewPassword = async (requestData: EditNewPasswordRequest) => { 13 | 14 | const response = await Post("/password/editPassword", requestData); 15 | 16 | if (response.status !== 200) { 17 | throw new Error("EditNewPassword Fail, Please Contact Author!"); 18 | } 19 | 20 | const data = response.data; 21 | 22 | return data 23 | 24 | }; 25 | 26 | export { editNewPassword }; -------------------------------------------------------------------------------- /web/src/apis/get2faToken.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface Get2faTokenRequest { 4 | secret: string; 5 | } 6 | 7 | const get2faToken = async (requestData: Get2faTokenRequest) => { 8 | 9 | 10 | const response = await Post("/password/get2faToken", requestData); 11 | 12 | 13 | if (response.status !== 200) { 14 | throw new Error("Get2faToken Fail, Please Contact Author!"); 15 | } 16 | 17 | const data = response.data; 18 | 19 | return data 20 | 21 | }; 22 | 23 | export { get2faToken }; -------------------------------------------------------------------------------- /web/src/apis/getPassword.ts: -------------------------------------------------------------------------------- 1 | import { Get } from "@/utils/request"; 2 | 3 | const getPassword = async () => { 4 | 5 | const response = await Get("/password/getPassword", {}); 6 | 7 | if (response.status !== 200) { 8 | throw new Error("GetPassword Fail, Please Contact Author!"); 9 | } 10 | 11 | const data = response.data; 12 | 13 | return data 14 | 15 | }; 16 | 17 | export { getPassword }; -------------------------------------------------------------------------------- /web/src/apis/login.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface LoginRequest { 4 | token: string; 5 | } 6 | 7 | const login = async (requestData: LoginRequest) => { 8 | 9 | 10 | const response = await Post("/password/login", requestData); 11 | 12 | 13 | if (response.status !== 200) { 14 | throw new Error("Login Fail, Please Contact Author!"); 15 | } 16 | 17 | const data = response.data; 18 | 19 | return data 20 | 21 | }; 22 | 23 | export { login }; -------------------------------------------------------------------------------- /web/src/apis/register.ts: -------------------------------------------------------------------------------- 1 | import { Post } from "@/utils/request"; 2 | 3 | interface RegisterRequest { 4 | user: string; 5 | } 6 | 7 | const register = async (requestData: RegisterRequest) => { 8 | 9 | 10 | const response = await Post("/password/register", requestData); 11 | 12 | 13 | if (response.status !== 200) { 14 | throw new Error("Register Fail, Please Contact Author!"); 15 | } 16 | 17 | const data = response.data; 18 | 19 | return data 20 | 21 | }; 22 | 23 | export { register }; -------------------------------------------------------------------------------- /web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HChaoHui/Password-Next/60c6acfec86d524bd1a533819a27cc6440a28a0d/web/src/app/favicon.ico -------------------------------------------------------------------------------- /web/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Password Next", 9 | description: "Personal Password Repository", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /web/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | max-width: 100%; 46 | width: var(--max-width); 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | min-width: 300px 56 | } 57 | 58 | .card span { 59 | display: inline-block; 60 | transition: transform 200ms; 61 | } 62 | 63 | .card h2 { 64 | font-weight: 600; 65 | margin-bottom: 0.7rem; 66 | } 67 | 68 | .card p { 69 | margin: 0; 70 | opacity: 0.6; 71 | font-size: 0.9rem; 72 | line-height: 1.5; 73 | max-width: 30ch; 74 | text-wrap: balance; 75 | } 76 | 77 | .center { 78 | width: 100%; 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | position: relative; 83 | max-width: var(--max-width); 84 | } 85 | 86 | .center::before { 87 | background: var(--secondary-glow); 88 | border-radius: 50%; 89 | width: 480px; 90 | height: 360px; 91 | margin-left: -400px; 92 | } 93 | 94 | .center::after { 95 | background: var(--primary-glow); 96 | width: 240px; 97 | height: 180px; 98 | z-index: -1; 99 | } 100 | 101 | .center::before, 102 | .center::after { 103 | content: ""; 104 | left: 50%; 105 | position: absolute; 106 | filter: blur(45px); 107 | transform: translateZ(0); 108 | } 109 | 110 | .logo { 111 | position: relative; 112 | } 113 | 114 | .loginForm { 115 | width: 600px; 116 | position: relative; 117 | display: flex; 118 | flex-wrap: wrap; 119 | justify-content: center; 120 | } 121 | 122 | .loginText { 123 | width: 100%; 124 | height: 200px; 125 | padding: 10px; 126 | resize: none; 127 | outline: none; 128 | border-radius: 6px; 129 | font-family: var(--font-mono); 130 | } 131 | 132 | .loginButton { 133 | width: 100%; 134 | padding: 16px 0px; 135 | margin: 20px auto; 136 | border: none; 137 | border-radius: 6px; 138 | background-color: #1e293b; 139 | color: #FFFFFF; 140 | cursor: pointer; 141 | font-family: var(--font-mono); 142 | } 143 | 144 | .loginTips { 145 | width: 100%; 146 | height: 20px; 147 | text-align: left; 148 | font-size: 12px; 149 | font-weight: bold; 150 | color: red; 151 | } 152 | 153 | .passwordList { 154 | position: relative; 155 | display: flex; 156 | flex-wrap: wrap; 157 | font-family: var(--font-mono); 158 | margin: 10px auto; 159 | justify-content: space-between; 160 | flex: 1; 161 | } 162 | 163 | .password { 164 | width: 49%; 165 | margin: 0; 166 | padding: 20px; 167 | background-color: rgba(var(--callout-rgb), 0.5); 168 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 169 | border-radius: var(--border-radius); 170 | cursor: pointer; 171 | margin: 10px 0px 0px 0px; 172 | min-width: 400px; 173 | position: relative; 174 | overflow: hidden; 175 | } 176 | 177 | .passwordOption { 178 | margin: 0px 10px; 179 | display: flex; 180 | align-items: center; 181 | line-height: 30px; 182 | } 183 | 184 | .passwordLabel { 185 | width: 100px; 186 | font-weight: bold; 187 | } 188 | 189 | .passwordHref { 190 | text-decoration: underline; 191 | } 192 | 193 | .setting { 194 | position: absolute; 195 | top: 0px; 196 | right: 0px; 197 | display: flex; 198 | padding: 10px; 199 | } 200 | 201 | @keyframes topInFromSlide { 202 | from { 203 | transform: translateY(100%); 204 | } 205 | 206 | to { 207 | transform: translateY(0); 208 | } 209 | } 210 | 211 | .newPassword { 212 | width: 100%; 213 | height: auto; 214 | font-family: var(--font-mono); 215 | padding: 20px; 216 | position: absolute; 217 | /* animation: topInFromSlide 0.7s ease-in-out forwards; */ 218 | } 219 | 220 | .newPasswordInput { 221 | display: flex; 222 | line-height: 40px; 223 | align-items: center; 224 | justify-content: center; 225 | margin: 0 auto; 226 | margin-bottom: 10px; 227 | } 228 | 229 | .newPasswordInput p { 230 | width: 90px; 231 | text-align: right; 232 | margin-right: 10px; 233 | font-weight: bold; 234 | } 235 | 236 | .newPasswordInput input { 237 | width: 70%; 238 | height: 40px; 239 | padding: 10px; 240 | outline: none; 241 | font-family: var(--font-mono); 242 | border: 1px solid #999; 243 | border-radius: 6px; 244 | } 245 | 246 | .newPasswordOperation { 247 | display: flex; 248 | line-height: 30px; 249 | align-items: center; 250 | justify-content: center; 251 | margin: 60px auto; 252 | margin-bottom: 0px; 253 | } 254 | 255 | .newPasswordOperation p { 256 | margin: 0px 60px; 257 | } 258 | 259 | @keyframes slideInFromTop { 260 | from { 261 | transform: translateY(-100%); 262 | } 263 | 264 | to { 265 | transform: translateY(0); 266 | } 267 | } 268 | 269 | .settingPage { 270 | position: absolute; 271 | width: 100%; 272 | height: 100%; 273 | top: 0; 274 | left: 0; 275 | background-color: rgba(var(--callout-rgb), 1); 276 | animation: slideInFromTop 0.4s ease-in-out forwards; 277 | padding: 20px; 278 | } 279 | 280 | .settingPageOperationList { 281 | display: flex; 282 | } 283 | 284 | .settingPageOperation { 285 | text-decoration: underline; 286 | margin-right: 10px; 287 | cursor: pointer; 288 | user-select: none; 289 | } 290 | 291 | .addNewPassword { 292 | text-decoration: underline; 293 | margin-right: 10px; 294 | cursor: pointer; 295 | user-select: none; 296 | position: relative; 297 | z-index: 99; 298 | font-weight: bold; 299 | } 300 | 301 | .copySuccess { 302 | color: green; 303 | } 304 | 305 | .disableSelect { 306 | user-select: none; 307 | } 308 | 309 | .noPassword { 310 | width: 100%; 311 | text-align: center; 312 | position: relative; 313 | } 314 | 315 | .register { 316 | font-size: 14px; 317 | } 318 | 319 | .register span { 320 | font-weight: bold; 321 | text-decoration: underline; 322 | cursor: pointer; 323 | user-select: none; 324 | } 325 | 326 | .registerSuccessTips { 327 | width: 100%; 328 | text-align: left; 329 | font-size: 12px; 330 | font-weight: bold; 331 | color: green; 332 | margin: 0px 0px 20px 0px; 333 | } 334 | 335 | .registerSuccessTips .code { 336 | font-size: 16px; 337 | } 338 | 339 | /* Enable hover only on non-touch devices */ 340 | @media (hover: hover) and (pointer: fine) { 341 | .card:hover { 342 | background: rgba(var(--card-rgb), 0.1); 343 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 344 | } 345 | 346 | .card:hover span { 347 | transform: translateX(4px); 348 | } 349 | } 350 | 351 | @media (prefers-reduced-motion) { 352 | .card:hover span { 353 | transform: none; 354 | } 355 | } 356 | 357 | @media (max-width: 600px) { 358 | 359 | .main { 360 | padding: 0; 361 | } 362 | 363 | .loginForm { 364 | width: 300px; 365 | } 366 | 367 | .password { 368 | min-width: 330px; 369 | padding: 10px; 370 | margin: 0 auto; 371 | margin-bottom: 20px; 372 | } 373 | } 374 | 375 | /* Mobile */ 376 | @media (max-width: 700px) { 377 | .content { 378 | padding: 4rem; 379 | } 380 | 381 | .grid { 382 | grid-template-columns: 1fr; 383 | margin-bottom: 120px; 384 | max-width: 320px; 385 | text-align: center; 386 | } 387 | 388 | .card { 389 | padding: 1rem 2.5rem; 390 | } 391 | 392 | .card h2 { 393 | margin-bottom: 0.5rem; 394 | } 395 | 396 | .center { 397 | padding: 8rem 0 6rem; 398 | position: relative; 399 | } 400 | 401 | .center::before { 402 | transform: none; 403 | height: 300px; 404 | } 405 | 406 | .description { 407 | font-size: 0.8rem; 408 | } 409 | 410 | .description a { 411 | padding: 1rem; 412 | } 413 | 414 | .description p, 415 | .description div { 416 | display: flex; 417 | justify-content: center; 418 | position: fixed; 419 | width: 100%; 420 | } 421 | 422 | .description p { 423 | align-items: center; 424 | inset: 0 0 auto; 425 | padding: 2rem 1rem 1.4rem; 426 | border-radius: 0; 427 | border: none; 428 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 429 | background: linear-gradient(to bottom, 430 | rgba(var(--background-start-rgb), 1), 431 | rgba(var(--callout-rgb), 0.5)); 432 | background-clip: padding-box; 433 | backdrop-filter: blur(24px); 434 | } 435 | 436 | .description div { 437 | align-items: flex-end; 438 | inset: auto 0 0; 439 | padding: 2rem; 440 | height: 100px; 441 | background: linear-gradient(to bottom, 442 | transparent 0%, 443 | rgb(var(--background-end-rgb)) 40%); 444 | z-index: 1; 445 | } 446 | } 447 | 448 | /* Tablet and Smaller Desktop */ 449 | @media (min-width: 701px) and (max-width: 1120px) { 450 | .grid { 451 | grid-template-columns: repeat(2, 50%); 452 | } 453 | 454 | .password { 455 | width: 100%; 456 | } 457 | } 458 | 459 | @media (prefers-color-scheme: dark) { 460 | .vercelLogo { 461 | filter: invert(1); 462 | } 463 | 464 | .logo { 465 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 466 | } 467 | } 468 | 469 | @keyframes rotate { 470 | from { 471 | transform: rotate(360deg); 472 | } 473 | 474 | to { 475 | transform: rotate(0deg); 476 | } 477 | } -------------------------------------------------------------------------------- /web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import styles from "./page.module.css"; 3 | import { useEffect, useState, useRef } from "react"; 4 | import Image from "next/image"; 5 | import { login } from "@/apis/login"; 6 | import { getPassword } from "@/apis/getPassword"; 7 | import { get2faToken } from "@/apis/get2faToken"; 8 | import { deletePassword } from "@/apis/deletePassword"; 9 | import { addPassword } from "@/apis/addPassword"; 10 | import { editNewPassword } from "@/apis/editNewPassword"; 11 | import { register } from "@/apis/register"; 12 | import Loading from "@/components/loading/page"; 13 | 14 | export default function Home() { 15 | 16 | const [token, setToken] = useState(""); 17 | 18 | const [localToken, setLocalToken] = useState(""); 19 | 20 | const [loadingStatus, setLoadingStatus] = useState(true); 21 | 22 | const [passwordList, setPasswordList] = useState([]); 23 | 24 | const [copiedIndex, setCopiedIndex] = useState(null); 25 | 26 | const [deleteStaus, setDeleteStaus] = useState(false); 27 | 28 | const [newPasswordStatus, setNewPasswordStatus] = useState(false); 29 | 30 | const [editPasswordStatus, setEditPasswordStatus] = useState(false); 31 | 32 | const [editAddPasswordStatus, setEditAddPasswordStatus] = useState(false); 33 | 34 | const [addPasswordStatus, setAddPasswordStatus] = useState(false); 35 | 36 | const [settingIndex, setSettingIndex] = useState(null); 37 | 38 | const [loginTips, setLoginTips] = useState(""); 39 | 40 | const [loginStatus, setLoginStatus] = useState(false); 41 | 42 | const [registerTips, setRegisterTips] = useState(""); 43 | 44 | const [registerToken, setRegisterToken] = useState(""); 45 | 46 | const [registerStatus, setRegisterStatus] = useState(false); 47 | 48 | const [registerRequestStatus, setRegisterRequestStatus] = useState(false); 49 | 50 | const [registerSuccessStatus, setRegisterSuccessStatus] = useState(false); 51 | 52 | const [registerUserName, setRegisterUserName] = useState(""); 53 | 54 | const [twofaIndex, setTwofaIndex] = useState(null); 55 | 56 | const [twofa, setTwofa] = useState(null); 57 | 58 | const copiedTimeoutRef = useRef(null); 59 | 60 | const twofaTimeoutRef = useRef(null); 61 | 62 | const newPasswordData = { 63 | appName: "", 64 | webSite: "", 65 | account: "", 66 | password: "", 67 | twofa: "", 68 | } 69 | 70 | const [newPassword, setNewPassword] = useState(newPasswordData); 71 | 72 | const [editPassword, setEditPassword] = useState(newPasswordData); 73 | 74 | useEffect(() => { 75 | const localToken = localStorage.getItem("localToken"); 76 | if (localToken) { 77 | loginRequest(localToken) 78 | return 79 | } 80 | 81 | setLoadingStatus(false) 82 | }, []) 83 | 84 | const loginUser = () => { 85 | 86 | if (token) { 87 | setLoginStatus(true) 88 | loginRequest(token) 89 | } 90 | 91 | } 92 | 93 | const registerUser = () => { 94 | if (registerUserName) { 95 | 96 | setRegisterRequestStatus(true) 97 | 98 | register({ user: registerUserName }).then(res => { 99 | setRegisterToken(res.data.token) 100 | setRegisterSuccessStatus(true) 101 | setRegisterRequestStatus(false) 102 | }).catch(err => { 103 | setRegisterRequestStatus(false) 104 | setRegisterTips(err.response.data.msg) 105 | }) 106 | 107 | } 108 | } 109 | 110 | const loginRequest = (token: string) => { 111 | const requestData = { 112 | token: token, 113 | } 114 | 115 | login(requestData).then((res) => { 116 | const { token } = res.data; 117 | setLocalToken(token) 118 | localStorage.setItem("localToken", token); 119 | setLoginStatus(false) 120 | getPasswordRequest() 121 | }).catch(err => { 122 | localStorage.removeItem("localToken"); 123 | setLoginTips(err.response.data.msg) 124 | setLoadingStatus(false) 125 | setLoginStatus(false) 126 | }) 127 | 128 | } 129 | 130 | const getPasswordRequest = () => { 131 | getPassword().then((res) => { 132 | setPasswordList(res.data); 133 | setLoadingStatus(false) 134 | setDeleteStaus(false) 135 | setAddPasswordStatus(false) 136 | setNewPasswordStatus(false) 137 | setEditPasswordStatus(false) 138 | setEditAddPasswordStatus(false) 139 | setSettingIndex(null) 140 | }).catch(() => { 141 | console.log("error"); 142 | }) 143 | } 144 | 145 | const copyPassword = (password: string, index: any) => { 146 | 147 | if (copiedTimeoutRef.current) { 148 | clearTimeout(copiedTimeoutRef.current); 149 | } 150 | 151 | navigator.clipboard.writeText(password).then(() => { 152 | setCopiedIndex(index); 153 | copiedTimeoutRef.current = setTimeout(() => setCopiedIndex(null), 2000); 154 | }); 155 | } 156 | 157 | const show2fa = (twofa: string, index: any) => { 158 | 159 | if (twofaTimeoutRef.current) { 160 | clearTimeout(twofaTimeoutRef.current); 161 | } 162 | 163 | get2faToken({ secret: twofa }).then(res => { 164 | setTwofaIndex(index) 165 | const dom = {res.data.token} {res.data.remainingTime}s 166 | setTwofa(dom) 167 | twofaTimeoutRef.current = setTimeout(() => setTwofaIndex(null), 3000); 168 | }) 169 | } 170 | 171 | const deletePasswordRequest = (id: string) => { 172 | setDeleteStaus(true) 173 | deletePassword({ id }).then(res => { 174 | getPasswordRequest() 175 | }) 176 | } 177 | 178 | const addNewPassword = () => { 179 | 180 | const data = newPassword as any; 181 | 182 | if (!data.appName) { 183 | return "appName cannot be empty"; 184 | } 185 | if (!data.webSite) { 186 | return "webSite cannot be empty"; 187 | } 188 | if (!data.account) { 189 | return "account cannot be empty"; 190 | } 191 | if (!data.password) { 192 | return "password cannot be empty"; 193 | } 194 | 195 | setAddPasswordStatus(true) 196 | 197 | addPassword(data).then(res => { 198 | getPasswordRequest(); 199 | }) 200 | 201 | } 202 | 203 | const editNewPasswordRequest = () => { 204 | const data = editPassword as any; 205 | 206 | if (!data.appName) { 207 | return "appName cannot be empty"; 208 | } 209 | if (!data.webSite) { 210 | return "webSite cannot be empty"; 211 | } 212 | if (!data.account) { 213 | return "account cannot be empty"; 214 | } 215 | if (!data.password) { 216 | return "password cannot be empty"; 217 | } 218 | 219 | setEditPasswordStatus(true) 220 | 221 | setEditAddPasswordStatus(true) 222 | 223 | editNewPassword(data).then(res => { 224 | getPasswordRequest(); 225 | }) 226 | } 227 | 228 | return ( 229 |
230 |
231 |

232 |    233 | Password Next 234 |    235 |

236 | {!localToken && ( 237 |
238 | By HChaoHui 239 |
240 | )} 241 | {(localToken && !newPasswordStatus && !editPasswordStatus) && ( 242 |
{ setNewPasswordStatus(true); setNewPassword(newPasswordData) }}> 243 | + New Password 244 |
245 | )} 246 | {(localToken && newPasswordStatus) && ( 247 |
{ setNewPasswordStatus(false) }}> 248 | Cancel 249 |
250 | )} 251 | {(localToken && editPasswordStatus) && ( 252 |
{ setEditPasswordStatus(false) }}> 253 | Cancel 254 |
255 | )} 256 |
257 | 258 |
259 | 260 | {(!localToken && !registerStatus) && ( 261 |
262 | 263 |

{loginTips}

264 | 265 | {!loginStatus && ( 266 | 267 | )} 268 | 269 | {loginStatus && ( 270 | 271 | )} 272 | 273 |

No account? { setRegisterStatus(true) }}>Click to register.

274 |
275 | )} 276 | 277 | {(!localToken && registerStatus) && ( 278 |
279 | 280 |

{registerTips}

281 | 282 | {registerSuccessStatus && ( 283 |

284 | Register Success.
285 | Token: {registerToken}
286 | Please keep the Token safe, as it will not appear again and serves as your sole login credential. 287 |

288 | )} 289 | 290 | {!registerRequestStatus && ( 291 | 292 | )} 293 | 294 | {registerRequestStatus && ( 295 | 296 | )} 297 | 298 |

Already have an account? { setRegisterStatus(false) }}>Click to log in.

299 |
300 | )} 301 | 302 | {(passwordList.length > 0 && !newPasswordStatus && !editPasswordStatus) && ( 303 |
304 | {passwordList.map((item, index) => { 305 | return ( 306 |
307 |
308 | AppName: {item.appName} 309 |
310 |
311 | WebSite: {item.webSite} 312 |
313 |
314 | Account: {item.account} 315 |
316 |
317 | Password: 318 | { copyPassword(item.password, index) }}>****** 319 | {copiedIndex === index &&  Copied Success!!! } 320 |
321 | {item.twofa && ( 322 |
323 | 2fa: 324 | {twofaIndex == index ? twofa : { show2fa(item.twofa, index) }}>******} 325 |
326 | )} 327 | 328 |
{ setSettingIndex(item.id) }}> 329 | setting 330 |
331 | 332 | {settingIndex == item.id && ( 333 |
334 | 335 |
336 |

{ setEditPasswordStatus(true); setEditPassword({ ...item }) }}>Edit

337 | {!deleteStaus && ( 338 |

{ deletePasswordRequest(item.id) }}>Delete

339 | )} 340 | {deleteStaus && ( 341 |

Deleteing...

342 | )} 343 |
344 | 345 |
{ setSettingIndex(null) }}> 346 | setting 347 |
348 | 349 |
350 | )} 351 | 352 |
353 | ) 354 | })} 355 |
356 | )} 357 | 358 | {(passwordList.length == 0 && !newPasswordStatus && !editPasswordStatus && localToken) && ( 359 |
360 | No password set, please add one. 361 |
362 | )} 363 | 364 | {newPasswordStatus && ( 365 |
366 | 367 |
368 |

Account:

369 | { setNewPassword({ ...newPassword, account: e.target.value }) }} /> 370 |
371 |
372 |

Password:

373 | { setNewPassword({ ...newPassword, password: e.target.value }) }} /> 374 |
375 |
376 |

AppName:

377 | { setNewPassword({ ...newPassword, appName: e.target.value }) }} /> 378 |
379 |
380 |

WebSite:

381 | { setNewPassword({ ...newPassword, webSite: e.target.value }) }} /> 382 |
383 |
384 |

2fa:

385 | { setNewPassword({ ...newPassword, twofa: e.target.value }) }} /> 386 |
387 | 388 | {!addPasswordStatus && ( 389 |
390 |

{ addNewPassword() }}>Submit

391 |
392 | )} 393 | 394 | {addPasswordStatus && ( 395 |
396 |

Submit...

397 |
398 | )} 399 | 400 |
401 | )} 402 | 403 | {editPasswordStatus && ( 404 |
405 | 406 |
407 |

Account:

408 | { setEditPassword({ ...editPassword, account: e.target.value }) }} /> 409 |
410 |
411 |

Password:

412 | { setEditPassword({ ...editPassword, password: e.target.value }) }} /> 413 |
414 |
415 |

AppName:

416 | { setEditPassword({ ...editPassword, appName: e.target.value }) }} /> 417 |
418 |
419 |

WebSite:

420 | { setEditPassword({ ...editPassword, webSite: e.target.value }) }} /> 421 |
422 |
423 |

2fa:

424 | { setEditPassword({ ...editPassword, twofa: e.target.value }) }} /> 425 |
426 | 427 | {!editAddPasswordStatus && ( 428 |
429 |

{ editNewPasswordRequest() }}>Submit

430 |
431 | )} 432 | 433 | {editAddPasswordStatus && ( 434 |
435 |

Submit...

436 |
437 | )} 438 | 439 |
440 | )} 441 | 442 |
443 | 444 |
445 | 446 |
447 | 448 | {loadingStatus && } 449 |
450 | ); 451 | } 452 | -------------------------------------------------------------------------------- /web/src/components/loading/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | z-index: 99; 8 | background-color:#FFFFFF; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | color: #666666; 13 | font-weight: bold; 14 | font-size: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /web/src/components/loading/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import styles from "./page.module.css"; 3 | export default function Loading() { 4 | 5 | return ( 6 |
7 | loading... 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /web/src/config/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | textInputMaxLength: 5000, 3 | } 4 | 5 | export default config -------------------------------------------------------------------------------- /web/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api: string = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:19899'; 4 | 5 | const request = axios.create({ 6 | baseURL: api, 7 | timeout: 60000, 8 | headers: { 9 | 'Content-Type': 'application/json' 10 | }, 11 | }); 12 | 13 | const getToken = () => { 14 | if (typeof window !== 'undefined') { 15 | return localStorage.getItem('localToken'); 16 | } 17 | return null; 18 | }; 19 | 20 | const Get = (url: string, params: object) => { 21 | const token = getToken(); 22 | return request.get(url, { 23 | params, 24 | headers: { 25 | 'Authorization': token ? `Bearer ${token}` : '', 26 | }, 27 | }); 28 | }; 29 | 30 | const Post = (url: string, data: object) => { 31 | const token = getToken(); 32 | return request.post(url, data, { 33 | headers: { 34 | 'Authorization': token ? `Bearer ${token}` : '', 35 | }, 36 | }); 37 | }; 38 | 39 | export { Get, Post }; -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------