import utils from "../services/utils.js"; import optionService from "../services/options.js"; import myScryptService from "../services/encryption/my_scrypt.js"; import log from "../services/log.js"; import passwordService from "../services/encryption/password.js"; import assetPath from "../services/asset_path.js"; import appPath from "../services/app_path.js"; import ValidationError from "../errors/validation_error.js"; import type { Request, Response } from 'express'; import recoveryCodeService from '../services/encryption/recovery_codes.js'; import openIDService from '../services/open_id.js'; import openIDEncryption from '../services/encryption/open_id_encryption.js'; import totp from '../services/totp.js'; import open_id from '../services/open_id.js'; function loginPage(req: Request, res: Response) { if (open_id.isOpenIDEnabled()) { res.redirect('/authenticate'); } else { res.render('login', { wrongPassword: false, wrongTotp: false, totpEnabled: totp.isTotpEnabled(), assetPath: assetPath, appPath: appPath, }); } } function setPasswordPage(req: Request, res: Response) { res.render("set_password", { error: false, assetPath, appPath }); } function setPassword(req: Request, res: Response) { if (passwordService.isPasswordSet()) { throw new ValidationError("Password has been already set"); } let { password1, password2 } = req.body; password1 = password1.trim(); password2 = password2.trim(); let error; if (password1 !== password2) { error = "Entered passwords don't match."; } else if (password1.length < 4) { error = "Password must be at least 4 characters long."; } if (error) { res.render("set_password", { error, assetPath, appPath }); return; } passwordService.setPassword(password1); res.redirect("login"); } function login(req: Request, res: Response) { const submittedPassword = req.body.password; const submittedTotpToken = req.body.totpToken; // 首先验证密码 if (!verifyPassword(submittedPassword)) { sendLoginError(req, res, 'password'); return; } // 如果密码正确且启用了 TOTP,验证 TOTP if (totp.isTotpEnabled()) { if (!verifyTOTP(submittedTotpToken)) { sendLoginError(req, res, 'totp'); return; } } const rememberMe = req.body.rememberMe; req.session.regenerate(() => { if (rememberMe) { req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks } else { // unset default maxAge set by sessionParser // Cookie becomes non-persistent and expires after current browser session (e.g. when browser is closed) req.session.cookie.maxAge = undefined; } // 记录当前的认证状态 req.session.lastAuthState = { totpEnabled: totp.isTotpEnabled(), ssoEnabled: open_id.isOpenIDEnabled() }; req.session.loggedIn = true; res.redirect('.'); }); } function verifyTOTP(submittedTotpToken: string) { if (totp.validateTOTP(submittedTotpToken)) return true; const recoveryCodeValidates = recoveryCodeService.verifyRecoveryCode(submittedTotpToken); return recoveryCodeValidates; } function verifyPassword(submittedPassword: string) { const hashed_password = utils.fromBase64(optionService.getOption("passwordVerificationHash")); const guess_hashed = myScryptService.getVerificationHash(submittedPassword); return guess_hashed.equals(hashed_password); } function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' = 'password') { // note that logged IP address is usually meaningless since the traffic should come from a reverse proxy if (totp.isTotpEnabled()) { log.info(`WARNING: Wrong ${errorType} from ${req.ip}, rejecting.`); } else { log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); } res.render('login', { wrongPassword: errorType === 'password', wrongTotp: errorType === 'totp', totpEnabled: totp.isTotpEnabled(), assetPath: assetPath, appPath: appPath, }); } function logout(req: Request, res: Response) { req.session.regenerate(() => { req.session.loggedIn = false; if (openIDService.isOpenIDEnabled() && openIDEncryption.isSubjectIdentifierSaved()) { res.oidc.logout({ returnTo: '/authenticate' }); } else res.redirect('login'); res.sendStatus(200); }); } export default { loginPage, setPasswordPage, setPassword, login, logout };