Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion backend/app/api/endpoints/admin/system_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

"""Admin system configuration endpoints."""

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.api.dependencies import get_db
Expand Down Expand Up @@ -229,9 +229,20 @@ async def mark_admin_setup_complete(
Mark admin setup wizard as completed.
This will prevent the wizard from showing on subsequent admin logins.

Requires that the admin password has been changed from default.

Returns:
AdminSetupCompleteResponse: Contains success status and message
"""
# Verify admin password has been changed from default
from app.services.admin_utils import is_admin_password_default

if is_admin_password_default(db):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Please change the default password before completing setup",
)

config = (
db.query(SystemConfig)
.filter(SystemConfig.config_key == ADMIN_SETUP_CONFIG_KEY)
Expand Down
6 changes: 6 additions & 0 deletions backend/app/api/endpoints/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ def health_check(db: Session = Depends(get_db)):
result = db.execute(text("SELECT COUNT(*) FROM users"))
user_count = result.scalar()

# Check if admin password has been changed from default
from app.services.admin_utils import is_admin_password_default

admin_password_changed = not is_admin_password_default(db)

return {
"status": "healthy",
"database": "connected",
"users_initialized": user_count > 0,
"user_count": user_count,
"shutting_down": shutdown_manager.is_shutting_down,
"admin_password_changed": admin_password_changed,
}
Comment on lines +38 to 50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that /health is indeed unauthenticated by checking dependencies
rg -n 'def health_check|@router.get\("/health"\)' backend/app/api/endpoints/health.py -A 5

Repository: wecode-ai/Wegent

Length of output: 355


🏁 Script executed:

#!/bin/bash
# Find the default admin password hash
rg -n 'DEFAULT_ADMIN_Password_HASH' backend/ -A 2 -B 2

Repository: wecode-ai/Wegent

Length of output: 3051


🏁 Script executed:

#!/bin/bash
# Search for uses of the health endpoint in frontend code
fd -e tsx -e ts -e jsx -e js . | xargs rg -l '/health' 2>/dev/null | head -20

Repository: wecode-ai/Wegent

Length of output: 287


🏁 Script executed:

#!/bin/bash
# Check if there are other endpoints that expose similar information
rg -n '@router.get\(' backend/app/api/endpoints/health.py -A 3

Repository: wecode-ai/Wegent

Length of output: 670


🏁 Script executed:

#!/bin/bash
# Check how LoginForm.tsx uses the /health endpoint
cat frontend/src/features/login/components/LoginForm.tsx | grep -n -A 10 -B 5 '/health'

Repository: wecode-ai/Wegent

Length of output: 756


🏁 Script executed:

#!/bin/bash
# Check if admin_password_changed is used in LoginForm
cat frontend/src/features/login/components/LoginForm.tsx | grep -n -A 5 -B 5 'admin_password_changed'

Repository: wecode-ai/Wegent

Length of output: 548


Security & code duplication: Exposing admin_password_changed on unauthenticated endpoint with duplicated logic across endpoints.

The /health endpoint is unauthenticated, exposing admin_password_changed publicly. This allows attackers to probe whether default credentials (admin/Wegent2025!) are still active. While this is an intentional design choice for the login page to auto-fill credentials (verified in frontend/src/features/login/components/LoginForm.tsx), it creates an information disclosure vector that should be explicitly documented as an approved security trade-off.

Additionally, the admin password check logic is duplicated across at least three endpoints:

  • backend/app/api/endpoints/health.py (lines 38-47)
  • backend/app/api/endpoints/users.py (lines 346-355)
  • backend/app/api/endpoints/admin/system_config.py (lines 238-241)

Extract this into a shared utility function to eliminate duplication and ensure consistent behavior:

# backend/app/core/admin_password.py
def is_admin_password_default(db: Session) -> bool:
    """Check if admin user still uses default password."""
    from app.core.yaml_init import DEFAULT_ADMIN_Password_HASH
    from app.models.user import User
    
    admin_user = db.query(User).filter(User.user_name == "admin").first()
    if admin_user:
        return admin_user.password_hash == DEFAULT_ADMIN_Password_HASH
    return True  # No admin user exists

Then replace duplicated logic in all three endpoints with calls to this utility.

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 49-56: Consider moving this statement to an else block

(TRY300)

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

In `@backend/app/api/endpoints/health.py` around lines 38 - 56, The health
endpoint currently computes admin_password_changed inline
(admin_password_changed) and duplicates that same logic in other places; extract
the check into a shared utility function named is_admin_password_default(db:
Session) that returns True/False based on comparing User.password_hash for
user_name "admin" against DEFAULT_ADMIN_Password_HASH (and returns True if no
admin user exists), then replace the inline logic in the health endpoint
(admin_password_changed assignment) and the duplicated checks in users.py and
admin/system_config.py with calls to is_admin_password_default(db) (negating as
needed to preserve the existing admin_password_changed boolean) so all three
locations use the single utility.

except Exception as e:
return {"status": "unhealthy", "database": "error", "error": str(e)}
Expand Down
40 changes: 39 additions & 1 deletion backend/app/api/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
QuickAccessTeam,
WelcomeConfigResponse,
)
from app.schemas.user import UserCreate, UserInDB, UserUpdate
from app.schemas.user import ChangePasswordRequest, UserCreate, UserInDB, UserUpdate
from app.services.kind import kind_service
from app.services.user import user_service

Expand Down Expand Up @@ -80,6 +80,36 @@ async def update_current_user_endpoint(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.put("/me/password", response_model=UserInDB)
async def change_password(
request: ChangePasswordRequest,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Change current user's password"""
if request.new_password != request.confirm_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Passwords do not match",
)

# Prevent admin from "changing" to the same default password
if current_user.user_name == "admin":
from app.core.yaml_init import DEFAULT_ADMIN_Password

if request.new_password == DEFAULT_ADMIN_Password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password cannot be the same as the default password",
)

# Hash and update the password
current_user.password_hash = security.get_password_hash(request.new_password)
db.commit()
db.refresh(current_user)
return current_user
Comment on lines +83 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Best practices for password change endpoint security requirements

💡 Result:

Password change endpoint (authenticated user) — security best practices

1) Require strong proof it’s really the user

  • Require the current password + new password + confirmation (and verify the current password server-side). [2], [5]
  • Use step-up / re-authentication for “sensitive actions” (password change, email/MFA changes), especially if sessions can be long-lived (mitigates stolen-cookie/session hijack). [6]
  • If MFA is enabled, consider requiring MFA for the change (step-up), not just for login. [6]

2) Prevent abuse (brute force/DoS)

  • Apply rate limiting and lockout / throttling on the current-password verification attempts (treat like an authentication surface). [7]
  • Add monitoring/alerting for repeated failures and unusual patterns (IP/device). [7]

3) Make password selection resistant to guessing and breaches

  • On password set/change, check the new password against a blocklist of common/expected/compromised passwords. [1]
  • Avoid outdated rules like mandatory periodic rotation; prefer event-driven changes (e.g., compromise) and encourage MFA. [3], [1]

4) Protect the endpoint itself

  • Enforce TLS end-to-end (never accept password changes over plaintext).
  • If cookie-based browser session: require CSRF protection (e.g., same-site cookies + CSRF token).
  • Return generic error messages (don’t reveal whether “current password” was close/valid beyond a simple failure).

5) Secure lifecycle after the change

  • Invalidate existing sessions/refresh tokens (at least all other sessions; often also the current one depending on risk). [6]
  • Send out-of-band notification (“Your password was changed”) to help detect account takeover. [3]
  • Write audit logs for password-change attempts/success (who/when/IP/device), protected from tampering.

6) Avoid authorization mistakes

  • Ensure the endpoint can only change the password for the authenticated account (no user ID in the request that could enable IDOR).
  • For admin-initiated resets vs. user-initiated changes: separate flows and controls; treat reset/forgot-password as an auth mechanism with equal rigor. [7]

Sources

  • NIST Digital Identity Guidelines (SP 800-63-4 / 63B successor): blocklist checks and guidance around password changes. [1]
  • OWASP ASVS: secure credential change requirements (include old password, confirmation). [2], [5]
  • OWASP Cheat Sheets / WSTG / API Security Top 10: re-authentication for sensitive changes and treating reset flows as auth surfaces. [3], [6], [7]

🏁 Script executed:

# Find the ChangePasswordRequest model definition
rg "class ChangePasswordRequest" --type py

Repository: wecode-ai/Wegent

Length of output: 127


🏁 Script executed:

# Search for where the change_password endpoint is called or documented
rg "change_password|/me/password" --type py -B 2 -A 2

Repository: wecode-ai/Wegent

Length of output: 422


🏁 Script executed:

# Check for rate limiting or security middleware
rg "rate_limit|RateLimiter|throttle" --type py

Repository: wecode-ai/Wegent

Length of output: 8207


🏁 Script executed:

# Search for session invalidation logic after password change
rg "session|Session|refresh_token|clear_cookies" backend/app/api/endpoints/users.py -B 2 -A 2

Repository: wecode-ai/Wegent

Length of output: 1480


🏁 Script executed:

# Check for audit logging or security event tracking
rg "audit|log_event|security_log" --type py

Repository: wecode-ai/Wegent

Length of output: 847


🏁 Script executed:

# Look at the full users.py file to understand the context of the endpoint
wc -l backend/app/api/endpoints/users.py

Repository: wecode-ai/Wegent

Length of output: 98


🏁 Script executed:

# View the ChangePasswordRequest model to see what fields it requires
cat -n backend/app/schemas/user.py | grep -A 10 "class ChangePasswordRequest"

Repository: wecode-ai/Wegent

Length of output: 277


🏁 Script executed:

# View the complete change_password endpoint implementation
sed -n '83,120p' backend/app/api/endpoints/users.py

Repository: wecode-ai/Wegent

Length of output: 1397


🏁 Script executed:

# Check if rate limiting is applied to the password change endpoint
sed -n '75,105p' backend/app/api/endpoints/users.py

Repository: wecode-ai/Wegent

Length of output: 1028


🏁 Script executed:

# Check for session/token invalidation after password change
rg "invalidate|revoke|token|session" backend/app/api/endpoints/users.py -B 2 -A 2

Repository: wecode-ai/Wegent

Length of output: 648


🏁 Script executed:

# Look at security module to understand what get_current_user provides
fd "security" --type f backend/app | head -10

Repository: wecode-ai/Wegent

Length of output: 135


🏁 Script executed:

# View the security module to understand authentication and token handling
cat backend/app/core/security.py | head -100

Repository: wecode-ai/Wegent

Length of output: 3592


🏁 Script executed:

# Check if there's any token refresh or invalidation logic after password change
rg "password|token|refresh" backend/app/core/security.py -B 2 -A 2

Repository: wecode-ai/Wegent

Length of output: 10906


🏁 Script executed:

# Look for any password change event handlers or callbacks
rg "change_password|password.*change" --type py

Repository: wecode-ai/Wegent

Length of output: 1546


🏁 Script executed:

# Check if rate limiting is defined anywhere for sensitive endpoints
cat backend/app/core/rate_limit.py

Repository: wecode-ai/Wegent

Length of output: 3168


Add current password verification to password change endpoint.

The endpoint currently allows password changes without verifying the current password, which violates OWASP ASVS security standards. This creates security risks:

  • Session hijacking or unattended devices could result in unauthorized password changes
  • No protection against brute force attempts (endpoint lacks rate limiting)
  • Existing tokens remain valid after password change, extending attack window

Add an old_password field to ChangePasswordRequest and verify it before allowing the change. Additionally:

  • Apply rate limiting to this endpoint using the @limiter decorator (framework already exists)
  • Consider invalidating existing tokens after a password change
  • Add audit logging for password change events

If this endpoint is only for initial admin setup (where the default password is well-known), document this constraint and consider a separate, more restrictive flow for general user password changes.

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 86-86: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 87-87: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

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

In `@backend/app/api/endpoints/users.py` around lines 83 - 100, The
change_password endpoint currently allows password updates without verifying the
current password; update the ChangePasswordRequest model to include an
old_password field and, inside change_password, call the existing password
verification helper (e.g., security.verify_password or equivalent) to check
request.old_password against current_user.password_hash and raise HTTP 400/401
on mismatch before hashing the new password; add the `@limiter`(...) decorator to
the change_password route to apply rate limiting; after successful password
update (before/after db.commit()) trigger token invalidation for the user (e.g.,
increment a token_version on User or call
security.invalidate_user_tokens(current_user)) and write an audit log entry for
the password change event (using the app logger/processLogger) noting user id
and timestamp; ensure db.commit() persists both the new password_hash and any
token_version change and db.refresh(current_user).



@router.delete("/me/git-token/{git_domain:path}", response_model=UserInDB)
async def delete_git_token(
git_domain: str,
Expand Down Expand Up @@ -312,6 +342,7 @@ async def get_welcome_config(

# Check admin setup status for admin users
admin_setup_completed = None
admin_password_changed = None
if current_user.role == "admin":
setup_config = (
db.query(SystemConfig)
Expand All @@ -323,6 +354,11 @@ async def get_welcome_config(
else:
admin_setup_completed = False

# Check if admin password has been changed from default
from app.services.admin_utils import is_admin_password_default

admin_password_changed = not is_admin_password_default(db)

if not config:
# Return default configuration
return WelcomeConfigResponse(
Expand All @@ -331,6 +367,7 @@ async def get_welcome_config(
],
tips=[ChatTipItem(**tip) for tip in DEFAULT_SLOGAN_TIPS_CONFIG["tips"]],
admin_setup_completed=admin_setup_completed,
admin_password_changed=admin_password_changed,
)

config_value = config.config_value or {}
Expand All @@ -344,6 +381,7 @@ async def get_welcome_config(
for tip in config_value.get("tips", DEFAULT_SLOGAN_TIPS_CONFIG["tips"])
],
admin_setup_completed=admin_setup_completed,
admin_password_changed=admin_password_changed,
)


Expand Down
10 changes: 9 additions & 1 deletion backend/app/core/yaml_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@

logger = logging.getLogger(__name__)

# Default admin password plaintext - used to verify if admin still uses default credentials
DEFAULT_ADMIN_Password = "Wegent2025!"

# Default admin password hash for "Wegent2025!" - used for initial user creation
DEFAULT_ADMIN_Password_HASH = (
"$2b$12$5jQMrJGO8NMXmF90f/xnKeLtM/Deh912k4GRPx.q3nTGOg1e1IJzW"
)


def load_yaml_documents(file_path: Path) -> List[Dict[str, Any]]:
"""
Expand Down Expand Up @@ -64,7 +72,7 @@ def ensure_default_user(db: Session) -> tuple[int, bool]:
# Default admin user (admin/Wegent2025!)
admin_user = User(
user_name="admin",
password_hash="$2b$12$5jQMrJGO8NMXmF90f/xnKeLtM/Deh912k4GRPx.q3nTGOg1e1IJzW",
password_hash=DEFAULT_ADMIN_Password_HASH,
email="[email protected]",
git_info=[],
is_active=True,
Expand Down
4 changes: 4 additions & 0 deletions backend/app/schemas/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ class WelcomeConfigResponse(BaseModel):
default=None,
description="Whether admin setup wizard has been completed (only returned for admin users)",
)
admin_password_changed: Optional[bool] = Field(
default=None,
description="Whether admin password has been changed from default (only returned for admin users)",
)


# Public Retriever Management Schemas
Expand Down
9 changes: 8 additions & 1 deletion backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator


class MCPProviderKeys(BaseModel):
Expand Down Expand Up @@ -167,3 +167,10 @@ class CLIPollResponse(BaseModel):
access_token: Optional[str] = None
username: Optional[str] = None
error: Optional[str] = None


class ChangePasswordRequest(BaseModel):
"""Password change request model"""

new_password: str = Field(..., min_length=6)
confirm_password: str = Field(..., min_length=6)
32 changes: 32 additions & 0 deletions backend/app/services/admin_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""Utility functions for admin user operations."""

from sqlalchemy.orm import Session

from app.core.security import verify_password
from app.core.yaml_init import DEFAULT_ADMIN_Password
from app.models.user import User


def is_admin_password_default(db: Session) -> bool:
"""
Check if the admin user's password is still the default value.

Uses bcrypt verification to compare the known default plaintext password
against the stored hash. This correctly handles re-hashed passwords
(same plaintext with different salt) unlike direct hash comparison.

Args:
db: Database session

Returns:
True if the admin password is still the default, False if changed
or if the admin user does not exist.
"""
admin_user = db.query(User).filter(User.user_name == "admin").first()
if not admin_user:
return False
return verify_password(DEFAULT_ADMIN_Password, admin_user.password_hash)
Comment on lines +29 to +32
Copy link
Copy Markdown
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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== UserUpdate schema =="
rg -n -C3 'class UserUpdate|user_name' backend/app/schemas/user.py

echo
echo "== update_current_user implementation =="
rg -n -C5 'def update_current_user|user_name' backend/app/services/user.py

echo
echo "== User model definition =="
rg -n -C3 'user_name' backend/app/models/user.py

Repository: wecode-ai/Wegent

Length of output: 5626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for admin account initialization/seeding =="
rg -n "admin" backend/app/models/user.py backend/app/db/init_db.py 2>/dev/null | head -30

echo
echo "== Check User model for id/role/flags =="
rg -n -A20 'class User\(' backend/app/models/user.py | head -40

echo
echo "== Check if admin check exists elsewhere with stable identifier =="
rg -n 'admin' backend/app/api/endpoints/users.py | grep -i 'def\|==\|admin' | head -20

Repository: wecode-ai/Wegent

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find User model file =="
fd -t f 'user\.py' backend/app/models/ 2>/dev/null

echo
echo "== Find all db initialization files =="
fd -t f '*.py' backend/app/db/ 2>/dev/null | head -20

echo
echo "== Search entire backend for 'admin' mentions in initialization context =="
rg -n "admin" backend/app/ -t py | grep -i "create\|seed\|init\|default" | head -20

Repository: wecode-ai/Wegent

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Read User model =="
cat -n backend/app/models/user.py

echo
echo
echo "== Search for admin user initialization =="
rg -n "admin" backend/app/ -t py --max-count=30

Repository: wecode-ai/Wegent

Length of output: 37366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if role field is updated in update_current_user =="
sed -n '172,220p' backend/app/services/user.py

echo
echo
echo "== Check UserUpdate schema for role field =="
rg -n 'class UserUpdate' -A15 backend/app/schemas/user.py | head -25

echo
echo
echo "== Check if role is ever set in user updates =="
rg -n 'user.role|obj_in.role' backend/app/services/user.py

Repository: wecode-ai/Wegent

Length of output: 2558


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find all locations checking user_name == 'admin' or similar =="
rg -n "user_name.*==.*admin|==.*\"admin\".*user_name" backend/app/ -t py

echo
echo
echo "== Check yaml_init for admin user creation to see if ID is stable =="
sed -n '59,90p' backend/app/core/yaml_init.py

Repository: wecode-ai/Wegent

Length of output: 1457


Use the immutable role field instead of username to identify the admin account.

The admin identity check depends on user_name == "admin", but this account can be renamed via update_current_user() (line 196 of user.py), which has no admin-specific protection. If renamed, three critical locations will break:

  • is_admin_password_default() (admin_utils.py:29) will fail to find the user
  • ensure_admin_user_exists() (yaml_init.py:68) will attempt to create a duplicate
  • Password reset guard (users.py:97) will become ineffective

Switch all three checks to use the immutable role == "admin" field, which already exists and is safely used in get_admin_user() (security.py:332).

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

In `@backend/app/services/admin_utils.py` around lines 29 - 32, The admin lookup
currently queries by user_name ("admin") in is_admin_password_default
(admin_utils.py), which breaks if username is changed; change the query to
filter on the immutable role field (role == "admin") instead. Update the same
logic in ensure_admin_user_exists (yaml_init.py) and the password reset guard in
users.py to use role == "admin" for identifying the admin account, mirroring how
get_admin_user (security.py) locates the admin; ensure any returned variable
names (e.g., admin_user) and subsequent checks (like
verify_password(DEFAULT_ADMIN_Password, admin_user.password_hash)) remain valid
after switching the filter.

13 changes: 12 additions & 1 deletion frontend/e2e/config/test-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,23 @@ export interface TestUser {
description: string
}

/**
* Default admin password (before change) - used only for initial login in global-setup
*/
export const DEFAULT_ADMIN_Password = 'Wegent2025!'

/**
* E2E admin password (after change) - used for all API calls after setup
*/
export const E2E_ADMIN_Password = 'WegentE2E2025!'

/**
* Default admin user for E2E tests
* Note: password is the E2E password (changed during global-setup), not the default
*/
export const ADMIN_USER: TestUser = {
username: 'admin',
password: 'Wegent2025!',
password: E2E_ADMIN_Password,
role: 'admin',
description: 'Default admin user with full access',
}
Expand Down
18 changes: 16 additions & 2 deletions frontend/e2e/tests/admin/admin-public-models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,23 @@ test.describe('Admin - Public Model Management', () => {
await apiClient.login(ADMIN_USER.username, ADMIN_USER.password)

// Mark admin setup as complete via API to prevent GlobalAdminSetupWizard from showing
await apiClient.markAdminSetupComplete().catch(() => {
// Ignore errors - setup may already be complete
// First change password (required before setup can be completed)
const passwordResult = await apiClient
.changeAdminPassword(ADMIN_USER.password, ADMIN_USER.password)
.catch((error: Error) => {
console.log(`password change request failed (may already be changed): ${error.message}`)
return null
})
Comment on lines +19 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't ignore setup-precondition failures in E2E beforeEach.

This .catch(() => {}) lets the test continue even when the admin password was not changed, which hides failures in the new wizard gate instead of surfacing them immediately. Based on learnings: "E2E tests MUST NOT silently fail: DO NOT catch errors and return early without failing the test, every assertion MUST pass for test to succeed".

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

In `@frontend/e2e/tests/admin/admin-public-models.spec.ts` around lines 19 - 24,
Remove the silent catch so setup failures surface: replace the current awaited
call with a direct await of apiClient.changeAdminPassword(ADMIN_USER.password,
ADMIN_USER.password) (i.e., remove the .catch(() => {}) wrapper) inside the
beforeEach so any error will throw and fail the test; if you need idempotency,
explicitly check a precondition endpoint or assert the specific expected error
instead of swallowing it.

if (passwordResult && passwordResult.status >= 400) {
console.log(`password change API returned ${passwordResult.status} - may already be changed`)
}
const setupResult = await apiClient.markAdminSetupComplete().catch((error: Error) => {
console.log(`Setup complete request failed (may already be complete): ${error.message}`)
return null
})
if (setupResult && setupResult.status >= 400) {
console.log(`Setup complete API returned ${setupResult.status} - may already be complete`)
}

// Navigate directly to admin page (already authenticated via global setup storageState)
await adminPage.navigateToTab('public-models')
Expand Down
18 changes: 16 additions & 2 deletions frontend/e2e/tests/admin/admin-users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,23 @@ test.describe('Admin - User Management', () => {
await apiClient.login(ADMIN_USER.username, ADMIN_USER.password)

// Mark admin setup as complete via API to prevent GlobalAdminSetupWizard from showing
await apiClient.markAdminSetupComplete().catch(() => {
// Ignore errors - setup may already be complete
// First change password (required before setup can be completed)
const passwordResult = await apiClient
.changeAdminPassword(ADMIN_USER.password, ADMIN_USER.password)
.catch((error: Error) => {
console.log(`password change request failed (may already be changed): ${error.message}`)
return null
})
Comment on lines +20 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't swallow the password-change precondition in beforeEach.

If this request fails, the test still proceeds as if the admin setup gate was cleared. That makes regressions in the new mandatory password step show up as flaky UI behavior instead of a hard test failure.

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

In `@frontend/e2e/tests/admin/admin-users.spec.ts` around lines 20 - 25, The
current beforeEach swallows all errors from
apiClient.changeAdminPassword(ADMIN_USER.password, ADMIN_USER.password) which
masks failures; modify the call in the beforeEach so that you only ignore a
known "password already changed" success condition and otherwise let the error
bubble (or explicitly fail the test). Specifically, replace the generic
.catch(() => {}) around changeAdminPassword with error inspection in the promise
rejection from apiClient.changeAdminPassword (or await in try/catch): if the
error/message/status indicates "already changed" treat it as a no-op, otherwise
rethrow or fail the test so failures in changeAdminPassword cause the test to
error; keep references to apiClient.changeAdminPassword and ADMIN_USER.password
to locate the code.

if (passwordResult && passwordResult.status >= 400) {
console.log(`password change API returned ${passwordResult.status} - may already be changed`)
}
const setupResult = await apiClient.markAdminSetupComplete().catch((error: Error) => {
console.log(`Setup complete request failed (may already be complete): ${error.message}`)
return null
})
if (setupResult && setupResult.status >= 400) {
console.log(`Setup complete API returned ${setupResult.status} - may already be complete`)
}

// Navigate directly to admin page (already authenticated via global setup storageState)
await adminPage.navigateToTab('users')
Expand Down
10 changes: 5 additions & 5 deletions frontend/e2e/tests/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test'
import { TEST_USER } from '../utils/auth'
import { ADMIN_USER } from '../config/test-users'

test.describe('Authentication', () => {
test.use({ storageState: { cookies: [], origins: [] } })
Expand All @@ -24,11 +24,11 @@ test.describe('Authentication', () => {
await page
.locator('input[name="user_name"], input[name="username"], input[type="text"]')
.first()
.fill(TEST_USER.username)
.fill(ADMIN_USER.username)
await page
.locator('input[name="password"], input[type="password"]')
.first()
.fill(TEST_USER.password)
.fill(ADMIN_USER.password)

// Submit form
await page.locator('button[type="submit"]').click()
Expand Down Expand Up @@ -93,8 +93,8 @@ test.describe('Logout', () => {
.first()
const passwordInput = page.locator('input[name="password"], input[type="password"]').first()

await usernameInput.fill(TEST_USER.username)
await passwordInput.fill(TEST_USER.password)
await usernameInput.fill(ADMIN_USER.username)
await passwordInput.fill(ADMIN_USER.password)
await page.locator('button[type="submit"]').click()

// Wait for login to complete
Expand Down
Loading