Skip to content

Conversation

kyubibii
Copy link
Contributor

@kyubibii kyubibii commented Oct 10, 2025

…irst login

  • Add is_initial_password field in database to track whether the user's password is an admin-assigned initial password
  • Update backend User model and business logic for the new field
  • Enforce password change for users logging in with the initial password, prompting a reset on first login
  • Update frontend login and registration logic to handle and guide initial password users, including modal/prompt for password update
  • Update i18n files to include relevant message strings (English, Chinese, French)
  • Ensure backward compatibility for existing users

This enhances security by preventing long-term usage of default/admin-set passwords.

closes #1985

Summary by CodeRabbit

  • New Features
    • Added first-login prompt for admin-created users to change the initial password.
    • Introduced a change-initial-password API endpoint with rate limiting.
    • Enhanced login UI with a modal to set a new password, including validation (length, match, not same as old).
    • Streamlined post-change re-login with updated credentials.
    • Improved handling of initial-password state across user creation and updates.
    • Added English, French, and Chinese translations for the new password-change flow.

…irst login

- Add `is_initial_password` field in database to track whether the user's password is an admin-assigned initial password
- Update backend User model and business logic for the new field
- Enforce password change for users logging in with the initial password, prompting a reset on first login
- Update frontend login and registration logic to handle and guide initial password users, including modal/prompt for password update
- Update i18n files to include relevant message strings (English, Chinese, French)
- Ensure backward compatibility for existing users

This enhances security by preventing long-term usage of default/admin-set passwords.

closes QuantumNous#1985
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds an initial-password workflow: backend introduces is_initial_password flag, updates login to require password change when true, provides a new ChangeInitialPassword API, and adjusts user creation/update semantics. Router exposes the new endpoint. Frontend login detects require_change_password, shows a modal to change password, and locales gain related strings.

Changes

Cohort / File(s) Summary of changes
Backend: Initial password semantics
controller/user.go
Enforce initial-password change on login; set/reset IsInitialPassword on user create/update paths; add public ChangeInitialPasswordRequest type and ChangeInitialPassword handler to validate old password, update hash, and clear is_initial_password.
Data model
model/user.go
Add IsInitialPassword bool with JSON tag is_initial_password and DB default; propagate field in edit/update map when password changes.
Routing
router/api-router.go
Register POST /api/user/change_initial_password under user group with CriticalRateLimit → controller.ChangeInitialPassword.
Frontend: Login flow
web/src/components/auth/LoginForm.jsx
Detect require_change_password from login; render Change Initial Password modal; validate inputs; call API to change initial password; reset state and re-login with new credentials.
Localization (EN)
web/src/i18n/locales/en.json
Add strings for initial-password notice, inputs, validation, success/failure messages.
Localization (FR)
web/src/i18n/locales/fr.json
Add French translations for initial-password workflow; adjust trailing comma after existing entry.
Localization (ZH)
web/src/i18n/locales/zh.json
Add Chinese translations for initial-password workflow and related messages.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User (Client)
  participant FE as Login UI
  participant API as Server API
  participant DB as Database

  U->>FE: Submit username/password
  FE->>API: POST /api/user/login
  API->>DB: Verify user + password
  alt is_initial_password = true
    API-->>FE: { require_change_password: true }
    FE->>U: Show "Change Initial Password" modal
    U->>FE: Enter old/new/confirm
    FE->>API: POST /api/user/change_initial_password
    API->>DB: Validate old pw, update hash, set is_initial_password=false
    API-->>FE: { success: true }
    FE->>API: POST /api/user/login (with new password)
    API-->>FE: { token / session }
    FE-->>U: Login success
  else normal login
    API-->>FE: { token / session }
    FE-->>U: Login success
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • Calcium-Ion

Poem

A whisk of keys, a hop, a bound,
First login chirps a gentle sound—
“Change your pass,” the burrow sings,
Safer warrens, safer things.
Tap-tap-tap, the carrot glows,
New secrets set—off Bunbun goes! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the main feature by indicating support for marking initial passwords and enforcing a forced password change, which directly reflects the core functionality introduced by the changeset.
Linked Issues Check ✅ Passed The changes implement the is_initial_password flag, enforce a password change upon first login via backend and frontend flows, and update relevant UI strings, fully addressing the requirement to prompt new users to change their initial passwords as described in issue #1985.
Out of Scope Changes Check ✅ Passed All modifications are directly related to tracking and enforcing initial password changes and updating corresponding UI and localization, with no unrelated or out-of-scope code alterations detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
model/user.go (1)

590-600: Clear initial-password flag on email resets to avoid forced-change loop.

Users resetting via email should no longer be marked initial password.

- err = DB.Model(&User{}).Where("email = ?", email).Update("password", hashedPassword).Error
+ err = DB.Model(&User{}).
+   Where("email = ?", email).
+   Updates(map[string]interface{}{
+     "password":            hashedPassword,
+     "is_initial_password": false,
+   }).Error
🧹 Nitpick comments (5)
model/user.go (1)

20-49: Harden schema for IsInitialPassword (add NOT NULL).

Current tag lacks NOT NULL. Recommend not null to avoid tri-state and align with migration.

- IsInitialPassword bool           `json:"is_initial_password" gorm:"type:boolean;default:false;column:is_initial_password"` // 是否是初始密码
+ IsInitialPassword bool           `json:"is_initial_password" gorm:"type:boolean;default:false;not null;column:is_initial_password"` // 是否是初始密码
bin/migration_add_initial_password.sql (2)

4-11: Enforce NOT NULL on is_initial_password across DBs.

Avoid NULL booleans; use NOT NULL defaults for all engines.

-- For SQLite
-ALTER TABLE users ADD COLUMN is_initial_password INTEGER DEFAULT 0;
+ALTER TABLE users ADD COLUMN is_initial_password INTEGER NOT NULL DEFAULT 0;

-- For MySQL (uncomment if using MySQL)
--- ALTER TABLE users ADD COLUMN is_initial_password BOOLEAN DEFAULT FALSE;
+-- ALTER TABLE users ADD COLUMN is_initial_password BOOLEAN NOT NULL DEFAULT FALSE;

-- For PostgreSQL (uncomment if using PostgreSQL)
--- ALTER TABLE users ADD COLUMN is_initial_password BOOLEAN DEFAULT FALSE;
+-- ALTER TABLE users ADD COLUMN is_initial_password BOOLEAN NOT NULL DEFAULT FALSE;

13-15: Optional: retain UPDATE for SQLite; not needed if NOT NULL DEFAULT used.

If you adopt NOT NULL DEFAULT, this UPDATE becomes redundant for new migrations but is harmless.

web/src/i18n/locales/en.json (1)

2283-2298: Avoid duplicate i18n keys (“Cancel”, “New Password”, etc.).

These keys already exist earlier; duplicates make maintenance error‑prone. Reuse existing keys or scope under a namespace.

web/src/i18n/locales/zh.json (1)

136-151: 去重重复的翻译键(如“取消”、“新密码”、“确认密码”)。

文件中已存在同名键,建议复用或按模块命名避免重复,提高可维护性。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 721357b and 52b39c6.

📒 Files selected for processing (8)
  • bin/migration_add_initial_password.sql (1 hunks)
  • controller/user.go (6 hunks)
  • model/user.go (2 hunks)
  • router/api-router.go (1 hunks)
  • web/src/components/auth/LoginForm.jsx (7 hunks)
  • web/src/i18n/locales/en.json (1 hunks)
  • web/src/i18n/locales/fr.json (1 hunks)
  • web/src/i18n/locales/zh.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/auth/LoginForm.jsx (2)
web/src/components/settings/PersonalSetting.jsx (1)
  • showChangePasswordModal (62-62)
web/src/helpers/utils.jsx (2)
  • showError (122-151)
  • showSuccess (157-159)
router/api-router.go (1)
controller/user.go (1)
  • ChangeInitialPassword (1153-1249)
controller/user.go (5)
common/constants.go (1)
  • RoleCommonUser (135-135)
common/gin.go (1)
  • ApiError (96-101)
model/main.go (1)
  • DB (63-63)
model/user.go (2)
  • User (19-49)
  • GetUserById (288-300)
model/log.go (1)
  • RecordLog (78-94)
🔇 Additional comments (9)
model/user.go (1)

466-469: Password update propagation looks correct.

Only writes is_initial_password when password changes, preventing accidental toggles.

controller/user.go (4)

97-109: Login gating for initial password: LGTM.

Early-return with require_change_password cleanly prevents session creation.


223-229: Registration sets non-initial password: LGTM.

Self-registered users correctly start with IsInitialPassword=false.


657-660: Admin password resets re-mark as initial: LGTM.

Consistent with the intended workflow.


750-779: Self password update clears initial flag: LGTM.

Hashes server-side and sets is_initial_password=false atomically.

web/src/components/auth/LoginForm.jsx (4)

204-214: Initial-password intercept flow on login: LGTM.

Captures credentials and shows modal without creating a session.


422-432: Cancel handler: LGTM.

Preserves username, clears secrets, resets state.


900-961: Change-password modal UX: LGTM.

Clear messaging, validations, and focused inputs.


986-987: Modal integration: LGTM.

Included alongside other auth modals; ordering fine.

Comment on lines 1146 to 1249
type ChangeInitialPasswordRequest struct {
UserId int `json:"user_id"`
Username string `json:"username"`
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}

func ChangeInitialPassword(c *gin.Context) {
var req ChangeInitialPasswordRequest
err := json.NewDecoder(c.Request.Body).Decode(&req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}

if req.UserId == 0 || req.Username == "" || req.OldPassword == "" || req.NewPassword == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "请提供完整的参数",
})
return
}

// 验证新密码长度
if len(req.NewPassword) < 8 || len(req.NewPassword) > 20 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "新密码长度必须在8-20个字符之间",
})
return
}

// 获取用户
user, err := model.GetUserById(req.UserId, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户不存在",
})
return
}

// 验证用户名匹配
if user.Username != req.Username {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户信息不匹配",
})
return
}

// 验证旧密码
if !common.ValidatePasswordAndHash(req.OldPassword, user.Password) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "原密码错误",
})
return
}

// 验证是否确实需要修改初始密码
if !user.IsInitialPassword {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该用户不需要修改初始密码",
})
return
}

// 更新密码并清除初始密码标记
hashedPassword, err := common.Password2Hash(req.NewPassword)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "密码加密失败",
})
return
}

// 使用 map 更新以确保 IsInitialPassword = false 能被正确写入
updates := map[string]interface{}{
"password": hashedPassword,
"is_initial_password": false,
}

if err := model.DB.Model(&model.User{}).Where("id = ?", user.Id).Updates(updates).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "修改密码失败: " + err.Error(),
})
return
}

// 记录日志
model.RecordLog(user.Id, model.LogTypeSystem, "修改初始密码")

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "密码修改成功,请重新登录",
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Brute-force risk: missing Turnstile on ChangeInitialPassword.

Endpoint is unauthenticated (by design) but lacks Turnstile verification; only rate-limited. Add Turnstile middleware and require token.

Follow-ups:

  • Router: add middleware.TurnstileCheck() to /api/user/change_initial_password.
  • Frontend: pass ?turnstile=${token} and block when token missing. See suggested diffs in router and frontend comments.
🤖 Prompt for AI Agents
controller/user.go lines 1146-1249: the unauthenticated ChangeInitialPassword
handler lacks Turnstile verification; require a validated Turnstile token to
prevent brute-force. Add a check at the start of the handler to require and
validate a Turnstile token (e.g., read token from query/body/header as agreed
and call middleware/utility to verify it), return an error when missing/invalid;
do not rely on rate limit alone. Also update routing to attach
middleware.TurnstileCheck() to the /api/user/change_initial_password route and
update frontend to send the token (e.g., ?turnstile=${token}) and block submit
when token is absent.

userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), controller.Verify2FALogin)
userRoute.POST("/change_initial_password", middleware.CriticalRateLimit(), controller.ChangeInitialPassword)
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), controller.PasskeyLoginBegin)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add Turnstile check to protect password-change endpoint.

Parity with login/register; mitigates brute-force attempts.

- userRoute.POST("/change_initial_password", middleware.CriticalRateLimit(), controller.ChangeInitialPassword)
+ userRoute.POST("/change_initial_password",
+   middleware.CriticalRateLimit(),
+   middleware.TurnstileCheck(),
+   controller.ChangeInitialPassword)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In router/api-router.go around line 55, the password-change endpoint needs
Turnstile protection for parity with login/register; update the route definition
to include the Turnstile middleware (e.g. add middleware.Turnstile() in the
middleware chain before the controller handler) so requests hit the Turnstile
check prior to the password-change controller; preserve existing rate-limit or
critical middleware ordering when inserting the Turnstile middleware.

Comment on lines +435 to +487
const handleChangeInitialPassword = async () => {
if (!newPassword || !confirmPassword) {
showError(t('请输入新密码和确认密码'));
return;
}

if (newPassword.length < 8 || newPassword.length > 20) {
showError(t('新密码长度必须在8-20个字符之间'));
return;
}

if (newPassword !== confirmPassword) {
showError(t('两次输入的密码不一致'));
return;
}

if (newPassword === changePasswordData.old_password) {
showError(t('新密码不能与原密码相同'));
return;
}

setChangePasswordLoading(true);
try {
const res = await API.post('/api/user/change_initial_password', {
user_id: changePasswordData.user_id,
username: changePasswordData.username,
old_password: changePasswordData.old_password,
new_password: newPassword,
});
const { success, message } = res.data;
if (success) {
showSuccess(t('密码修改成功,请输入新密码登录'));
// 关闭修改密码对话框
setShowChangePasswordModal(false);
// 保存用户名,用于重新登录
const savedUsername = changePasswordData.username;
setChangePasswordData(null);
// 清空修改密码表单
setNewPassword('');
setConfirmPassword('');
// 保留用户名,只清空密码字段,让用户输入新密码即可
setInputs({ username: savedUsername, password: '', wechat_verification_code: '' });
// 重置提交状态
setSubmitted(false);
} else {
showError(message);
}
} catch (error) {
showError(t('密码修改失败,请重试'));
} finally {
setChangePasswordLoading(false);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass Turnstile token and block when missing.

Align with backend Turnstile protection; prevents automated abuse.

   const handleChangeInitialPassword = async () => {
+    if (turnstileEnabled && turnstileToken === '') {
+      showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
+      return;
+    }
     if (!newPassword || !confirmPassword) {
       showError(t('请输入新密码和确认密码'));
       return;
     }
@@
     setChangePasswordLoading(true);
     try {
-      const res = await API.post('/api/user/change_initial_password', {
+      const res = await API.post(
+        `/api/user/change_initial_password?turnstile=${turnstileToken}`,
+        {
           user_id: changePasswordData.user_id,
           username: changePasswordData.username,
           old_password: changePasswordData.old_password,
           new_password: newPassword,
-      });
+        }
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleChangeInitialPassword = async () => {
if (!newPassword || !confirmPassword) {
showError(t('请输入新密码和确认密码'));
return;
}
if (newPassword.length < 8 || newPassword.length > 20) {
showError(t('新密码长度必须在8-20个字符之间'));
return;
}
if (newPassword !== confirmPassword) {
showError(t('两次输入的密码不一致'));
return;
}
if (newPassword === changePasswordData.old_password) {
showError(t('新密码不能与原密码相同'));
return;
}
setChangePasswordLoading(true);
try {
const res = await API.post('/api/user/change_initial_password', {
user_id: changePasswordData.user_id,
username: changePasswordData.username,
old_password: changePasswordData.old_password,
new_password: newPassword,
});
const { success, message } = res.data;
if (success) {
showSuccess(t('密码修改成功,请输入新密码登录'));
// 关闭修改密码对话框
setShowChangePasswordModal(false);
// 保存用户名,用于重新登录
const savedUsername = changePasswordData.username;
setChangePasswordData(null);
// 清空修改密码表单
setNewPassword('');
setConfirmPassword('');
// 保留用户名,只清空密码字段,让用户输入新密码即可
setInputs({ username: savedUsername, password: '', wechat_verification_code: '' });
// 重置提交状态
setSubmitted(false);
} else {
showError(message);
}
} catch (error) {
showError(t('密码修改失败,请重试'));
} finally {
setChangePasswordLoading(false);
}
};
const handleChangeInitialPassword = async () => {
if (turnstileEnabled && turnstileToken === '') {
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return;
}
if (!newPassword || !confirmPassword) {
showError(t('请输入新密码和确认密码'));
return;
}
if (newPassword.length < 8 || newPassword.length > 20) {
showError(t('新密码长度必须在8-20个字符之间'));
return;
}
if (newPassword !== confirmPassword) {
showError(t('两次输入的密码不一致'));
return;
}
if (newPassword === changePasswordData.old_password) {
showError(t('新密码不能与原密码相同'));
return;
}
setChangePasswordLoading(true);
try {
const res = await API.post(
`/api/user/change_initial_password?turnstile=${turnstileToken}`,
{
user_id: changePasswordData.user_id,
username: changePasswordData.username,
old_password: changePasswordData.old_password,
new_password: newPassword,
}
);
const { success, message } = res.data;
if (success) {
showSuccess(t('密码修改成功,请输入新密码登录'));
// 关闭修改密码对话框
setShowChangePasswordModal(false);
// 保存用户名,用于重新登录
const savedUsername = changePasswordData.username;
setChangePasswordData(null);
// 清空修改密码表单
setNewPassword('');
setConfirmPassword('');
// 保留用户名,只清空密码字段,让用户输入新密码即可
setInputs({ username: savedUsername, password: '', wechat_verification_code: '' });
// 重置提交状态
setSubmitted(false);
} else {
showError(message);
}
} catch (error) {
showError(t('密码修改失败,请重试'));
} finally {
setChangePasswordLoading(false);
}
};
🤖 Prompt for AI Agents
In web/src/components/auth/LoginForm.jsx around lines 435 to 487, the
change-initial-password flow must include the Turnstile token and block
submission when it's missing: ensure you read the current Turnstile token (from
state or the Turnstile widget callback), validate it before calling the API and
show an error/return if absent, include the token in the POST payload (e.g.
turnstile_token or captcha_token) sent to /api/user/change_initial_password, and
after the request complete or on error reset/clear the token and loading states;
also preserve existing UX (success/error messages, modal close, form reset)
while adding these checks.

Comment on lines +2277 to +2292
"访问隐私政策": "Accéder à la politique de confidentialité",
"修改初始密码": "Modifier le mot de passe initial",
"您使用的是管理员设置的初始密码,为了账户安全,请立即修改密码。": "Vous utilisez un mot de passe initial défini par l'administrateur. Pour la sécurité du compte, veuillez le modifier immédiatement.",
"新密码": "Nouveau mot de passe",
"请输入新密码(8-20个字符)": "Veuillez entrer le nouveau mot de passe (8-20 caractères)",
"确认密码": "Confirmer le mot de passe",
"请再次输入新密码": "Veuillez entrer à nouveau le nouveau mot de passe",
"取消": "Annuler",
"确认修改": "Confirmer",
"请输入新密码和确认密码": "Veuillez entrer le nouveau mot de passe et la confirmation",
"新密码长度必须在8-20个字符之间": "La longueur du nouveau mot de passe doit être comprise entre 8 et 20 caractères",
"两次输入的密码不一致": "Les deux mots de passe ne correspondent pas",
"新密码不能与原密码相同": "Le nouveau mot de passe ne peut pas être identique à l'ancien mot de passe",
"密码修改成功,请输入新密码登录": "Mot de passe modifié avec succès, veuillez entrer votre nouveau mot de passe pour vous connecter",
"密码修改成功,请重新登录": "Mot de passe modifié avec succès, veuillez vous reconnecter",
"密码修改失败,请重试": "Échec de la modification du mot de passe, veuillez réessayer"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: duplicate JSON keys; last-wins overrides earlier translations

This block redefines keys already present earlier in the file, which is invalid JSON and will silently override prior values:

  • 新密码 (Line 2280) duplicates Line 730
  • 确认密码 (Line 2282) duplicates Line 780
  • 取消 (Line 2284) duplicates Line 522
  • 两次输入的密码不一致 (Line 2288) duplicates Line 318
  • 请再次输入新密码 (Line 2283) duplicates Line 1558

Remove duplicates and keep only the new, unique keys. Also minor FR tweaks for consistency.

Apply this diff within the shown range:

   "访问隐私政策": "Accéder à la politique de confidentialité",
   "修改初始密码": "Modifier le mot de passe initial",
   "您使用的是管理员设置的初始密码,为了账户安全,请立即修改密码。": "Vous utilisez un mot de passe initial défini par l'administrateur. Pour la sécurité du compte, veuillez le modifier immédiatement.",
-  "新密码": "Nouveau mot de passe",
-  "请输入新密码(8-20个字符)": "Veuillez entrer le nouveau mot de passe (8-20 caractères)",
-  "确认密码": "Confirmer le mot de passe",
-  "请再次输入新密码": "Veuillez entrer à nouveau le nouveau mot de passe",
-  "取消": "Annuler",
-  "确认修改": "Confirmer",
-  "请输入新密码和确认密码": "Veuillez entrer le nouveau mot de passe et la confirmation",
+  "请输入新密码(8-20个字符)": "Veuillez saisir le nouveau mot de passe (8-20 caractères)",
+  "确认修改": "Confirmer la modification",
+  "请输入新密码和确认密码": "Veuillez saisir le nouveau mot de passe et le confirmer",
   "新密码长度必须在8-20个字符之间": "La longueur du nouveau mot de passe doit être comprise entre 8 et 20 caractères",
-  "两次输入的密码不一致": "Les deux mots de passe ne correspondent pas",
   "新密码不能与原密码相同": "Le nouveau mot de passe ne peut pas être identique à l'ancien mot de passe",
   "密码修改成功,请输入新密码登录": "Mot de passe modifié avec succès, veuillez entrer votre nouveau mot de passe pour vous connecter",
   "密码修改成功,请重新登录": "Mot de passe modifié avec succès, veuillez vous reconnecter",
   "密码修改失败,请重试": "Échec de la modification du mot de passe, veuillez réessayer"

Please also:

  • Verify these keys exist and align in en.json and zh.json for parity.
  • Confirm frontend references use the existing shared keys (e.g., 新密码, 确认密码, 取消) and do not rely on the now-removed duplicates.

🏁 Script executed:

#!/bin/bash
set -e
rg -n '"新密码"' web/src/i18n/locales/fr.json
rg -n '"确认密码"' web/src/i18n/locales/fr.json
rg -n '"取消"' web/src/i18n/locales/fr.json
rg -n '"两次输入的密码不一致"' web/src/i18n/locales/fr.json
rg -n '"请再次输入新密码"' web/src/i18n/locales/fr.json

Length of output: 494


Remove duplicate JSON keys and refine FR translations

  • Duplicate keys override earlier entries; remove these duplicate definitions in lines 2277–2292:
    • 新密码 (2280)
    • 确认密码 (2282)
    • 请再次输入新密码 (2283)
    • 取消 (2284)
    • 两次输入的密码不一致 (2288)
  • Update remaining translations for consistency:
    • 请输入新密码(8-20个字符) → “Veuillez saisir le nouveau mot de passe (8-20 caractères)”
    • 确认修改 → “Confirmer la modification”
    • 请输入新密码和确认密码 → “Veuillez saisir le nouveau mot de passe et le confirmer”
  • Verify en.json/zh.json parity and ensure frontend uses the original shared keys.
🤖 Prompt for AI Agents
In web/src/i18n/locales/fr.json around lines 2277 to 2292, remove the duplicate
JSON keys (新密码 at 2280, 确认密码 at 2282, 请再次输入新密码 at 2283, 取消 at 2284, 两次输入的密码不一致
at 2288) so earlier definitions are not overridden; update the remaining
translations as requested: 请输入新密码(8-20个字符) → "Veuillez saisir le nouveau mot de
passe (8-20 caractères)", 确认修改 → "Confirmer la modification", 请输入新密码和确认密码 →
"Veuillez saisir le nouveau mot de passe et le confirmer"; then verify en.json
and zh.json have matching keys and confirm the frontend uses the original shared
keys (not the removed duplicates) to preserve parity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个项目用的gorm AutoMigrate,不需要这个脚本

// 验证用户名匹配
if user.Username != req.Username {
c.JSON(http.StatusOK, gin.H{
"success": false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个要用gin自带的bind和validate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改为使用binding,并删除.sql

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
controller/user.go (1)

1153-1228: Add Turnstile verification before processing unauthenticated password reset.

This endpoint is intentionally unauthenticated and fully exposed on Line 1153, but it still lacks any Turnstile validation, so an attacker can brute-force {user_id, username, old_password} despite rate limits. Please enforce the same Turnstile check previously requested (attach middleware.TurnstileCheck() on the route and reject when the token is missing/invalid) before proceeding with the reset logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52b39c6 and c8361e1.

📒 Files selected for processing (1)
  • controller/user.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
controller/user.go (4)
common/gin.go (1)
  • ApiError (96-101)
model/main.go (1)
  • DB (63-63)
model/user.go (2)
  • User (19-49)
  • GetUserById (288-300)
model/log.go (1)
  • RecordLog (78-94)

Comment on lines +762 to 780
updates := map[string]interface{}{
"username": user.Username,
"display_name": user.DisplayName,
}

// 如果正在更新密码,同时清除初始密码标记
if updatePassword {
hashedPassword, err := common.Password2Hash(user.Password)
if err != nil {
common.ApiError(c, err)
return
}
updates["password"] = hashedPassword
updates["is_initial_password"] = false // 明确设置为 false
}

// 执行更新
if err := model.DB.Model(&model.User{}).Where("id = ?", userId).Updates(updates).Error; err != nil {
common.ApiError(c, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Prevent wiping username/display name on partial updates.

Line 762 unconditionally drops username/display_name into the update map. When the client submits a password-only payload (very common), those fields are omitted in JSON, unmarshal defaults them to "", and the DB write will blank the username/display name, breaking login. Only include keys that were actually provided in the request.

-	// 构建更新字段的 map
-	updates := map[string]interface{}{
-		"username":     user.Username,
-		"display_name": user.DisplayName,
-	}
+	// 构建更新字段的 map,仅在请求中包含时才更新
+	updates := map[string]interface{}{}
+	if _, ok := requestData["username"]; ok {
+		updates["username"] = user.Username
+	}
+	if _, ok := requestData["display_name"]; ok {
+		updates["display_name"] = user.DisplayName
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In controller/user.go around lines 762–780, the code unconditionally sets
"username" and "display_name" in the updates map which causes blanking when the
client omits those fields; only include these keys when they were actually
provided in the request — change the code to add username/display_name into
updates only if user.Username != "" and user.DisplayName != "" (or, if you want
to support empty-string updates, change the request DTO to use pointer fields
and check for nil pointers instead), leaving the password update logic as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

可增加首次登陆修改密码

2 participants