Browse Source

🎉 init: init commit

Pchen. 3 months ago
commit
6c3d984a2e

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+node_modules/
+*.log
+config.json
+uploads/

+ 85 - 0
BaseStdResponse.js

@@ -0,0 +1,85 @@
+let BaseStdResponse = {
+    OK:{
+        code:0,
+        msg:'ok'
+    },
+    ERR:{
+        code:-1,
+        msg:'err'
+    },
+    MISSING_PARAMETER:{//缺少参数
+        code:-2,
+        msg:'Missing parameter'
+    },
+    MISSING_FILE:{//找不到文件
+        code:-404,
+        msg:'Missing file'
+    },
+    METHOD_NOT_EXIST:{//方法不存在
+        code:-400,
+        msg:'Method not exist'
+    },
+    ACCESS_DENIED:{//访问拒绝:通常是AK不正确
+        code:-100,
+        msg:"Access denied"
+    },
+    UNDER_DEVELOPMENT:{//处于开发中
+        code:-201,
+        msg:"Under development"
+    },
+    DATABASE_CONNECT_FAIL:{
+        code:-500,
+        msg:"Database connect fail"
+    },
+    DATABASE_ERR:{
+        code:-501,
+        msg:"Database err"
+    },
+    DATABASE_NOT_FOUNT:{
+        code:-504,
+        msg:"Database not fount"
+    },
+    REQUEST_BUSY:{
+        code:-505,
+        msg:"Request busy!",
+    },
+    
+    // 用户相关
+    USER_NOT_EXISTS:{//用户不存在
+        code:-20404,
+        msg:"User not exists"
+    },
+    USER_ALREADY_EXISTS:{
+        code:-20405,
+        msg:"User already exists"
+    },
+    USER_ACCESS_DENIED:{//用户密码错误
+        code:-20500,
+        msg:"User access denied"
+    },
+    USER_ILLEAGAL_NAME:{
+        code:-20501,
+        msg:"User illeagal name"
+    },
+
+    // 短信相关业务
+    SMS_SEND_COOLING:{// 验证码冷却
+        code:-10500,
+        msg:"Sms send cooling"
+    },
+    SMS_SEND_FAIL:{// 发送失败
+        code:-10300,
+        msg:"Sms send fail"
+    },
+    SMS_CHECK_FAIL:{// 短信验证失败
+        code:-10501,
+        msg:"Sms check fail"
+    },
+
+    //权限相关
+    PERMISSION_DENIED:{//权限不足
+        code:-403,
+        msg:"Permission denied"
+    },
+}
+module.exports.BaseStdResponse = BaseStdResponse;

+ 56 - 0
apis/Captcha/ImageCaptcha.js

@@ -0,0 +1,56 @@
+const svgCaptcha = require('svg-captcha');
+const { v4: uuidv4 } = require('uuid');
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const Redis = require('../../plugin/DataBase/Redis'); 
+
+// 生成图片验证码
+class ImageCaptcha extends API {
+    constructor() {
+        super();
+
+        this.setMethod("GET");
+        this.setPath("/Captcha/ImageCaptcha");
+    }
+
+    async onRequest(req, res) {
+        const options = {
+            size: 4, // 4个字母
+            noise: 2, // 干扰线2条
+            color: true, 
+            background: "#666",
+        }
+        const captcha = svgCaptcha.create(options) //字母和数字随机验证码
+        let { text, data } = captcha
+
+        text = text.toLowerCase() 
+
+        data = Buffer.from(data).toString('base64');
+        // 构建Base64编码的Data URL
+        data = `data:image/svg+xml;base64,${data}`;
+
+        const id = uuidv4();
+
+        try {
+            await Redis.set(`captcha:${id}`, text, {
+                EX: 600
+            });
+        } catch (err) {
+            this.logger.error(`获取图片验证码失败!${err.stack}`);
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg:'获取图片验证码失败!'
+            })
+        }
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: {
+                img: data,
+                id
+            }
+        })
+    }
+}
+
+module.exports.ImageCaptcha = ImageCaptcha;

+ 80 - 0
apis/Captcha/SendEmail.js

@@ -0,0 +1,80 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const Redis = require('../../plugin/DataBase/Redis');
+const sendEmail = require('../../plugin/Email/Email');
+
+// 发送邮箱验证码
+class SendEmail extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/Captcha/SendEmail");
+    }
+
+    async onRequest(req, res) {
+        const { email, text, id, type } = req.body;
+
+        if ([email, text, id, type].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+            return;
+        }
+
+        try {
+            const code = await Redis.get(`captcha:${id}`);
+            if (!code || code != text.toLowerCase())
+                return res.json({
+                    ...BaseStdResponse.SMS_CHECK_FAIL,
+                    msg: '验证码输入错误或已过期'
+                })
+
+            await Redis.del(`captcha:${id}`);
+        } catch (err) {
+            this.logger.error(`验证图片验证码失败!${err.stack}`);
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '验证失败!'
+            })
+        }
+
+        let content;
+        switch (type) {
+            case 'register':
+                content = '您正在注册CTBU_CLUB账号,';
+                break;
+            case 'forget':
+                content = '您正在找回CTBU_CLUB账号,';
+                break;
+            case 'bind':
+                content = '您正在进行换绑邮箱操作,';
+                break;
+            default:
+                return res.json({
+                    ...BaseStdResponse.METHOD_NOT_EXIST
+                })
+        }
+
+        const code = Math.random().toFixed(6).slice(-6);
+        try {
+            await Redis.set(`email:${email}`, code, {
+                EX: 600
+            });
+            await sendEmail(email, '验证码', `${content}您的验证码为:${code}。此验证码10分钟内有效,请妥善保管您的验证码,非本人操作请忽略。`);
+        } catch (err) {
+            this.logger.error(`发送邮箱验证码失败!${err.stack}`);
+            return res.json({
+                ...BaseStdResponse.SMS_SEND_FAIL,
+                msg: '请检查邮箱格式后再试!'
+            })
+        }
+
+        res.json({
+            ...BaseStdResponse.OK
+        })
+    }
+}
+
+module.exports.SendEmail = SendEmail;

+ 92 - 0
apis/Data/IndexData.js

@@ -0,0 +1,92 @@
+const API = require("../../lib/API");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+
+class IndexData extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Admin/IndexData')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session } = req.query
+
+        if ([uuid, session].some(value => value === '' || value === null || value === undefined)) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            })
+        }
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = `
+                    SELECT
+                        (SELECT COUNT(*) FROM jw_account WHERE create_user = ?) AS accounts,
+                        (SELECT COUNT(*) FROM ic_rule) AS rules
+                `
+
+        try {
+            let r = await db.query(sql, [uuid])
+            if (!r)
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR
+                })
+            
+            let { accounts, rules } = r[0]
+
+            sql = `
+            SELECT 
+                a.id,
+                a.title,
+                a.\`describe\`,
+                a.views,
+                a.type,
+                a.time,
+                u.username AS author
+            FROM 
+                article a
+            LEFT JOIN 
+                users u 
+            ON 
+                a.author = u.uuid
+            WHERE 
+                a.state = 1 
+                AND a.type = 'news'
+            ORDER BY 
+                a.id DESC
+            LIMIT 9
+        `
+
+            r = await db.query(sql)
+            if(!r)
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR
+                })
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: {
+                    accounts,
+                    rules,
+                    article: r
+                }
+            })
+
+        } catch (err) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: `获取数据失败!${err.message || ''}`
+            })
+        }
+    }
+}
+
+module.exports.IndexData = IndexData;

+ 65 - 0
apis/QK/Rule/AddRule.js

@@ -0,0 +1,65 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class AddRule extends API {
+    constructor() {
+        super()
+
+        this.setPath('/QK/Rule')
+        this.setMethod('POST')
+    }
+
+    async onRequest(req, res) {
+        let {
+            uuid,
+            session,
+            id,
+            name,
+            account,
+            crouse,
+            state
+        } = req.body
+
+        if ([uuid, session, name, account, crouse].some(value => value === '' || value === null || value === undefined) || (loop && !day_of_week))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql, r
+        const time = new Date().getTime()
+
+        if (!id) {
+            sql = 'INSERT INTO qk_rule (\`name\`, create_user, create_time, account, crouse) VALUES (?, ?, ?, ?, ?)'
+            r = await db.query(sql, [name, uuid, time, account, crouse])
+        } else {
+            sql = 'UPDATE qk_rule SET \`name\` = ?, account = ?, crouse = ?, update_time = ?, \`state\` = ? WHERE id = ? AND create_user = ?'
+            r = await db.query(sql, [name, account, crouse, time, state, id, uuid])
+        }
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK
+                })
+            } else {
+                res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '添加规则失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`添加规则失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "添加规则失败!",
+            });
+        }
+    }
+}
+
+module.exports.AddRule = AddRule;

+ 49 - 0
apis/QK/Rule/DeleteRule.js

@@ -0,0 +1,49 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class DeleteRule extends API {
+    constructor() {
+        super();
+
+        this.setPath('/QK/Rule')
+        this.setMethod('DELETE')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.body
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'DELETE FROM qk_rule WHERE id = ? AND create_user = ?'
+        let r = await db.query(sql, [id, uuid])
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK
+                })
+            } else {
+                res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '删除规则失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`删除规则失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "删除规则失败!",
+            });
+        }
+    }
+}
+
+module.exports.DeleteRule = DeleteRule

+ 47 - 0
apis/QK/Rule/GetRule.js

@@ -0,0 +1,47 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class GetRule extends API {
+    constructor() {
+        super();
+
+        this.setPath('/QK/Rule')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let {
+            uuid,
+            session
+        } = req.query
+
+        if ([uuid, session].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'SELECT * FROM qk_rule WHERE create_user = ?'
+        let rows = await db.query(sql, [uuid])
+
+        if (!rows)
+            return res.json({
+                ...BaseStdResponse.MISSING_FILE,
+                msg: '获取规则列表失败!'
+            })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows
+        })
+    }
+}
+
+module.exports.GetRule = GetRule;

+ 96 - 0
apis/Upload/UploadPicture.js

@@ -0,0 +1,96 @@
+const API = require("../../lib/API");
+const { v4: uuidv4 } = require('uuid');
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const AccessControl = require("../../lib/AccessControl");
+const multer = require('multer');
+const path = require('path');
+const { url } = require("../../config.json")
+
+// 配置Multer的存储选项
+const storage = multer.diskStorage({
+    destination: (req, file, cb) => {
+        cb(null, 'uploads/');
+    },
+    filename: (req, file, cb) => {
+        const fileName = uuidv4();
+        const fileExtension = path.extname(file.originalname);
+        cb(null, `${fileName}${fileExtension}`);
+    }
+});
+
+// 限制文件类型
+const fileFilter = (req, file, cb) => {
+    // 只接受以下扩展名的图片文件
+    const allowedTypes = /jpeg|jpg|png|gif/;
+    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
+    const mimetype = allowedTypes.test(file.mimetype);
+
+    if (extname && mimetype) {
+        return cb(null, true);
+    } else {
+        cb(new Error('只允许上传图片文件 (jpeg, jpg, png, gif)'));
+    }
+};
+
+// 初始化Multer中间件
+const upload = multer({
+    storage: storage,
+    fileFilter: fileFilter,
+    limits: { fileSize: 3 * 1024 * 1024 } // 限制文件大小为3MB
+}).single('upload');
+
+class UploadAvatar extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/UploadPicture");
+    }
+
+    async onRequest(req, res) {
+        // 使用Multer中间件处理文件上传
+        upload(req, res, async (err) => {
+            if (err) {
+                this.logger.error(`图片上传失败!${err.stack || ''}`)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '图片上传失败!'
+                });
+            }
+
+            let { uuid, session } = req.body
+
+            if ([uuid, session].some(value => value === '' || value === null || value === undefined)) {
+                return res.json({
+                    ...BaseStdResponse.MISSING_PARAMETER,
+                    endpoint: 1513126
+                });
+            }
+
+            if (!await AccessControl.checkSession(uuid, session)) {
+                return res.status(401).json({
+                    ...BaseStdResponse.ACCESS_DENIED,
+                    endpoint: 481545
+                });
+            }
+
+            if (!req.file) {
+                return res.json({
+                    ...BaseStdResponse.MISSING_PARAMETER,
+                    msg: '请上传图片文件'
+                });
+            }
+
+            const picturePath = url + req.file.filename; // 获取文件路径
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: {
+                    picturePath
+                }
+            });
+        });
+    }
+}
+
+module.exports.UploadAvatar = UploadAvatar;

+ 106 - 0
apis/User/Admin/AddUser.js

@@ -0,0 +1,106 @@
+const { v4: uuidv4 } = require('uuid');
+const API = require("../../../lib/API");
+const bcryptjs = require('bcryptjs');
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+const db = require("../../../plugin/DataBase/db");
+
+// 添加用户
+class AddUser extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/User/AddUser");
+    }
+
+    CheckPassword(password) {
+        if (password.length < 8 || password.length > 16) {
+            return false;
+        }
+
+        const hasLetter = /[a-zA-Z]/.test(password);
+        const hasNumber = /\d/.test(password);
+
+        return hasLetter && hasNumber;
+    }
+
+    checkUsername(username) {
+        const regex = /^[\u4e00-\u9fa5A-Za-z0-9]{2,8}$/;
+        return regex.test(username);
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, username, password, permissions } = req.body;
+
+        if ([uuid, session, username, password].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+            return;
+        }
+
+        if (!this.checkUsername(username))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '用户名需在2到8位之间,且只能含有英文字母和汉字'
+            })
+
+        // password = atob(password);
+
+        if (!this.CheckPassword(password))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '密码需在8到16位之间,且包含字母和数字'
+            })
+
+        // TODO 检查是否存在权限
+        if (permissions.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '请选择用户权限'
+            })
+        
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            })
+
+        // 检查权限
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin"))
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                endpoint: 4815478,
+            })
+
+        let sql = 'SELECT username FROM users WHERE username = ?';
+        let UsernameRows = await db.query(sql, [username]);
+        if (UsernameRows.length > 0)
+            return res.json({
+                ...BaseStdResponse.USER_ALREADY_EXISTS,
+                msg: '用户名已被占用!'
+            })
+
+        uuid = uuidv4();
+        session = uuidv4();
+        const hashPassword = bcryptjs.hashSync(password, 10);
+
+        sql = 'INSERT INTO users (uuid, username, session, password, permission) VALUES (?, ?, ?, ?, ?)';
+        
+        let result = await db.query(sql, [uuid, username, session, hashPassword, JSON.stringify(permissions)]);
+
+        if (result && result.affectedRows > 0) {            
+            res.json({
+                ...BaseStdResponse.OK
+            })
+        } else {
+            res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '添加用户失败!' });
+        }
+    }
+}
+
+module.exports.AddUser = AddUser;

+ 80 - 0
apis/User/ChangePassword.js

@@ -0,0 +1,80 @@
+const API = require("../../lib/API");
+const db = require("../../plugin/DataBase/db");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const AccessControl = require("../../lib/AccessControl");
+const bcryptjs = require('bcryptjs');
+
+class ChangePassword extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/User/ChangePassword");
+    }
+
+    CheckPassword(password) {
+        if (password.length < 8 || password.length > 16) {
+            return false;
+        }
+
+        const hasLetter = /[a-zA-Z]/.test(password);
+        const hasNumber = /\d/.test(password);
+
+        return hasLetter && hasNumber;
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, oldpassword, password } = req.body;
+
+        if ([uuid, session, oldpassword, password].some(value => value === '' || value === null || value === undefined)) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        oldpassword = atob(oldpassword);
+        password = atob(password);
+
+        if (!this.CheckPassword(password))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '密码需在8到16位之间,且包含字母和数字'
+            })
+
+        let sql = 'SELECT email, password FROM users WHERE uuid = ?';
+        let rows = await db.query(sql, [uuid]);
+        if(!rows || rows.length === 0)
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR
+            })
+
+        if (!bcryptjs.compareSync(oldpassword, rows[0].password))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '密码错误!'
+            })
+
+        const hashPassword = bcryptjs.hashSync(password, 10);
+        sql = 'UPDATE users SET password = ? WHERE uuid = ?';
+        let result = await db.query(sql, [hashPassword, uuid]);
+
+        if (result && result.affectedRows > 0) {
+            res.json({
+                ...BaseStdResponse.OK
+            });
+        } else {
+            res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '操作失败!' });
+        }
+    }
+}
+
+module.exports.ChangePassword = ChangePassword;

+ 65 - 0
apis/User/ChangeUsername.js

@@ -0,0 +1,65 @@
+const API = require("../../lib/API");
+const db = require("../../plugin/DataBase/db");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const AccessControl = require("../../lib/AccessControl");
+
+class ChangeUsername extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/User/ChangeUsername");
+    }
+
+    checkUsername(username) {
+        const regex = /^[\u4e00-\u9fa5A-Za-z0-9]{4,8}$/;
+        return regex.test(username);
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, username } = req.body;
+
+        if ([uuid, session, username].some(value => value === '' || value === null || value === undefined)) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+        }
+
+        if (!this.checkUsername(username))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '用户名需在4到8位之间,且只能含有英文字母和汉字'
+            })
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        let sql = 'SELECT username FROM users WHERE username = ?';
+        let UserRows = await db.query(sql, [username]);
+        if (UserRows.length > 0)
+            return res.json({
+                ...BaseStdResponse.USER_ALREADY_EXISTS,
+                msg: '该用户名已被占用!'
+            })
+
+        sql = 'UPDATE users SET username = ? WHERE uuid = ?';
+        let result = await db.query(sql, [username, uuid]);
+        if (!result || result.affectedRows !== 1)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '更新用户名失败'
+            });
+
+        res.json({
+            ...BaseStdResponse.OK
+        });
+    }
+}
+
+module.exports.ChangeUsername = ChangeUsername;

+ 47 - 0
apis/User/GetPermissions.js

@@ -0,0 +1,47 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const AccessControl = require("../../lib/AccessControl");
+
+// 获取用户权限
+class GetPermissions extends API {
+    constructor() {
+        super();
+
+        this.setMethod("GET");
+        this.setPath("/User/GetPermissions");
+    }
+
+    async onRequest(req, res) {
+        let {
+            uuid,
+            session
+        } = req.query;
+
+        // 检查必需的参数是否缺失
+        if ([uuid, session].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+            return;
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+            return;
+        }
+
+        let permission = await AccessControl.getPermission(uuid);
+
+        res.json({
+            ...BaseStdResponse.OK,
+            roles: permission
+        });
+    }
+}
+
+module.exports.GetPermissions = GetPermissions;

+ 80 - 0
apis/User/Login.js

@@ -0,0 +1,80 @@
+const { v4: uuidv4 } = require('uuid');
+const API = require("../../lib/API");
+const bcryptjs = require('bcryptjs');
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const Redis = require('../../plugin/DataBase/Redis');
+
+// 用户登录
+class Login extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/User/Login");
+    }
+
+    async onRequest(req, res) {
+        let { username, password, captcha, id } = req.body;
+
+        if ([username, password, captcha, id].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+            return;
+        }
+
+        password = atob(password);
+
+        try {
+            const code = await Redis.get(`captcha:${id}`);
+            if (!code || code != captcha.toLowerCase())
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '验证码错误或已过期!'
+                })
+            Redis.del(`captcha:${id}`);
+        } catch (err) {
+            this.logger.error(`验证图片验证码失败!${err.stack}`);
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '验证失败!'
+            })
+        }
+
+        let sql = 'SELECT * FROM users WHERE username = ?';
+        let rows = await db.query(sql, [username]);
+
+        if (!rows || rows.length !== 1 || !bcryptjs.compareSync(password, rows[0].password))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '用户名或密码错误'
+            })
+
+        const session = uuidv4();
+
+        sql = 'UPDATE users SET session = ? WHERE id = ?';
+        let result = await db.query(sql, [session, rows[0].id]);
+
+        if (result && result.affectedRows > 0) {
+            res.json({
+                ...BaseStdResponse.OK,
+                data: {
+                    uuid: rows[0].uuid,
+                    username: rows[0].username,
+                    session,
+                    roles: rows[0].permission || [],
+                    vip: rows[0].vip,
+                    ic_count: rows[0].ic_count,
+                    crouse_count: rows[0].crouse_count,
+                    realname: rows[0].realname
+                }
+            });
+        } else {
+            res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '登录失败!' });
+        }
+    }
+}
+
+module.exports.Login = Login;

+ 113 - 0
apis/User/Register.js

@@ -0,0 +1,113 @@
+const { v4: uuidv4 } = require('uuid');
+const API = require("../../lib/API");
+const bcryptjs = require('bcryptjs');
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const Redis = require('../../plugin/DataBase/Redis');
+const sendEmail = require('../../plugin/Email/Email');
+
+// 用户注册
+class Register extends API {
+    constructor() {
+        super();
+
+        this.setMethod("POST");
+        this.setPath("/User/Register");
+    }
+
+    CheckPassword(password) {
+        if (password.length < 8 || password.length > 16) {
+            return false;
+        }
+
+        const hasLetter = /[a-zA-Z]/.test(password);
+        const hasNumber = /\d/.test(password);
+
+        return hasLetter && hasNumber;
+    }
+
+    checkUsername(username) {
+        const regex = /^[\u4e00-\u9fa5A-Za-z0-9]{4,12}$/;
+        return regex.test(username);
+    }
+
+    async onRequest(req, res) {
+        let { username, email, code, password } = req.body;
+
+        if ([username, email, code, password].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            });
+            return;
+        }
+
+        if(!this.checkUsername(username))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '用户名需在4到8位之间,且只能含有英文字母和汉字'
+            })
+
+        password = atob(password);
+
+        if (!this.CheckPassword(password))
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '密码需在8到16位之间,且包含字母和数字'
+            })
+
+        try {
+            const VerifyCode = await Redis.get(`email:${email}`);
+            if (!VerifyCode || VerifyCode != code)
+                return res.json({
+                    ...BaseStdResponse.SMS_CHECK_FAIL,
+                    msg: '邮箱验证码输入错误或已过期'
+                })
+
+        } catch (err) {
+            this.logger.error(`验证邮箱验证码失败!${err.stack}`);
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '验证失败!'
+            })
+        }
+
+        let sql = 'SELECT username FROM users WHERE username = ?';
+        let UsernameRows = await db.query(sql, [username]);
+        if (UsernameRows.length > 0)
+            return res.json({
+                ...BaseStdResponse.USER_ALREADY_EXISTS,
+                msg: '用户名已被占用!'
+            })
+
+        sql = 'SELECT email FROM users WHERE email = ?';
+        let EmailRows = await db.query(sql, [email]);
+        if (EmailRows.length > 0)
+            return res.json({
+                ...BaseStdResponse.USER_ALREADY_EXISTS,
+                msg: '该邮箱已被注册!'
+            })
+
+        const uuid = uuidv4()
+        const session = uuidv4()
+        const hashPassword = bcryptjs.hashSync(password, 10);
+        const time = new Date().getTime()
+
+        sql = 'INSERT INTO users (uuid, username, session, email, password, registTime) VALUES (?, ?, ?, ?, ?, ?)';
+        let result = await db.query(sql, [uuid, username, session, email, hashPassword, time]);
+
+        if (result && result.affectedRows > 0) {
+            // 注册成功后删除邮箱对应的验证码 避免注册失败后重复获取
+            await Redis.del(`email:${email}`);
+
+            res.json({
+                ...BaseStdResponse.OK
+            });
+            await sendEmail(email, '账号注册成功', `您已成功注册CTBU_CLUB账号,用户名${username},注册时间:${new Date().toLocaleString()}`);
+        } else {
+            res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '注册失败!'});
+        }
+    }
+}
+
+module.exports.Register = Register;

+ 5 - 0
index.js

@@ -0,0 +1,5 @@
+const SERVER = require('./lib/Server');
+
+const server = new SERVER();
+
+server.start();

+ 38 - 0
lib/API.js

@@ -0,0 +1,38 @@
+const express = require('express');
+const Logger = require('./Logger');
+const path = require('path');
+
+class API {
+    constructor() {
+        this.router = express.Router();
+        this.namespace = '';
+        this.path = '';
+        this.method = 'get';
+
+        this.logger = new Logger(path.join(__dirname, '../logs/API.log'), 'INFO');
+    }
+
+    setPath(path) {
+        this.path = path;
+    }
+
+    setMethod(method) {
+        this.method = method.toLowerCase();
+    }
+
+    getRouter() {
+        return this.router;
+    }
+
+    async onRequest(req, res) {
+        throw new Error('onRequest方法未实现');
+    }
+
+    setupRoute() {
+        this.router[this.method](this.path, async (req, res) => {
+            await this.onRequest(req, res);
+        });
+    }
+}
+
+module.exports = API;

+ 26 - 0
lib/AccessControl.js

@@ -0,0 +1,26 @@
+const db = require('../plugin/DataBase/db');
+
+class AccessControl {
+    async checkSession(uuid, session) {
+        const sql = 'SELECT uuid, session FROM users WHERE uuid = ? AND session = ?';
+        const rows = await db.query(sql, [uuid, session]);
+        return rows.length > 0;
+    }
+
+    async getPermission(uuid) {
+        const sql = 'SELECT permission FROM users WHERE uuid = ?';
+        const rows = await db.query(sql, [uuid]);
+
+        return rows[0].permission || [];
+    }
+
+    async checkJwAccount(uuid, username) {
+        const sql = 'SELECT password FROM jw_account WHERE create_user = ? AND state = 1 AND username = ?'
+        const rows = await db.query(sql, [uuid, username]);
+        if (!rows || rows.length !== 1 || !rows[0].password)
+            return false
+        return rows[0].password;
+    }
+}
+
+module.exports = new AccessControl();

+ 65 - 0
lib/Logger.js

@@ -0,0 +1,65 @@
+const fs = require('fs');
+const path = require('path');
+const chalk = require('chalk');
+
+class Logger {
+    constructor(logFilePath = null, level = 'INFO') {
+        this.logLevels = ['INFO', 'WARN', 'ERROR'];
+        this.level = level.toUpperCase(); //不区分大小写 可以偷懒
+        this.logFilePath = logFilePath ? path.resolve(logFilePath) : null;
+
+        if (this.logFilePath) {
+            // 确保日志目录存在
+            const dir = path.dirname(this.logFilePath);
+            if (!fs.existsSync(dir)) {
+                fs.mkdirSync(dir, { recursive: true });
+            }
+        }
+    }
+
+    log(level, message) {
+        level = level.toUpperCase();
+        if (this.logLevels.indexOf(level) >= this.logLevels.indexOf(this.level)) {
+            const timestamp = new Date().toLocaleString();
+            const logMessage = `${timestamp} [${level}] ${message}`;
+
+            // 控制台颜色
+            let coloredMessage;
+            switch (level) {
+                case 'INFO':
+                    coloredMessage = chalk.white(logMessage);
+                    break;
+                case 'WARN':
+                    coloredMessage = chalk.yellow(logMessage);
+                    break;
+                case 'ERROR':
+                    coloredMessage = chalk.red(logMessage);
+                    break;
+                default:
+                    coloredMessage = logMessage;
+            }
+
+            console.log(coloredMessage);
+
+            if (this.logFilePath) {
+                fs.appendFile(this.logFilePath, `${logMessage}\n`, (err) => {
+                    if (err) throw err;
+                });
+            }
+        }
+    }
+
+    info(message) {
+        this.log('INFO', message);
+    }
+
+    warn(message) {
+        this.log('WARN', message);
+    }
+
+    error(message) {
+        this.log('ERROR', message);
+    }
+}
+
+module.exports = Logger;

+ 95 - 0
lib/Server.js

@@ -0,0 +1,95 @@
+const express = require('express');
+const cors = require('cors');
+const path = require('path');
+const fs = require('fs');
+const config = require('../config.json');
+const Logger = require('./Logger');
+const MySQL = require('../plugin/DataBase/MySQL');
+
+class SERVER {
+    constructor() {
+        this.app = express();
+        this.port = config.port || 3000;
+        this.apiDirectory = path.join(__dirname, '../apis'); // API 文件存放目录
+
+        this.logger = new Logger(path.join(__dirname, '../logs/Server.log'), 'INFO');
+
+        // 解析 JSON 请求体
+        this.app.use(express.json());
+
+        //解决cors跨域
+        this.app.use(cors());
+
+        //使用静态资源
+        this.app.use(express.static('./uploads'));
+
+        // 初始化数据库连接
+        this.db = new MySQL();
+
+        // 加载 API 路由
+        this.loadAPIs(this.apiDirectory);
+    }
+
+    // 测试数据库连接
+    async initDB() {
+        try {
+            this.logger.info('正在测试数据库连接');
+            await this.db.connect();
+            await this.db.close();
+        } catch (error) {
+            this.logger.error(`数据库连接失败: ${error.stack}`);
+            process.exit(1);
+        }
+    }
+
+    loadAPIs(directory) {
+        const items = fs.readdirSync(directory);
+
+        items.forEach(item => {
+            const itemPath = path.join(directory, item);
+            const stats = fs.statSync(itemPath);
+
+            if (stats.isDirectory()) {
+                // 如果是目录,递归调用
+                this.loadAPIs(itemPath);
+            } else if (stats.isFile() && itemPath.endsWith('.js')) {
+                // 如果是文件且是 JavaScript 文件
+                this.loadAPIFile(itemPath);
+            }
+        });
+    }
+
+    // 加载单个 API 文件
+    loadAPIFile(filePath) {
+        try {
+            const APIClass = require(filePath);
+
+            for (const key in APIClass) {
+                if (APIClass.hasOwnProperty(key)) {
+                    const apiInstance = new APIClass[key]();
+                    apiInstance.setupRoute();
+                    this.app.use('/', apiInstance.getRouter());
+                    this.logger.info(`已加载API:${apiInstance.path} 类型:${apiInstance.method}`);
+                }
+            }
+        } catch (error) {
+            this.logger.error(`加载API文件失败: ${filePath},错误: ${error.stack}`);
+        }
+    }
+
+    start() {
+        this.logger.info('============正在启动服务器============');
+
+        // 初始化数据库连接
+        this.initDB().then(() => {
+            this.app.listen(this.port, () => {
+                this.logger.info(`==========服务器正在 ${this.port} 端口上运行==========`);
+            });
+        }).catch(err => {
+            this.logger.error(`启动服务器失败: ${err.message}`);
+            process.exit(1); // 启动失败时退出进程
+        });
+    }
+}
+
+module.exports = SERVER;

+ 19 - 0
lib/UserInfoCache.js

@@ -0,0 +1,19 @@
+const db = require('../plugin/DataBase/db');
+
+class UserInfoCache {
+    async getUserByUuid(uuid) {
+        const sql = 'SELECT uuid, username, avatar FROM users WHERE uuid = ?';
+        const rows = await db.query(sql, [uuid]);
+
+        return rows[0] ? rows[0] : null;
+    }
+
+    async getUuidByName(name) {
+        const sql = 'SELECT uuid, username, avatar FROM users WHERE name = ?';
+        const rows = await db.query(sql, [name]);
+
+        return rows[0] ? rows[0] : null;
+    }
+}
+
+module.exports = new UserInfoCache();

+ 25 - 0
package.json

@@ -0,0 +1,25 @@
+{
+  "name": "gitnexus-backend",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "start": "node index.js"
+  },
+  "author": "thc",
+  "dependencies": {
+    "axios": "^1.7.4",
+    "bcryptjs": "^2.4.3",
+    "body-parser": "^1.20.2",
+    "chalk": "^4.1.2",
+    "cors": "^2.8.5",
+    "express": "^4.21.1",
+    "multer": "^1.4.5-lts.1",
+    "mysql2": "^3.11.0",
+    "nodemailer": "^6.9.14",
+    "puppeteer": "^23.9.0",
+    "redis": "^4.7.0",
+    "svg-captcha": "^1.4.0",
+    "uuid": "^11.0.3"
+  }
+}

+ 192 - 0
plugin/Browser/IcCookie.js

@@ -0,0 +1,192 @@
+const puppeteer = require('puppeteer');
+const getCaptcha = require('../Captcha/tesseract');
+const axios = require('axios');
+
+class icCookie {
+    constructor() {
+        console.log('启动浏览器服务')
+        this.browser = 0
+    }
+
+    async getLoginUrl() {
+        return new Promise(async (resolve, reject) => {
+            const url1 = 'https://ic.ctbu.edu.cn/ic-web/auth/address?finalAddress=https:%2F%2Fic.ctbu.edu.cn&errPageUrl=https:%2F%2Fic.ctbu.edu.cn%2F%23%2Ferror&manager=false&consoleType=16';
+
+            try {
+                const res1 = await axios.get(url1, {
+                    maxRedirects: 5, // 增加最大重定向次数
+                    validateStatus: function (status) {
+                        return (status >= 200 && status < 300) || status === 302; // 接受 302 重定向
+                    }
+                });
+
+                let redirectUrl = res1.data.data || res1.headers.location;
+
+                if (!redirectUrl) {
+                    reject('无法获取登录 URL');
+                    return;
+                }
+
+                resolve(redirectUrl);
+            } catch (error) {
+                if (error.response) {
+                    const redirectUrl = error.response.headers.location;
+                    if (redirectUrl) {
+                        resolve(redirectUrl);
+                    } else {
+                        reject(error);
+                    }
+                } else {
+                    reject(error);
+                }
+            }
+        });
+    }
+
+   async refresh(page) {
+        page.evaluate(async () => {
+            // window.location.reload();
+            await refreshCode();
+        });
+    }
+
+    async getAuthcode(page) {
+        return new Promise(async (resolve, reject) => {
+            try {
+                let base64Image = '';
+                const url = await this.getLoginUrl();
+                page.goto(url);
+                page.on('response', async (response) => {
+                    if (response.url().includes('kaptcha?time=')) {
+                        const buffer = await response.buffer();
+                        base64Image = buffer.toString('base64');
+
+                        const authcodeResult = await getCaptcha(base64Image);
+                        if (!authcodeResult.success) {
+                            return await this.refresh(page);
+                        }
+
+                        const authcode = authcodeResult.msg;
+                        resolve(authcode);
+                    }
+                });
+
+                // 超时处理
+                setTimeout(async () => {
+                    if (base64Image === '') {
+                        await this.refresh(page);
+                    }
+                }, 5000);
+            } catch (error) {
+                console.log(error.stack)
+                reject(error);
+            }
+        });
+    }
+
+    async tryLogin(page, username, password, authcode) {
+        return new Promise(async (resolve, reject) => {
+            try {
+                await page.waitForSelector('#username');
+                await page.waitForSelector('#authcode');
+                await page.waitForSelector('#password');
+                await page.waitForSelector('#fm1');
+
+                await page.evaluate(async (username, password, authcode) => {
+                    document.querySelector("#username").value = username;
+                    document.querySelector("#authcode").value = authcode;
+
+                    function checkForm() {
+                        var key = new RSAUtils.getKeyPair(public_exponent, "", Modulus);
+                        var reversedPwd = password.split("").reverse().join("");
+                        var encrypedPwd = RSAUtils.encryptedString(key, reversedPwd);
+
+                        document.querySelector("#password").value = encrypedPwd;
+                        document.querySelector("#fm1").submit();
+
+                        return { public_exponent, Modulus, encrypedPwd, authcode };
+                    }
+                    return checkForm();
+                }, username, password, authcode);
+
+                // 响应处理
+                page.on('response', async (response) => {
+                    if (response.status() >= 300 && response.status() < 400) {
+                        return resolve();
+                    }
+
+                    if (response.url().includes('login?v=')) {
+                        const responseText = await response.text();
+                        if (responseText.includes('验证码输入有误') || responseText.includes('必须录入验证码')) {
+                            reject({ code: -603, msg: '验证码输入有误' });
+                        } else if (responseText.includes('用户名或密码错误')) {
+                            reject({ code: -600, msg: '用户名或密码错误' });
+                        } else if (responseText.includes('锁定')) {
+                            reject({ code: -601, msg: '账户被锁定' });
+                        } else {
+                            reject({ code: -604, msg: '未知错误' });
+                        }
+                    }
+                });
+            } catch (error) {
+                reject({ code: -602, msg: error.message });
+            }
+        });
+    }
+
+    async loading(username, password) {
+        if (this.browser.length >= 5) {
+            return { code: -666, msg: '并发数量达到上限,请稍后再试' };
+        }
+
+        let browser;
+        this.browser += 1
+        try {
+            browser = await puppeteer.launch({
+                headless: true,
+                timeout: 60000
+            });
+
+            const page = await browser.newPage();
+
+            page.setDefaultTimeout(30000);
+
+            const authcode = await this.getAuthcode(page);
+
+            await this.tryLogin(page, username, password, authcode);
+            let cookie = await this.getCookie(page);
+            
+            cookie = cookie.split(';')[0]
+
+            return { code: 0, msg: cookie };
+        } catch (error) {
+            console.error('登录过程错误:', error);
+            return { code: -600, msg: error.message };
+        } finally {
+            this.browser -= 1
+            if (browser) {
+                await browser.close();
+            }
+        }
+    }
+
+    async getCookie (page) {
+        return new Promise((resolve, reject) => {
+            page.on('response', async response => {
+                if (response.url().includes('https://ic.ctbu.edu.cn/ic-web//auth/token?uuid=')) {
+                    // 获取响应头中的 Set-Cookie
+                    const cookies = response.headers()['set-cookie'];
+
+                    if (cookies) {
+                        resolve(cookies); 
+                    } else {
+                        reject('No cookies found in response headers');
+                    }
+                }
+            });
+        });
+    };
+}
+
+let IcCookie = new icCookie()
+module.exports = IcCookie;

+ 62 - 0
plugin/DataBase/MySQL.js

@@ -0,0 +1,62 @@
+const mysql = require('mysql2/promise');
+const path = require('path');
+const config = require('../../config.json');
+const Logger = require('../../lib/Logger');
+
+class MySQL {
+    constructor() {
+        this.config = config.database;
+        this.connection = null;
+        this.logger = new Logger(path.join(__dirname, '../../logs/MySQL.log'), 'INFO');
+    }
+
+    async connect() {
+        if (!this.connection) {
+            try {
+                this.logger.info('正在连接数据库');
+                this.connection = await mysql.createConnection(this.config);
+                this.logger.info('已连接到数据库');
+                return this.connection;
+            } catch (error) {
+                this.logger.error('连接数据库失败:', error.message);
+                throw error;
+            }
+        } else {
+            try {
+                // 检查当前连接的有效性
+                await this.connection.ping();
+                return this.connection;
+            } catch (error) {
+                this.logger.warn('数据库连接失效,尝试重新连接...');
+                this.connection = null; // 清除当前连接
+                return await this.connect(); // 递归调用 connect() 重新连接
+            }
+        }
+    }
+
+    async query(sql, params = []) {
+        try {
+            // 确保在执行查询之前连接已建立并有效
+            await this.connect();
+            const [rows] = await this.connection.execute(sql, params);
+            return rows;
+        } catch (error) {
+            this.logger.error(`执行SQL语句时出错:${error.stack}`);
+        }
+    }
+
+    async close() {
+        if (this.connection) {
+            try {
+                await this.connection.end();
+                this.logger.info('已关闭与数据库的连接');
+                this.connection = null;
+            } catch (error) {
+                this.logger.error('关闭与数据库的连接时出错:', error);
+                throw error;
+            }
+        }
+    }
+}
+
+module.exports = MySQL;

+ 50 - 0
plugin/DataBase/Redis.js

@@ -0,0 +1,50 @@
+const redis = require('redis');
+const config = require('../../config.json');
+const Logger = require('../../lib/Logger');
+const path = require('path');
+
+class RedisClient {
+    constructor() {
+        // 创建 Redis 客户端实例
+        this.client = redis.createClient({
+            host: config.redis.host,
+            port: config.redis.port,
+            password: config.redis.password || null
+        });
+
+        this.logger = new Logger(path.join(__dirname, '../../logs/Redis.log'), 'INFO');
+
+        // 处理连接错误
+        this.client.on('error', (err) => {
+            this.logger.error(`Redis连接出错:${err.stack}`);
+        });
+
+        // 确保连接成功
+        this.client.on('connect', () => {
+            this.logger.info('Redis连接成功!');
+        });
+
+        // 在连接建立时标记为已连接状态
+        this.client.on('ready', () => {
+            this.logger.info('Redis客户端已就绪!');
+        });
+
+        // 在客户端关闭时处理事件
+        this.client.on('end', () => {
+            this.logger.warn('Redis客户端连接已关闭!');
+        });
+    }
+
+    // 获取 Redis 客户端实例
+    getClient() {
+        if (!this.client.isOpen) {
+            this.client.connect().catch((err) => {
+                this.logger.error(`重新连接Redis时出错:${err.stack}`);
+            });
+        }
+        return this.client;
+    }
+}
+
+const Redis = new RedisClient().getClient();
+module.exports = Redis;

+ 5 - 0
plugin/DataBase/db.js

@@ -0,0 +1,5 @@
+const MySQL = require('./MySQL');
+
+const db = new MySQL();
+
+module.exports = db;

+ 42 - 0
plugin/Email/Email.js

@@ -0,0 +1,42 @@
+const nodemailer = require('nodemailer');
+const config = require('../../config.json');
+const path = require('path');
+const Logger = require('../../lib/Logger');
+
+const logger = new Logger(path.join(__dirname, '../../logs/Email.log'), 'INFO');
+
+const transporter = nodemailer.createTransport({
+    host: config.email.host,
+    port: config.email.port,
+    secure: config.email.secure,
+    auth: {
+        user: config.email.user,
+        pass: config.email.password
+    }
+})
+
+async function sendEmail(email, subject, content) {
+    return new Promise((resolve, reject) => {
+        const mail = {
+            from: `CTBU_CLUB <${transporter.options.auth.user}>`,
+            to: email,
+            subject: subject,
+            text: content
+        }
+        try {
+            transporter.sendMail(mail, (error) => {
+                if (error) {
+                    logger.error('邮件发送失败:', error);
+                    reject(error);
+                } else {
+                    resolve();
+                }
+            })
+        } catch (error) {
+            logger.error('邮件发送失败:', error);
+            reject(error);
+        }
+    })
+}
+
+module.exports = sendEmail;

+ 27 - 0
plugin/Kaptcha/kaptcha.js

@@ -0,0 +1,27 @@
+const axios = require('axios');
+const apiUrl = 'https://api.kuaishibie.cn/predict';
+
+const getKaptcha = async (base64data) => {
+    try {
+        const response = await axios.post(apiUrl, {
+            username: 'Pchen0', // 用户名
+            password: 'Yx3ud937', // 密码
+            typeid: '3',
+            image: base64data
+        });
+
+        const data = response.data;
+        if (data.success) {
+            const { result } = data.data;
+            return result;
+        } else {
+            console.error(data.message);
+            throw new Error(data.message);
+        }
+    } catch (error) {
+        console.error('请求失败:', error.message);
+        throw error;
+    }
+};
+
+module.exports = getKaptcha;

+ 696 - 0
plugin/RSAUtils/RSAUtils.js

@@ -0,0 +1,696 @@
+/*
+ * RSA, a suite of routines for performing RSA public-key computations in JavaScript.
+ * Copyright 1998-2005 David Shapiro.
+ * Dave Shapiro
+ * dave@ohdave.com
+ * changed by Fuchun, 2010-05-06
+ * fcrpg2005@gmail.com
+ */
+/*
+ * Modified by +v then, 2018-03-28
+ */
+
+var RSAUtils = {}
+
+// var biRadixBase = 2
+var biRadixBits = 16
+var bitsPerDigit = biRadixBits
+var biRadix = 1 << 16 // = 2^16 = 65536
+var biHalfRadix = biRadix >>> 1
+var biRadixSquared = biRadix * biRadix
+var maxDigitVal = biRadix - 1
+// var maxInteger = 9999999999999998
+
+// maxDigits:
+// Change this to accommodate your largest number size. Use setMaxDigits()
+// to change it!
+//
+// In general, if you're working with numbers of size N bits, you'll need 2*N
+// bits of storage. Each digit holds 16 bits. So, a 1024-bit key will need
+//
+// 1024 * 2 / 16 = 128 digits of storage.
+//
+var maxDigits
+var ZERO_ARRAY
+var bigZero, bigOne
+
+var BigInt = function (flag) {
+    if (typeof flag === 'boolean' && flag === true) {
+        this.digits = null
+    } else {
+        this.digits = ZERO_ARRAY.slice(0)
+    }
+    this.isNeg = false
+}
+
+RSAUtils.setMaxDigits = function (value) {
+    maxDigits = value
+    ZERO_ARRAY = new Array(maxDigits)
+    for (var iza = 0; iza < ZERO_ARRAY.length; iza++) {
+        ZERO_ARRAY[iza] = 0
+    }
+    bigZero = new BigInt()
+    bigOne = new BigInt()
+    bigOne.digits[0] = 1
+}
+RSAUtils.setMaxDigits(20)
+
+// The maximum number of digits in base 10 you can convert to an
+// integer without JavaScript throwing up on you.
+var dpl10 = 15
+
+RSAUtils.biFromNumber = function (i) {
+    var result = new BigInt()
+    result.isNeg = i < 0
+    i = Math.abs(i)
+    var j = 0
+    while (i > 0) {
+        result.digits[j++] = i & maxDigitVal
+        i = Math.floor(i / biRadix)
+    }
+    return result
+}
+
+// lr10 = 10 ^ dpl10
+var lr10 = RSAUtils.biFromNumber(1000000000000000)
+
+RSAUtils.biFromDecimal = function (s) {
+    var isNeg = s.charAt(0) === '-'
+    var i = isNeg ? 1 : 0
+    var result
+    // Skip leading zeros.
+    while (i < s.length && s.charAt(i) === '0') ++i
+    if (i === s.length) {
+        result = new BigInt()
+    } else {
+        var digitCount = s.length - i
+        var fgl = digitCount % dpl10
+        if (fgl === 0) fgl = dpl10
+        result = RSAUtils.biFromNumber(Number(s.substr(i, fgl)))
+        i += fgl
+        while (i < s.length) {
+            result = RSAUtils.biAdd(RSAUtils.biMultiply(result, lr10),
+                RSAUtils.biFromNumber(Number(s.substr(i, dpl10))))
+            i += dpl10
+        }
+        result.isNeg = isNeg
+    }
+    return result
+}
+
+RSAUtils.biCopy = function (bi) {
+    var result = new BigInt(true)
+    result.digits = bi.digits.slice(0)
+    result.isNeg = bi.isNeg
+    return result
+}
+
+RSAUtils.reverseStr = function (s) {
+    var result = ''
+    for (var i = s.length - 1; i > -1; --i) {
+        result += s.charAt(i)
+    }
+    return result
+}
+
+var hexatrigesimalToChar = [
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+    'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+    'u', 'v', 'w', 'x', 'y', 'z'
+]
+
+RSAUtils.biToString = function (x, radix) { // 2 <= radix <= 36
+    var b = new BigInt()
+    b.digits[0] = radix
+    var qr = RSAUtils.biDivideModulo(x, b)
+    var result = hexatrigesimalToChar[qr[1].digits[0]]
+    while (RSAUtils.biCompare(qr[0], bigZero) === 1) {
+        qr = RSAUtils.biDivideModulo(qr[0], b)
+        // digit = qr[1].digits[0]
+        result += hexatrigesimalToChar[qr[1].digits[0]]
+    }
+    return (x.isNeg ? '-' : '') + RSAUtils.reverseStr(result)
+}
+
+RSAUtils.biToDecimal = function (x) {
+    var b = new BigInt()
+    b.digits[0] = 10
+    var qr = RSAUtils.biDivideModulo(x, b)
+    var result = String(qr[1].digits[0])
+    while (RSAUtils.biCompare(qr[0], bigZero) === 1) {
+        qr = RSAUtils.biDivideModulo(qr[0], b)
+        result += String(qr[1].digits[0])
+    }
+    return (x.isNeg ? '-' : '') + RSAUtils.reverseStr(result)
+}
+
+var hexToChar = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+    'a', 'b', 'c', 'd', 'e', 'f']
+
+RSAUtils.digitToHex = function (n) {
+    var mask = 0xf
+    var result = ''
+    for (var i = 0; i < 4; ++i) {
+        result += hexToChar[n & mask]
+        n >>>= 4
+    }
+    return RSAUtils.reverseStr(result)
+}
+
+RSAUtils.biToHex = function (x) {
+    var result = ''
+    // var n = RSAUtils.biHighIndex(x)
+    for (var i = RSAUtils.biHighIndex(x); i > -1; --i) {
+        result += RSAUtils.digitToHex(x.digits[i])
+    }
+    return result
+}
+
+RSAUtils.charToHex = function (c) {
+    var ZERO = 48
+    var NINE = ZERO + 9
+    var littleA = 97
+    var littleZ = littleA + 25
+    var bigA = 65
+    var bigZ = 65 + 25
+    var result
+
+    if (c >= ZERO && c <= NINE) {
+        result = c - ZERO
+    } else if (c >= bigA && c <= bigZ) {
+        result = 10 + c - bigA
+    } else if (c >= littleA && c <= littleZ) {
+        result = 10 + c - littleA
+    } else {
+        result = 0
+    }
+    return result
+}
+
+RSAUtils.hexToDigit = function (s) {
+    var result = 0
+    var sl = Math.min(s.length, 4)
+    for (var i = 0; i < sl; ++i) {
+        result <<= 4
+        result |= RSAUtils.charToHex(s.charCodeAt(i))
+    }
+    return result
+}
+
+RSAUtils.biFromHex = function (s) {
+    var result = new BigInt()
+    var sl = s.length
+    for (var i = sl, j = 0; i > 0; i -= 4, ++j) {
+        result.digits[j] = RSAUtils.hexToDigit(s.substr(Math.max(i - 4, 0), Math.min(i, 4)))
+    }
+    return result
+}
+
+RSAUtils.biFromString = function (s, radix) {
+    var isNeg = s.charAt(0) === '-'
+    var istop = isNeg ? 1 : 0
+    var result = new BigInt()
+    var place = new BigInt()
+    place.digits[0] = 1 // radix^0
+    for (var i = s.length - 1; i >= istop; i--) {
+        var c = s.charCodeAt(i)
+        var digit = RSAUtils.charToHex(c)
+        var biDigit = RSAUtils.biMultiplyDigit(place, digit)
+        result = RSAUtils.biAdd(result, biDigit)
+        place = RSAUtils.biMultiplyDigit(place, radix)
+    }
+    result.isNeg = isNeg
+    return result
+}
+
+RSAUtils.biDump = function (b) {
+    return (b.isNeg ? '-' : '') + b.digits.join(' ')
+}
+
+RSAUtils.biAdd = function (x, y) {
+    var result
+
+    if (x.isNeg !== y.isNeg) {
+        y.isNeg = !y.isNeg
+        result = RSAUtils.biSubtract(x, y)
+        y.isNeg = !y.isNeg
+    } else {
+        result = new BigInt()
+        var c = 0
+        var n
+        for (var i = 0; i < x.digits.length; ++i) {
+            n = x.digits[i] + y.digits[i] + c
+            result.digits[i] = n % biRadix
+            c = Number(n >= biRadix)
+        }
+        result.isNeg = x.isNeg
+    }
+    return result
+}
+
+RSAUtils.biSubtract = function (x, y) {
+    var result
+    if (x.isNeg !== y.isNeg) {
+        y.isNeg = !y.isNeg
+        result = RSAUtils.biAdd(x, y)
+        y.isNeg = !y.isNeg
+    } else {
+        result = new BigInt()
+        var n, c
+        c = 0
+        for (var i = 0; i < x.digits.length; ++i) {
+            n = x.digits[i] - y.digits[i] + c
+            result.digits[i] = n % biRadix
+            // Stupid non-conforming modulus operation.
+            if (result.digits[i] < 0) result.digits[i] += biRadix
+            c = 0 - Number(n < 0)
+        }
+        // Fix up the negative sign, if any.
+        if (c === -1) {
+            c = 0
+            for (i = 0; i < x.digits.length; ++i) {
+                n = 0 - result.digits[i] + c
+                result.digits[i] = n % biRadix
+                // Stupid non-conforming modulus operation.
+                if (result.digits[i] < 0) result.digits[i] += biRadix
+                c = 0 - Number(n < 0)
+            }
+            // Result is opposite sign of arguments.
+            result.isNeg = !x.isNeg
+        } else {
+            // Result is same sign.
+            result.isNeg = x.isNeg
+        }
+    }
+    return result
+}
+
+RSAUtils.biHighIndex = function (x) {
+    var result = x.digits.length - 1
+    while (result > 0 && x.digits[result] === 0) --result
+    return result
+}
+
+RSAUtils.biNumBits = function (x) {
+    var n = RSAUtils.biHighIndex(x)
+    var d = x.digits[n]
+    var m = (n + 1) * bitsPerDigit
+    var result
+    for (result = m; result > m - bitsPerDigit; --result) {
+        if ((d & 0x8000) !== 0) break
+        d <<= 1
+    }
+    return result
+}
+
+RSAUtils.biMultiply = function (x, y) {
+    var result = new BigInt()
+    var c
+    var n = RSAUtils.biHighIndex(x)
+    var t = RSAUtils.biHighIndex(y)
+    // var u, uv, k
+    var uv, k
+
+    for (var i = 0; i <= t; ++i) {
+        c = 0
+        k = i
+        for (var j = 0; j <= n; ++j, ++k) {
+            uv = result.digits[k] + x.digits[j] * y.digits[i] + c
+            result.digits[k] = uv & maxDigitVal
+            c = uv >>> biRadixBits
+            // c = Math.floor(uv / biRadix);
+        }
+        result.digits[i + n + 1] = c
+    }
+    // Someone give me a logical xor, please.
+    result.isNeg = x.isNeg !== y.isNeg
+    return result
+}
+
+RSAUtils.biMultiplyDigit = function (x, y) {
+    var n, c, uv, result
+
+    result = new BigInt()
+    n = RSAUtils.biHighIndex(x)
+    c = 0
+    for (var j = 0; j <= n; ++j) {
+        uv = result.digits[j] + x.digits[j] * y + c
+        result.digits[j] = uv & maxDigitVal
+        c = uv >>> biRadixBits
+        // c = Math.floor(uv / biRadix);
+    }
+    result.digits[1 + n] = c
+    return result
+}
+
+RSAUtils.arrayCopy = function (src, srcStart, dest, destStart, n) {
+    var m = Math.min(srcStart + n, src.length)
+    for (var i = srcStart, j = destStart; i < m; ++i, ++j) {
+        dest[j] = src[i]
+    }
+}
+
+var highBitMasks = [0x0000, 0x8000, 0xC000, 0xE000, 0xF000, 0xF800,
+    0xFC00, 0xFE00, 0xFF00, 0xFF80, 0xFFC0, 0xFFE0,
+    0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF]
+
+RSAUtils.biShiftLeft = function (x, n) {
+    var digitCount = Math.floor(n / bitsPerDigit)
+    var result = new BigInt()
+    RSAUtils.arrayCopy(x.digits, 0, result.digits, digitCount,
+        result.digits.length - digitCount)
+    var bits = n % bitsPerDigit
+    var rightBits = bitsPerDigit - bits
+    for (var i = result.digits.length - 1, i1 = i - 1; i > 0; --i, --i1) {
+        result.digits[i] = ((result.digits[i] << bits) & maxDigitVal) |
+            ((result.digits[i1] & highBitMasks[bits]) >>>
+                (rightBits))
+    }
+    result.digits[0] = ((result.digits[i] << bits) & maxDigitVal)
+    result.isNeg = x.isNeg
+    return result
+}
+
+var lowBitMasks = [0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
+    0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF,
+    0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF]
+
+RSAUtils.biShiftRight = function (x, n) {
+    var digitCount = Math.floor(n / bitsPerDigit)
+    var result = new BigInt()
+    RSAUtils.arrayCopy(x.digits, digitCount, result.digits, 0,
+        x.digits.length - digitCount)
+    var bits = n % bitsPerDigit
+    var leftBits = bitsPerDigit - bits
+    for (var i = 0, i1 = i + 1; i < result.digits.length - 1; ++i, ++i1) {
+        result.digits[i] = (result.digits[i] >>> bits) |
+            ((result.digits[i1] & lowBitMasks[bits]) << leftBits)
+    }
+    result.digits[result.digits.length - 1] >>>= bits
+    result.isNeg = x.isNeg
+    return result
+}
+
+RSAUtils.biMultiplyByRadixPower = function (x, n) {
+    var result = new BigInt()
+    RSAUtils.arrayCopy(x.digits, 0, result.digits, n, result.digits.length - n)
+    return result
+}
+
+RSAUtils.biDivideByRadixPower = function (x, n) {
+    var result = new BigInt()
+    RSAUtils.arrayCopy(x.digits, n, result.digits, 0, result.digits.length - n)
+    return result
+}
+
+RSAUtils.biModuloByRadixPower = function (x, n) {
+    var result = new BigInt()
+    RSAUtils.arrayCopy(x.digits, 0, result.digits, 0, n)
+    return result
+}
+
+RSAUtils.biCompare = function (x, y) {
+    if (x.isNeg !== y.isNeg) {
+        return 1 - 2 * Number(x.isNeg)
+    }
+    for (var i = x.digits.length - 1; i >= 0; --i) {
+        if (x.digits[i] !== y.digits[i]) {
+            if (x.isNeg) {
+                return 1 - 2 * Number(x.digits[i] > y.digits[i])
+            } else {
+                return 1 - 2 * Number(x.digits[i] < y.digits[i])
+            }
+        }
+    }
+    return 0
+}
+
+RSAUtils.biDivideModulo = function (x, y) {
+    var nb = RSAUtils.biNumBits(x)
+    var tb = RSAUtils.biNumBits(y)
+    var origYIsNeg = y.isNeg
+    var q, r
+    if (nb < tb) {
+        // |x| < |y|
+        if (x.isNeg) {
+            q = RSAUtils.biCopy(bigOne)
+            q.isNeg = !y.isNeg
+            x.isNeg = false
+            y.isNeg = false
+            r = RSAUtils.biSubtract(y, x)
+            // Restore signs, 'cause they're references.
+            x.isNeg = true
+            y.isNeg = origYIsNeg
+        } else {
+            q = new BigInt()
+            r = RSAUtils.biCopy(x)
+        }
+        return [q, r]
+    }
+
+    q = new BigInt()
+    r = x
+
+    // Normalize Y.
+    var t = Math.ceil(tb / bitsPerDigit) - 1
+    var lambda = 0
+    while (y.digits[t] < biHalfRadix) {
+        y = RSAUtils.biShiftLeft(y, 1)
+        ++lambda
+        ++tb
+        t = Math.ceil(tb / bitsPerDigit) - 1
+    }
+    // Shift r over to keep the quotient constant. We'll shift the
+    // remainder back at the end.
+    r = RSAUtils.biShiftLeft(r, lambda)
+    nb += lambda // Update the bit count for x.
+    var n = Math.ceil(nb / bitsPerDigit) - 1
+
+    var b = RSAUtils.biMultiplyByRadixPower(y, n - t)
+    while (RSAUtils.biCompare(r, b) !== -1) {
+        ++q.digits[n - t]
+        r = RSAUtils.biSubtract(r, b)
+    }
+    for (var i = n; i > t; --i) {
+        var ri = (i >= r.digits.length) ? 0 : r.digits[i]
+        var ri1 = (i - 1 >= r.digits.length) ? 0 : r.digits[i - 1]
+        var ri2 = (i - 2 >= r.digits.length) ? 0 : r.digits[i - 2]
+        var yt = (t >= y.digits.length) ? 0 : y.digits[t]
+        var yt1 = (t - 1 >= y.digits.length) ? 0 : y.digits[t - 1]
+        if (ri === yt) {
+            q.digits[i - t - 1] = maxDigitVal
+        } else {
+            q.digits[i - t - 1] = Math.floor((ri * biRadix + ri1) / yt)
+        }
+
+        var c1 = q.digits[i - t - 1] * ((yt * biRadix) + yt1)
+        var c2 = (ri * biRadixSquared) + ((ri1 * biRadix) + ri2)
+        while (c1 > c2) {
+            --q.digits[i - t - 1]
+            c1 = q.digits[i - t - 1] * ((yt * biRadix) | yt1)
+            c2 = (ri * biRadix * biRadix) + ((ri1 * biRadix) + ri2)
+        }
+
+        b = RSAUtils.biMultiplyByRadixPower(y, i - t - 1)
+        r = RSAUtils.biSubtract(r, RSAUtils.biMultiplyDigit(b, q.digits[i - t - 1]))
+        if (r.isNeg) {
+            r = RSAUtils.biAdd(r, b)
+            --q.digits[i - t - 1]
+        }
+    }
+    r = RSAUtils.biShiftRight(r, lambda)
+    // Fiddle with the signs and stuff to make sure that 0 <= r < y.
+    q.isNeg = x.isNeg !== origYIsNeg
+    if (x.isNeg) {
+        if (origYIsNeg) {
+            q = RSAUtils.biAdd(q, bigOne)
+        } else {
+            q = RSAUtils.biSubtract(q, bigOne)
+        }
+        y = RSAUtils.biShiftRight(y, lambda)
+        r = RSAUtils.biSubtract(y, r)
+    }
+    // Check for the unbelievably stupid degenerate case of r === -0.
+    if (r.digits[0] === 0 && RSAUtils.biHighIndex(r) === 0) r.isNeg = false
+
+    return [q, r]
+}
+
+RSAUtils.biDivide = function (x, y) {
+    return RSAUtils.biDivideModulo(x, y)[0]
+}
+
+RSAUtils.biModulo = function (x, y) {
+    return RSAUtils.biDivideModulo(x, y)[1]
+}
+
+RSAUtils.biMultiplyMod = function (x, y, m) {
+    return RSAUtils.biModulo(RSAUtils.biMultiply(x, y), m)
+}
+
+RSAUtils.biPow = function (x, y) {
+    var result = bigOne
+    var a = x
+    while (true) {
+        if ((y & 1) !== 0) result = RSAUtils.biMultiply(result, a)
+        y >>= 1
+        if (y === 0) break
+        a = RSAUtils.biMultiply(a, a)
+    }
+    return result
+}
+
+RSAUtils.biPowMod = function (x, y, m) {
+    var result = bigOne
+    var a = x
+    var k = y
+    while (true) {
+        if ((k.digits[0] & 1) !== 0) result = RSAUtils.biMultiplyMod(result, a, m)
+        k = RSAUtils.biShiftRight(k, 1)
+        if (k.digits[0] === 0 && RSAUtils.biHighIndex(k) === 0) break
+        a = RSAUtils.biMultiplyMod(a, a, m)
+    }
+    return result
+}
+
+var BarrettMu = function (m) {
+    this.modulus = RSAUtils.biCopy(m)
+    this.k = RSAUtils.biHighIndex(this.modulus) + 1
+    var b2k = new BigInt()
+    b2k.digits[2 * this.k] = 1 // b2k = b^(2k)
+    this.mu = RSAUtils.biDivide(b2k, this.modulus)
+    this.bkplus1 = new BigInt()
+    this.bkplus1.digits[this.k + 1] = 1 // bkplus1 = b^(k+1)
+    this.modulo = BarrettMuModulo
+    this.multiplyMod = BarrettMuMultiplyMod
+    this.powMod = BarrettMuPowMod
+}
+
+function BarrettMuModulo(x) {
+    var $dmath = RSAUtils
+    var q1 = $dmath.biDivideByRadixPower(x, this.k - 1)
+    var q2 = $dmath.biMultiply(q1, this.mu)
+    var q3 = $dmath.biDivideByRadixPower(q2, this.k + 1)
+    var r1 = $dmath.biModuloByRadixPower(x, this.k + 1)
+    var r2term = $dmath.biMultiply(q3, this.modulus)
+    var r2 = $dmath.biModuloByRadixPower(r2term, this.k + 1)
+    var r = $dmath.biSubtract(r1, r2)
+    if (r.isNeg) {
+        r = $dmath.biAdd(r, this.bkplus1)
+    }
+    var rgtem = $dmath.biCompare(r, this.modulus) >= 0
+    while (rgtem) {
+        r = $dmath.biSubtract(r, this.modulus)
+        rgtem = $dmath.biCompare(r, this.modulus) >= 0
+    }
+    return r
+}
+
+function BarrettMuMultiplyMod(x, y) {
+    /*
+     x = this.modulo(x);
+     y = this.modulo(y);
+     */
+    var xy = RSAUtils.biMultiply(x, y)
+    return this.modulo(xy)
+}
+
+function BarrettMuPowMod(x, y) {
+    var result = new BigInt()
+    result.digits[0] = 1
+    var a = x
+    var k = y
+    while (true) {
+        if ((k.digits[0] & 1) !== 0) result = this.multiplyMod(result, a)
+        k = RSAUtils.biShiftRight(k, 1)
+        if (k.digits[0] === 0 && RSAUtils.biHighIndex(k) === 0) break
+        a = this.multiplyMod(a, a)
+    }
+    return result
+}
+
+var RSAKeyPair = function (encryptionExponent, decryptionExponent, modulus) {
+    var $dmath = RSAUtils
+    this.e = $dmath.biFromHex(encryptionExponent)
+    this.d = $dmath.biFromHex(decryptionExponent)
+    this.m = $dmath.biFromHex(modulus)
+    // We can do two bytes per digit, so
+    // chunkSize = 2 * (number of digits in modulus - 1).
+    // Since biHighIndex returns the high index, not the number of digits, 1 has
+    // already been subtracted.
+    this.chunkSize = 2 * $dmath.biHighIndex(this.m)
+    this.radix = 16
+    this.barrett = new BarrettMu(this.m)
+}
+
+RSAUtils.getKeyPair = function (encryptionExponent, decryptionExponent, modulus) {
+    return new RSAKeyPair(encryptionExponent, decryptionExponent, modulus)
+}
+
+// var twoDigit = function (n) {
+//   return (n < 10 ? '0' : '') + String(n)
+// }
+
+// Altered by Rob Saunders (rob@robsaunders.net). New routine pads the
+// string after it has been converted to an array. This fixes an
+// incompatibility with Flash MX's ActionScript.
+RSAUtils.encryptedString = function (key, s) {
+    var a = []
+    var sl = s.length
+    var i = 0
+    while (i < sl) {
+        a[i] = s.charCodeAt(i)
+        i++
+    }
+
+    while (a.length % key.chunkSize !== 0) {
+        a[i++] = 0
+    }
+
+    var al = a.length
+    var result = ''
+    var j, k, block
+    for (i = 0; i < al; i += key.chunkSize) {
+        block = new BigInt()
+        j = 0
+        for (k = i; k < i + key.chunkSize; ++j) {
+            block.digits[j] = a[k++]
+            block.digits[j] += a[k++] << 8
+        }
+        var crypt = key.barrett.powMod(block, key.e)
+        var text = key.radix === 16 ? RSAUtils.biToHex(crypt) : RSAUtils.biToString(crypt, key.radix)
+        result += text + ' '
+    }
+    return result.substring(0, result.length - 1) // Remove last space.
+}
+
+RSAUtils.decryptedString = function (key, s) {
+    var blocks = s.split(' ')
+    var result = ''
+    var i, j, block
+    for (i = 0; i < blocks.length; ++i) {
+        var bi
+        if (key.radix === 16) {
+            bi = RSAUtils.biFromHex(blocks[i])
+        } else {
+            bi = RSAUtils.biFromString(blocks[i], key.radix)
+        }
+        block = key.barrett.powMod(bi, key.d)
+        for (j = 0; j <= RSAUtils.biHighIndex(block); ++j) {
+            result += String.fromCharCode(block.digits[j] & 255,
+                block.digits[j] >> 8)
+        }
+    }
+    // Remove trailing null, if any.
+    if (result.charCodeAt(result.length - 1) === 0) {
+        result = result.substring(0, result.length - 1)
+    }
+    return result
+}
+
+RSAUtils.setMaxDigits(130)
+
+module.exports = {
+    RSAUtils
+};