Skip to content

feat: enforce first-login admin token change#1

Open
xuyufengfei wants to merge 2 commits intomainfrom
feat/first-login-force-change-token
Open

feat: enforce first-login admin token change#1
xuyufengfei wants to merge 2 commits intomainfrom
feat/first-login-force-change-token

Conversation

@xuyufengfei
Copy link
Copy Markdown
Owner

@xuyufengfei xuyufengfei commented Mar 22, 2026

变更内容

  • 后端新增默认管理员 Token 检测,认证信息接口返回 requirePasswordChange,供前端判断是否必须首登改密。
  • 管理员 Token 修改接口统一要求新 Token 至少 12 位,并禁止改回默认值 change-me-admin-token
  • 前端登录后若命中 requirePasswordChange=true,直接进入强制改密页,改密成功后清理会话并要求重新登录。
  • 设置页改密弹窗同步到 12 位最小长度校验。
  • 新增首登改密定向测试,补齐后端接口与前端强制改密流覆盖。

验证步骤

  1. 安装测试依赖(跳过 Electron 二进制下载):
    ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install --include=dev
  2. 运行定向验证:
    npx vitest run --root . src/server/routes/api/auth.first-login.test.ts src/server/middleware/auth.test.ts src/web/App.first-login.test.tsx
  3. 重点验证场景:
    • 全新安装:默认管理员 Token 登录后返回 requirePasswordChange=true
    • 已有实例升级:自定义管理员 Token 不会被误拦到首登改密页
    • 首次登录设置新密码:必须输入至少 12 位新 Token,且不能继续使用默认值
    • 改密后重新登录:改密成功后清理旧会话,使用新 Token 重新登录

风险说明

  • App 级前端测试当前依赖最小 DOM 宿主桩,后续若扩大 UI 交互覆盖,建议沉淀统一 test setup。
  • 12 位管理员 Token 长度规则目前在前后端各有校验;后续如果调整强度策略,建议抽共享常量以避免漂移。
  • 若后续新增其他登录壳层(例如桌面端特殊入口),需同步接入 requirePasswordChange 门禁。

回归点

  • 普通非默认管理员 Token 登录不受影响
  • 设置页内的管理员 Token 修改流程仍可正常使用
  • 改密成功后 settings 表与运行时配置同步更新

Summary by CodeRabbit

  • New Features

    • Enforced first-login admin token change with an in-app forced token-change flow.
  • Improvements

    • Increased minimum admin token length to 12 characters.
    • Prevented reverting to the factory-default admin token.
    • Simplified login UI, user modal, and shell/mobile navigation behaviors.
  • Removals

    • Removed site announcements page.
  • Tests

    • Added client and server tests covering first-login and token-change behaviors.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds a first-login enforcement flow: the server detects the factory-reset admin token and returns requirePasswordChange: true; the web app blocks normal UI and forces an admin token change (minimum 12 chars, cannot reuse factory token); token persistence and runtime config are updated, and tests cover both API and UI behaviors.

Changes

Cohort / File(s) Summary
Server Auth API
src/server/routes/api/auth.ts, src/server/routes/api/auth.first-login.test.ts
Detect factory-reset admin token, trim inputs, enforce 12-character minimum, reject setting the default token, persist trimmed new token to settings and runtime config, and expose requirePasswordChange. Added tests for /api/settings/auth/info and /api/settings/auth/change.
First-Login UI Flow
src/web/App.tsx, src/web/App.first-login.test.tsx
Introduce ForceChangeTokenPage and client flow that calls api.getAuthInfo() to gate access when requirePasswordChange is true; handle session clearing on expired sessions and after token change. Added UI tests that simulate first-login and session-expiry cases. Removed site-announcements route and simplified related UI elements.
Token Change Validation
src/web/components/ChangeKeyModal.tsx
Client-side validation updated to require minimum 12-character token and placeholder/error text updated. Removed persisting auth session to localStorage after token change.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant App as Web App
    participant API as Auth API
    participant DB as Database
    participant Config as Runtime Config

    User->>App: Log in with token
    App->>API: GET /api/settings/auth/info
    API->>Config: Check current auth token vs factory default
    API-->>App: { requirePasswordChange: true }
    App->>User: Render ForceChangeTokenPage

    User->>App: Submit oldToken, newToken
    App->>App: Trim & validate (>=12, not default)
    App->>API: POST /api/settings/auth/change
    API->>API: Trim & validate inputs
    API->>Config: Verify oldToken matches runtime token
    API->>DB: Persist trimmed new token in settings
    API->>Config: Update runtime authToken
    API-->>App: 200 { success: true, requirePasswordChange: false }

    App->>App: Clear session / redirect to login
    User->>App: Log in with new token
    App->>API: GET /api/settings/auth/info
    API->>Config: Confirm default token no longer in use
    API-->>App: { requirePasswordChange: false }
    App->>User: Show normal app UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled defaults under moonlit light,

A factory token no longer feels right.
Change it now—twelve characters or more,
Watch doors unlock and rabbits cheer galore.
Hoppity security, safe and tight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: enforce first-login admin token change' directly and accurately summarizes the main change: enforcing a required admin token change on first login, which is the core objective across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/first-login-force-change-token

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

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

Copy link
Copy Markdown

@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 (2)
src/web/App.tsx (1)

772-785: ⚠️ Potential issue | 🟠 Major

Keep the user-menu trigger as a real button.

Replacing the trigger with a <div> on Lines 772-785 removes keyboard focus and Enter/Space activation, so keyboard users can no longer open the profile menu. This needs button semantics here.

Suggested fix
-          <div
+          <button
+            type="button"
             className="topbar-avatar"
             aria-label={displayName}
+            aria-haspopup="menu"
+            aria-expanded={showUserMenu}
             onClick={() => {
               setShowUserMenu(!showUserMenu);
               setShowThemeMenu(false);
             }}
           >
@@
-          </div>
+          </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/App.tsx` around lines 772 - 785, The avatar trigger currently uses a
non-interactive <div>, breaking keyboard accessibility; replace it with a real
<button> element that preserves existing behavior: toggle
setShowUserMenu(!showUserMenu) and call setShowThemeMenu(false) on click, keep
aria-label={displayName}, and render the <img> (avatarUrl/ displayName) inside
the button with the same styles; ensure the button has type="button" and retains
the "topbar-avatar" class so styling and JS references remain unchanged.
src/web/components/ChangeKeyModal.tsx (1)

42-48: ⚠️ Potential issue | 🟠 Major

Add a dedicated success callback for token changes.

Lines 43-48 reuse onClose() for the success path, so the parent never learns that the credential changed. The old auth session stays in storage and later API calls keep sending a stale bearer token even though the toast tells the user to log in again. Please surface a distinct success callback that clears the session and returns the user to login.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/components/ChangeKeyModal.tsx` around lines 42 - 48, The modal
currently calls onClose() on a successful api.changeAuthToken which prevents the
parent from clearing the auth session; add a dedicated success callback prop
(e.g., onTokenChangeSuccess) to the ChangeKeyModal component, update its prop
types, and invoke onTokenChangeSuccess() in the success branch instead of
onClose() after showing the toast and resetting local state (setOldToken,
setNewToken, setConfirmToken); leave onClose() for cancel/close behavior so the
parent can implement session clearing and redirect-to-login when
onTokenChangeSuccess is called.
🧹 Nitpick comments (1)
src/web/App.tsx (1)

130-167: Extract the client token policy into one shared helper.

Lines 130-167 now duplicate rules that also exist in src/web/components/ChangeKeyModal.tsx, and the two flows have already diverged: this page blocks the factory-reset token client-side, while the modal leaves that check to the server. A shared validator/constant would keep the settings modal and first-login flow aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/App.tsx` around lines 130 - 167, Extract the client-side admin token
policy into a single shared helper (e.g., export a FACTORY_RESET_ADMIN_TOKEN
constant and a validateAdminToken or getAdminTokenValidationError function) and
import it into both ForceChangeTokenPage and the ChangeKeyModal component; move
the rules currently in ForceChangeTokenPage (non-empty, min length 12, not equal
FACTORY_RESET_ADMIN_TOKEN) into that helper, replace the local
FACTORY_RESET_ADMIN_TOKEN and inline checks in ForceChangeTokenPage with calls
to the helper, and update ChangeKeyModal to use the same helper so both flows
share identical client-side validation while server-side enforcement remains
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/routes/api/auth.first-login.test.ts`:
- Around line 15-17: The test overwrites process.env.DATA_DIR in beforeAll
without restoring it, causing order-dependent failures; modify the
setup/teardown to capture the original value (e.g., const previousDataDir =
process.env.DATA_DIR) before assigning process.env.DATA_DIR =
mkdtempSync(join(tmpdir(), 'metapi-auth-first-login-')) in beforeAll, and in
afterAll restore it (process.env.DATA_DIR = previousDataDir or delete
process.env.DATA_DIR if previousDataDir was undefined) while still performing
the existing temp-directory cleanup so the environment is exactly as it was
before the test; update the beforeAll/afterAll blocks in
auth.first-login.test.ts accordingly and reference the same captured variable
name in both hooks.

In `@src/server/routes/api/auth.ts`:
- Around line 11-13: Normalize (trim) both the configured token and any
incoming/submitted token before comparing them: update
isDefaultAdminTokenInUse() to trim config.authToken consistently, and change the
token-equality checks in the handlers for /api/settings/auth/info and
/api/settings/auth/change (the code that compares the incoming token to
config.authToken) to compare trimmed versions of both values (e.g., use
(config.authToken || '').trim() === (incomingToken || '').trim()). Ensure every
place that checks equality against FACTORY_RESET_ADMIN_TOKEN or config.authToken
uses the same trimmed normalization.

In `@src/web/App.first-login.test.tsx`:
- Around line 56-106: The test overwrites global DOM bindings in beforeEach
(localStorage, window, document, Node, Element, HTMLElement, Text,
MutationObserver) but never restores them; save the original global
descriptors/values before you replace them (e.g., origLocalStorage, origWindow,
origDocument, origNode, origElement, origHTMLElement, origText,
origMutationObserver) and then in afterEach restore each original via
Object.defineProperty or direct assignment and then call vi.clearAllMocks();
update the tests around the existing beforeEach/afterEach and the mocked helpers
(apiMock.getEvents) so restored globals are re-used by later tests.

In `@src/web/App.tsx`:
- Around line 581-593: In loadAuthInfo's catch branch that detects error.message
=== 'Session expired', don't leave the app in an authenticated state; instead
clear the stored session and force the app back to the login screen by invoking
the app's logout/clear-auth routine (e.g., remove stored token/session, call the
existing logout function or setAuthed(false)), and ensure any related state like
requirePasswordChange is not used to render protected UI; keep the existing
setAuthInfoLoaded(true) behavior but make sure authed is set false so the shell
doesn't render with a stale token.

---

Outside diff comments:
In `@src/web/App.tsx`:
- Around line 772-785: The avatar trigger currently uses a non-interactive
<div>, breaking keyboard accessibility; replace it with a real <button> element
that preserves existing behavior: toggle setShowUserMenu(!showUserMenu) and call
setShowThemeMenu(false) on click, keep aria-label={displayName}, and render the
<img> (avatarUrl/ displayName) inside the button with the same styles; ensure
the button has type="button" and retains the "topbar-avatar" class so styling
and JS references remain unchanged.

In `@src/web/components/ChangeKeyModal.tsx`:
- Around line 42-48: The modal currently calls onClose() on a successful
api.changeAuthToken which prevents the parent from clearing the auth session;
add a dedicated success callback prop (e.g., onTokenChangeSuccess) to the
ChangeKeyModal component, update its prop types, and invoke
onTokenChangeSuccess() in the success branch instead of onClose() after showing
the toast and resetting local state (setOldToken, setNewToken, setConfirmToken);
leave onClose() for cancel/close behavior so the parent can implement session
clearing and redirect-to-login when onTokenChangeSuccess is called.

---

Nitpick comments:
In `@src/web/App.tsx`:
- Around line 130-167: Extract the client-side admin token policy into a single
shared helper (e.g., export a FACTORY_RESET_ADMIN_TOKEN constant and a
validateAdminToken or getAdminTokenValidationError function) and import it into
both ForceChangeTokenPage and the ChangeKeyModal component; move the rules
currently in ForceChangeTokenPage (non-empty, min length 12, not equal
FACTORY_RESET_ADMIN_TOKEN) into that helper, replace the local
FACTORY_RESET_ADMIN_TOKEN and inline checks in ForceChangeTokenPage with calls
to the helper, and update ChangeKeyModal to use the same helper so both flows
share identical client-side validation while server-side enforcement remains
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ca7c504-93b9-4f77-84a1-d57e71740e64

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc6b7b and f4dc7f0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • src/server/routes/api/auth.first-login.test.ts
  • src/server/routes/api/auth.ts
  • src/web/App.first-login.test.tsx
  • src/web/App.tsx
  • src/web/components/ChangeKeyModal.tsx

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
src/web/App.first-login.test.tsx (2)

129-159: Consider adding a test for successful token change completion.

The apiMock.changeAuthToken mock is defined (line 10) but never exercised. Given the PR objective states "on successful change the session is cleared and re-login is required," consider adding a test that:

  1. Sets requirePasswordChange: true
  2. Simulates the token change submission
  3. Verifies changeAuthToken was called with expected parameters
  4. Confirms auth tokens are cleared and login UI is shown

This would increase coverage of the complete first-login flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/App.first-login.test.tsx` around lines 129 - 159, Add a new test that
completes the first-login token change flow: mock apiMock.getAuthInfo to return
{ requirePasswordChange: true }, render <App /> (same pattern using
create/act/flush), simulate submitting the token-change form so that
apiMock.changeAuthToken is invoked (assert apiMock.changeAuthToken was called
with the expected payload), then assert storage.getItem('auth_token') and
storage.getItem('auth_token_expires_at') are null and the UI shows the sign-in
elements (e.g., contains 'Admin Token' and 'Sign In'); ensure the test follows
the same setup/teardown pattern as the existing tests and references
apiMock.changeAuthToken, requirePasswordChange, and storage.getItem for clarity.

37-45: Unused dump() method.

The dump() method on the storage helper is defined but never used in the tests. Consider removing it to keep the helper minimal, or add a comment if it's intended for debugging purposes.

♻️ Proposed fix
 function createStorage(initial: Record<string, string> = {}) {
   const store = new Map(Object.entries(initial));
   return {
     getItem: (key: string) => store.has(key) ? store.get(key)! : null,
     setItem: (key: string, value: string) => void store.set(key, value),
     removeItem: (key: string) => void store.delete(key),
-    dump: () => Object.fromEntries(store.entries()),
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/App.first-login.test.tsx` around lines 37 - 45, The helper function
createStorage exposes an unused dump() method; remove dump from the returned
object (i.e., stop returning dump in createStorage) to keep the helper minimal,
and ensure no tests rely on it (if any do, either update those tests or instead
replace dump with a short comment noting it was intentionally omitted for
brevity). Reference: createStorage and dump.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/web/App.first-login.test.tsx`:
- Around line 129-159: Add a new test that completes the first-login token
change flow: mock apiMock.getAuthInfo to return { requirePasswordChange: true },
render <App /> (same pattern using create/act/flush), simulate submitting the
token-change form so that apiMock.changeAuthToken is invoked (assert
apiMock.changeAuthToken was called with the expected payload), then assert
storage.getItem('auth_token') and storage.getItem('auth_token_expires_at') are
null and the UI shows the sign-in elements (e.g., contains 'Admin Token' and
'Sign In'); ensure the test follows the same setup/teardown pattern as the
existing tests and references apiMock.changeAuthToken, requirePasswordChange,
and storage.getItem for clarity.
- Around line 37-45: The helper function createStorage exposes an unused dump()
method; remove dump from the returned object (i.e., stop returning dump in
createStorage) to keep the helper minimal, and ensure no tests rely on it (if
any do, either update those tests or instead replace dump with a short comment
noting it was intentionally omitted for brevity). Reference: createStorage and
dump.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a31df01-cb7d-42f8-99ad-20e1e9fa37a9

📥 Commits

Reviewing files that changed from the base of the PR and between f4dc7f0 and 3657595.

📒 Files selected for processing (4)
  • src/server/routes/api/auth.first-login.test.ts
  • src/server/routes/api/auth.ts
  • src/web/App.first-login.test.tsx
  • src/web/App.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/server/routes/api/auth.ts
  • src/server/routes/api/auth.first-login.test.ts
  • src/web/App.tsx

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