mirror of
				https://github.com/whyour/qinglong.git
				synced 2025-10-31 17:06:07 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			252 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			252 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { Service, Inject } from 'typedi';
 | ||
| import winston from 'winston';
 | ||
| import { createRandomString, getNetIp } from '../config/util';
 | ||
| import config from '../config';
 | ||
| import * as fs from 'fs';
 | ||
| import _ from 'lodash';
 | ||
| import jwt from 'jsonwebtoken';
 | ||
| import { authenticator } from '@otplib/preset-default';
 | ||
| import { exec } from 'child_process';
 | ||
| import DataStore from 'nedb';
 | ||
| import { AuthInfo, LoginStatus } from '../data/auth';
 | ||
| 
 | ||
| @Service()
 | ||
| export default class AuthService {
 | ||
|   private authDb = new DataStore({ filename: config.authDbFile });
 | ||
| 
 | ||
|   constructor(@Inject('logger') private logger: winston.Logger) {
 | ||
|     this.authDb.loadDatabase((err) => {
 | ||
|       if (err) throw err;
 | ||
|     });
 | ||
|     this.authDb.persistence.setAutocompactionInterval(30000);
 | ||
|   }
 | ||
| 
 | ||
|   public async login(
 | ||
|     payloads: {
 | ||
|       username: string;
 | ||
|       password: string;
 | ||
|     },
 | ||
|     req: any,
 | ||
|   ): Promise<any> {
 | ||
|     if (!fs.existsSync(config.authConfigFile)) {
 | ||
|       return this.initAuthInfo();
 | ||
|     }
 | ||
| 
 | ||
|     let { username, password } = payloads;
 | ||
|     const content = this.getAuthInfo();
 | ||
|     const timestamp = Date.now();
 | ||
|     if (content) {
 | ||
|       const {
 | ||
|         username: cUsername,
 | ||
|         password: cPassword,
 | ||
|         retries = 0,
 | ||
|         lastlogon,
 | ||
|         lastip,
 | ||
|         lastaddr,
 | ||
|         twoFactorActived,
 | ||
|         isTwoFactorChecking,
 | ||
|       } = content;
 | ||
| 
 | ||
|       if (
 | ||
|         (cUsername === 'admin' && cPassword === 'adminadmin') ||
 | ||
|         !cUsername ||
 | ||
|         !cPassword
 | ||
|       ) {
 | ||
|         return this.initAuthInfo();
 | ||
|       }
 | ||
| 
 | ||
|       if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
 | ||
|         return {
 | ||
|           code: 410,
 | ||
|           message: `失败次数过多,请${Math.round(
 | ||
|             (Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
 | ||
|           )}秒后重试`,
 | ||
|           data: Math.round(
 | ||
|             (Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
 | ||
|           ),
 | ||
|         };
 | ||
|       }
 | ||
| 
 | ||
|       const { ip, address } = await getNetIp(req);
 | ||
|       if (username === cUsername && password === cPassword) {
 | ||
|         if (twoFactorActived && !isTwoFactorChecking) {
 | ||
|           this.updateAuthInfo(content, {
 | ||
|             isTwoFactorChecking: true,
 | ||
|           });
 | ||
|           return {
 | ||
|             code: 420,
 | ||
|             message: '请输入两步验证token',
 | ||
|           };
 | ||
|         }
 | ||
| 
 | ||
|         const data = createRandomString(50, 100);
 | ||
|         const expiration = twoFactorActived ? 30 : 3;
 | ||
|         let token = jwt.sign({ data }, config.secret as any, {
 | ||
|           expiresIn: 60 * 60 * 24 * expiration,
 | ||
|           algorithm: 'HS384',
 | ||
|         });
 | ||
| 
 | ||
|         this.updateAuthInfo(content, {
 | ||
|           token,
 | ||
|           lastlogon: timestamp,
 | ||
|           retries: 0,
 | ||
|           lastip: ip,
 | ||
|           lastaddr: address,
 | ||
|           isTwoFactorChecking: false,
 | ||
|         });
 | ||
|         exec(
 | ||
|           `notify 登陆通知 你于${new Date(
 | ||
|             timestamp,
 | ||
|           ).toLocaleString()}在${address}登陆成功,ip地址${ip}`,
 | ||
|         );
 | ||
|         await this.getLoginLog();
 | ||
|         await this.insertDb({
 | ||
|           type: 'loginLog',
 | ||
|           info: { timestamp, address, ip, status: LoginStatus.success },
 | ||
|         });
 | ||
|         return {
 | ||
|           code: 200,
 | ||
|           data: { token, lastip, lastaddr, lastlogon, retries },
 | ||
|         };
 | ||
|       } else {
 | ||
|         this.updateAuthInfo(content, {
 | ||
|           retries: retries + 1,
 | ||
|           lastlogon: timestamp,
 | ||
|           lastip: ip,
 | ||
|           lastaddr: address,
 | ||
|         });
 | ||
|         exec(
 | ||
|           `notify 登陆通知 你于${new Date(
 | ||
|             timestamp,
 | ||
|           ).toLocaleString()}在${address}登陆失败,ip地址${ip}`,
 | ||
|         );
 | ||
|         await this.getLoginLog();
 | ||
|         await this.insertDb({
 | ||
|           type: 'loginLog',
 | ||
|           info: { timestamp, address, ip, status: LoginStatus.fail },
 | ||
|         });
 | ||
|         return { code: 400, message: config.authError };
 | ||
|       }
 | ||
|     } else {
 | ||
|       return this.initAuthInfo();
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   public async getLoginLog(): Promise<AuthInfo[]> {
 | ||
|     return new Promise((resolve) => {
 | ||
|       this.authDb.find({ type: 'loginLog' }).exec((err, docs) => {
 | ||
|         if (err || docs.length === 0) {
 | ||
|           resolve(docs);
 | ||
|         } else {
 | ||
|           const result = docs.sort(
 | ||
|             (a, b) => b.info.timestamp - a.info.timestamp,
 | ||
|           );
 | ||
|           if (result.length > 100) {
 | ||
|             this.authDb.remove({ _id: result[result.length - 1]._id });
 | ||
|           }
 | ||
|           resolve(result.map((x) => x.info));
 | ||
|         }
 | ||
|       });
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
 | ||
|     return new Promise((resolve) => {
 | ||
|       this.authDb.insert(payload, (err, doc) => {
 | ||
|         if (err) {
 | ||
|           this.logger.error(err);
 | ||
|         } else {
 | ||
|           resolve(doc);
 | ||
|         }
 | ||
|       });
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   private initAuthInfo() {
 | ||
|     const newPassword = createRandomString(16, 22);
 | ||
|     fs.writeFileSync(
 | ||
|       config.authConfigFile,
 | ||
|       JSON.stringify({
 | ||
|         username: 'admin',
 | ||
|         password: newPassword,
 | ||
|       }),
 | ||
|     );
 | ||
|     return {
 | ||
|       code: 100,
 | ||
|       message: '已初始化密码,请前往auth.json查看并重新登录',
 | ||
|     };
 | ||
|   }
 | ||
| 
 | ||
|   public getUserInfo(): Promise<any> {
 | ||
|     return new Promise((resolve) => {
 | ||
|       fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
 | ||
|         if (err) console.log(err);
 | ||
|         resolve(JSON.parse(data));
 | ||
|       });
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   public initTwoFactor() {
 | ||
|     const secret = authenticator.generateSecret();
 | ||
|     const authInfo = this.getAuthInfo();
 | ||
|     const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
 | ||
|     this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
 | ||
|     return { secret, url: otpauth };
 | ||
|   }
 | ||
| 
 | ||
|   public activeTwoFactor(code: string) {
 | ||
|     const authInfo = this.getAuthInfo();
 | ||
|     const isValid = authenticator.verify({
 | ||
|       token: code,
 | ||
|       secret: authInfo.twoFactorSecret,
 | ||
|     });
 | ||
|     if (isValid) {
 | ||
|       this.updateAuthInfo(authInfo, { twoFactorActived: true });
 | ||
|     }
 | ||
|     return isValid;
 | ||
|   }
 | ||
| 
 | ||
|   public async twoFactorLogin({ username, password, code }, req) {
 | ||
|     const authInfo = this.getAuthInfo();
 | ||
|     const { isTwoFactorChecking, retries, twoFactorSecret } = authInfo;
 | ||
|     if (!isTwoFactorChecking) {
 | ||
|       return { code: 450, message: '未知错误' };
 | ||
|     }
 | ||
|     const isValid = authenticator.verify({
 | ||
|       token: code,
 | ||
|       secret: twoFactorSecret,
 | ||
|     });
 | ||
|     if (isValid) {
 | ||
|       return this.login({ username, password }, req);
 | ||
|     } else {
 | ||
|       const { ip, address } = await getNetIp(req);
 | ||
|       this.updateAuthInfo(authInfo, {
 | ||
|         lastip: ip,
 | ||
|         lastaddr: address,
 | ||
|       });
 | ||
|       return { code: 430, message: '验证失败' };
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   public deactiveTwoFactor() {
 | ||
|     const authInfo = this.getAuthInfo();
 | ||
|     this.updateAuthInfo(authInfo, {
 | ||
|       twoFactorActived: false,
 | ||
|       twoFactorSecret: '',
 | ||
|     });
 | ||
|     return true;
 | ||
|   }
 | ||
| 
 | ||
|   private getAuthInfo() {
 | ||
|     const content = fs.readFileSync(config.authConfigFile, 'utf8');
 | ||
|     return JSON.parse(content || '{}');
 | ||
|   }
 | ||
| 
 | ||
|   private updateAuthInfo(authInfo: any, info: any) {
 | ||
|     fs.writeFileSync(
 | ||
|       config.authConfigFile,
 | ||
|       JSON.stringify({ ...authInfo, ...info }),
 | ||
|     );
 | ||
|   }
 | ||
| }
 | 
