Fix TOTP login
This commit is contained in:
@@ -18,7 +18,9 @@ import QRCode from "qrcode";
|
||||
import type { Request, Response } from "express";
|
||||
import { authLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { UserCrypto } from "../../utils/user-crypto.js";
|
||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||
import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js";
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
@@ -893,11 +895,7 @@ router.post("/login", async (req, res) => {
|
||||
await authManager.registerUser(userRecord.id, password);
|
||||
}
|
||||
} catch (setupError) {
|
||||
authLogger.error("Failed to initialize user encryption", setupError, {
|
||||
operation: "user_encryption_setup_failed",
|
||||
username,
|
||||
userId: userRecord.id,
|
||||
});
|
||||
// Continue if setup fails - authenticateUser will handle it
|
||||
}
|
||||
|
||||
const dataUnlocked = await authManager.authenticateUser(
|
||||
@@ -905,14 +903,7 @@ router.post("/login", async (req, res) => {
|
||||
password,
|
||||
);
|
||||
if (!dataUnlocked) {
|
||||
authLogger.error("Failed to unlock user data during login", undefined, {
|
||||
operation: "user_login_data_unlock_failed",
|
||||
username,
|
||||
userId: userRecord.id,
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "Failed to unlock user data - please contact administrator",
|
||||
});
|
||||
return res.status(401).json({ error: "Incorrect password" });
|
||||
}
|
||||
|
||||
if (userRecord.totp_enabled) {
|
||||
@@ -921,6 +912,7 @@ router.post("/login", async (req, res) => {
|
||||
expiresIn: "10m",
|
||||
});
|
||||
return res.json({
|
||||
success: true,
|
||||
requires_totp: true,
|
||||
temp_token: tempToken,
|
||||
});
|
||||
@@ -1488,17 +1480,42 @@ router.post("/totp/verify-login", async (req, res) => {
|
||||
return res.status(400).json({ error: "TOTP not enabled for this user" });
|
||||
}
|
||||
|
||||
const userDataKey = authManager.getUserDataKey(userRecord.id);
|
||||
if (!userDataKey) {
|
||||
return res.status(401).json({
|
||||
error: "Session expired - please log in again",
|
||||
code: "SESSION_EXPIRED",
|
||||
});
|
||||
}
|
||||
|
||||
const totpSecret = LazyFieldEncryption.safeGetFieldValue(
|
||||
userRecord.totp_secret,
|
||||
userDataKey,
|
||||
userRecord.id,
|
||||
"totp_secret",
|
||||
);
|
||||
|
||||
const verified = speakeasy.totp.verify({
|
||||
secret: userRecord.totp_secret,
|
||||
secret: totpSecret,
|
||||
encoding: "base32",
|
||||
token: totp_code,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
if (!verified) {
|
||||
const backupCodes = userRecord.totp_backup_codes
|
||||
? JSON.parse(userRecord.totp_backup_codes)
|
||||
: [];
|
||||
let backupCodes = [];
|
||||
try {
|
||||
backupCodes = userRecord.totp_backup_codes
|
||||
? JSON.parse(userRecord.totp_backup_codes)
|
||||
: [];
|
||||
} catch (parseError) {
|
||||
backupCodes = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(backupCodes)) {
|
||||
backupCodes = [];
|
||||
}
|
||||
|
||||
const backupIndex = backupCodes.indexOf(totp_code);
|
||||
|
||||
if (backupIndex === -1) {
|
||||
@@ -1516,18 +1533,40 @@ router.post("/totp/verify-login", async (req, res) => {
|
||||
expiresIn: "50d",
|
||||
});
|
||||
|
||||
const isElectron =
|
||||
req.headers["x-electron-app"] === "true" ||
|
||||
req.headers["X-Electron-App"] === "true";
|
||||
|
||||
const isDataUnlocked = authManager.isUserUnlocked(userRecord.id);
|
||||
|
||||
if (!isDataUnlocked) {
|
||||
return res.status(401).json({
|
||||
error: "Session expired - please log in again",
|
||||
code: "SESSION_EXPIRED",
|
||||
});
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
userId: userRecord.id,
|
||||
is_oidc: !!userRecord.is_oidc,
|
||||
totp_enabled: !!userRecord.totp_enabled,
|
||||
data_unlocked: isDataUnlocked,
|
||||
};
|
||||
|
||||
if (isElectron) {
|
||||
response.token = token;
|
||||
}
|
||||
|
||||
return res
|
||||
.cookie(
|
||||
"jwt",
|
||||
token,
|
||||
authManager.getSecureCookieOptions(req, 50 * 24 * 60 * 60 * 1000),
|
||||
)
|
||||
.json({
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
token: req.headers["x-electron-app"] === "true" ? token : undefined,
|
||||
});
|
||||
.json(response);
|
||||
} catch (err) {
|
||||
authLogger.error("TOTP verification failed", err);
|
||||
return res.status(500).json({ error: "TOTP verification failed" });
|
||||
|
||||
@@ -1174,6 +1174,7 @@
|
||||
"networkError": "Network error",
|
||||
"databaseConnection": "Could not connect to the database.",
|
||||
"unknownError": "Unknown error",
|
||||
"loginFailed": "Login failed",
|
||||
"failedPasswordReset": "Failed to initiate password reset",
|
||||
"failedVerifyCode": "Failed to verify reset code",
|
||||
"failedCompleteReset": "Failed to complete password reset",
|
||||
@@ -1193,7 +1194,8 @@
|
||||
"usernameExists": "Username already exists",
|
||||
"emailExists": "Email already exists",
|
||||
"loadFailed": "Failed to load data",
|
||||
"saveError": "Failed to save"
|
||||
"saveError": "Failed to save",
|
||||
"sessionExpired": "Session expired - please log in again"
|
||||
},
|
||||
"messages": {
|
||||
"saveSuccess": "Saved successfully",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"networkError": "网络错误",
|
||||
"databaseConnection": "无法连接到数据库。",
|
||||
"unknownError": "未知错误",
|
||||
"loginFailed": "登录失败",
|
||||
"failedPasswordReset": "无法启动密码重置",
|
||||
"failedVerifyCode": "验证重置代码失败",
|
||||
"failedCompleteReset": "无法完成密码重置",
|
||||
@@ -1170,7 +1171,8 @@
|
||||
"usernameExists": "用户名已存在",
|
||||
"emailExists": "邮箱已存在",
|
||||
"loadFailed": "加载数据失败",
|
||||
"saveError": "保存失败"
|
||||
"saveError": "保存失败",
|
||||
"sessionExpired": "会话已过期 - 请重新登录"
|
||||
},
|
||||
"messages": {
|
||||
"saveSuccess": "保存成功",
|
||||
|
||||
@@ -362,32 +362,46 @@ export function HomepageAuth({
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("TOTP login successful - JWT set as secure HttpOnly cookie");
|
||||
const meRes = await getUserInfo();
|
||||
if (isElectron() && res.token) {
|
||||
localStorage.setItem("jwt", res.token);
|
||||
}
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
setUserId(meRes.userId || null);
|
||||
setIsAdmin(!!res.is_admin);
|
||||
setUsername(res.username || null);
|
||||
setUserId(res.userId || null);
|
||||
setDbError(null);
|
||||
onAuthSuccess({
|
||||
isAdmin: !!meRes.is_admin,
|
||||
username: meRes.username || null,
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onAuthSuccess({
|
||||
isAdmin: !!res.is_admin,
|
||||
username: res.username || null,
|
||||
userId: res.userId || null,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err: any) {
|
||||
const errorCode = err?.response?.data?.code;
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
toast.error(errorMessage);
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
setTab("login");
|
||||
toast.error(t("errors.sessionExpired"));
|
||||
} else {
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setTotpLoading(false);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
setCookie,
|
||||
getCookie,
|
||||
logoutUser,
|
||||
isElectron,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
|
||||
@@ -341,30 +342,46 @@ export function HomepageAuth({
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
const meRes = await getUserInfo();
|
||||
if (isElectron() && res.token) {
|
||||
localStorage.setItem("jwt", res.token);
|
||||
}
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
setUserId(meRes.userId || null);
|
||||
setIsAdmin(!!res.is_admin);
|
||||
setUsername(res.username || null);
|
||||
setUserId(res.userId || null);
|
||||
setDbError(null);
|
||||
onAuthSuccess({
|
||||
isAdmin: !!meRes.is_admin,
|
||||
username: meRes.username || null,
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onAuthSuccess({
|
||||
isAdmin: !!res.is_admin,
|
||||
username: res.username || null,
|
||||
userId: res.userId || null,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err: any) {
|
||||
const errorCode = err?.response?.data?.code;
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
toast.error(errorMessage);
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
setTab("login");
|
||||
toast.error(t("errors.sessionExpired"));
|
||||
} else {
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setTotpLoading(false);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ interface AuthResponse {
|
||||
success?: boolean;
|
||||
is_admin?: boolean;
|
||||
username?: string;
|
||||
userId?: string;
|
||||
is_oidc?: boolean;
|
||||
totp_enabled?: boolean;
|
||||
data_unlocked?: boolean;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
@@ -1540,6 +1544,8 @@ export async function loginUser(
|
||||
success: response.data.success,
|
||||
is_admin: response.data.is_admin,
|
||||
username: response.data.username,
|
||||
requires_totp: response.data.requires_totp,
|
||||
temp_token: response.data.temp_token,
|
||||
};
|
||||
} catch (error) {
|
||||
handleApiError(error, "login user");
|
||||
|
||||
Reference in New Issue
Block a user