Skip to content

feat: add session login + per-user API keys to NutWatch - #50

Merged
JuanCF merged 4 commits into
mainfrom
feat/auth-system
Jul 4, 2026
Merged

feat: add session login + per-user API keys to NutWatch#50
JuanCF merged 4 commits into
mainfrom
feat/auth-system

Conversation

@JuanCF

@JuanCF JuanCF commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Replace the single-secret NUTWATCH_API_KEY env var with a proper authentication system: accounts (admin/viewer roles) with session login for the dashboard and per-user API keys for scripts. The app stays fully open (bootstrap mode) until the first admin account is created via the first-run Setup page or manage.py create-admin.

Backend:

  • New services/auth_db.py: SQLite store for accounts and API keys (pbkdf2:sha256 passwords, SHA-256 hashed keys, soft-delete revoke)
  • Rewritten auth.py: resolve_principal() checks session cookie then Bearer key; @require_admin/@require_auth/@require_admin_strict decorators with bootstrap-open fallback when zero accounts exist
  • New routes/auth.py: setup, login, logout, /me, accounts CRUD, API key CRUD endpoints
  • New manage.py: create-admin, reset-password, list-accounts CLI for bootstrap and lockout recovery
  • config.py: removed NUTWATCH_API_KEY; added NUTWATCH_SECRET_KEY (auto-generated and persisted in auth DB) and NUTWATCH_SESSION_COOKIE_SECURE
  • All existing route decorators updated: read endpoints use @require_auth (any authenticated principal), mutating endpoints stay @require_admin; viewer role gets 403 on writes

Frontend:

  • AuthProvider: loads auth status, gates App.tsx rendering between Setup -> Login -> AppLayout based on bootstrap state
  • New Setup.tsx (first-run admin creation with Skip), Login.tsx, Accounts.tsx (admin CRUD), AccountModal.tsx, ApiKeys.tsx (self- service), ApiKeyModal.tsx (shows raw key once), LogoMark.tsx
  • api.ts: 401 handler resets account state mid-session; optional in-memory bearer token fallback for non-cookie clients
  • Role-gated UI: Dashboard System Actions, UpsCard/UpsDevices mutations, Notifications editor, ConfigFiles save, HooksSection editor, Users CRUD, WakeOnLan targets/mappings all hidden for viewers
  • Sidebar: user chip with role badge + logout, Accounts and API Keys nav entries
  • Users tab renamed to NUT Users with info box distinguishing from dashboard Accounts

Tests:

  • New test_services_auth_db.py (account/key CRUD, login, revoke)
  • New test_routes_auth.py (setup, login, rate-limiting, accounts, apikeys)
  • New test_manage_cli.py (create-admin, reset-password, list-accounts)
  • Rewritten test_auth.py: session, Bearer key, bootstrap-open, revoked keys, inactive accounts, key-inherits-owner-role, role gating
  • test_routes.py: viewer-role coverage for all read/mutate endpoints
  • Frontend: AuthProvider, Setup, Login, Accounts, ApiKeys, updated Dashboard/WakeOnLan/UpsCard tests

Docs: updated AGENTS.md, README.md, CONTRIBUTING.md, .coderabbit.yaml; added docs/auth-plan.md; updated docs/modularization-plan.md

Summary by CodeRabbit

  • New Features
    • Replaced legacy single-key authentication with account-based sign-in (setup/login/logout) and a bootstrap flow that starts fully open until the first admin exists.
    • Added admin-managed accounts and per-user API key creation, listing, and revocation, with new “API Keys”/“Accounts” navigation.
    • Added viewer vs admin UI gating across pages.
    • Added a restart prompt/modal after saves that require a refresh.
  • Bug Fixes
    • Improved handling of unauthorized access and made additional read-only pages available to authenticated non-admins.
    • Better user-facing API error messages.
  • Documentation
    • Updated setup/install and auth documentation to match the new model.
  • Tests
    • Expanded auth, API key, and UI coverage.

Replace the single-secret NUTWATCH_API_KEY env var with a proper
authentication system: accounts (admin/viewer roles) with session
login for the dashboard and per-user API keys for scripts. The app
stays fully open (bootstrap mode) until the first admin account is
created via the first-run Setup page or manage.py create-admin.

Backend:
- New services/auth_db.py: SQLite store for accounts and API keys
  (pbkdf2:sha256 passwords, SHA-256 hashed keys, soft-delete revoke)
- Rewritten auth.py: resolve_principal() checks session cookie then
  Bearer key; @require_admin/@require_auth/@require_admin_strict
  decorators with bootstrap-open fallback when zero accounts exist
- New routes/auth.py: setup, login, logout, /me, accounts CRUD, API
  key CRUD endpoints
- New manage.py: create-admin, reset-password, list-accounts CLI for
  bootstrap and lockout recovery
- config.py: removed NUTWATCH_API_KEY; added NUTWATCH_SECRET_KEY
  (auto-generated and persisted in auth DB) and
  NUTWATCH_SESSION_COOKIE_SECURE
- All existing route decorators updated: read endpoints use
  @require_auth (any authenticated principal), mutating endpoints
  stay @require_admin; viewer role gets 403 on writes

Frontend:
- AuthProvider: loads auth status, gates App.tsx rendering between
  Setup -> Login -> AppLayout based on bootstrap state
- New Setup.tsx (first-run admin creation with Skip), Login.tsx,
  Accounts.tsx (admin CRUD), AccountModal.tsx, ApiKeys.tsx (self-
  service), ApiKeyModal.tsx (shows raw key once), LogoMark.tsx
- api.ts: 401 handler resets account state mid-session; optional
  in-memory bearer token fallback for non-cookie clients
- Role-gated UI: Dashboard System Actions, UpsCard/UpsDevices
  mutations, Notifications editor, ConfigFiles save, HooksSection
  editor, Users CRUD, WakeOnLan targets/mappings all hidden for
  viewers
- Sidebar: user chip with role badge + logout, Accounts and API Keys
  nav entries
- Users tab renamed to NUT Users with info box distinguishing from
  dashboard Accounts

Tests:
- New test_services_auth_db.py (account/key CRUD, login, revoke)
- New test_routes_auth.py (setup, login, rate-limiting, accounts,
  apikeys)
- New test_manage_cli.py (create-admin, reset-password, list-accounts)
- Rewritten test_auth.py: session, Bearer key, bootstrap-open, revoked
  keys, inactive accounts, key-inherits-owner-role, role gating
- test_routes.py: viewer-role coverage for all read/mutate endpoints
- Frontend: AuthProvider, Setup, Login, Accounts, ApiKeys, updated
  Dashboard/WakeOnLan/UpsCard tests

Docs: updated AGENTS.md, README.md, CONTRIBUTING.md, .coderabbit.yaml;
added docs/auth-plan.md; updated docs/modularization-plan.md
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JuanCF, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6220eaaf-5848-4e43-a432-e97af97db1e5

📥 Commits

Reviewing files that changed from the base of the PR and between 457488c and 39fc4a5.

📒 Files selected for processing (2)
  • src/backend/tests/conftest.py
  • src/backend/tests/test_routes_auth.py
📝 Walkthrough

Walkthrough

NutWatch now uses database-backed accounts, session login, and per-user API keys instead of a single environment Bearer token. The backend adds auth storage, endpoints, and CLI tooling; several read-only routes accept any authenticated user; the frontend adds auth gating and account/API-key management; tests and docs were updated throughout.

Changes

Account-backed auth migration

Layer / File(s) Summary
Backend auth core
src/backend/config.py, src/backend/auth.py, src/backend/services/auth_db.py, src/backend/app.py, src/backend/nutwatch.service, src/backend/routes/__init__.py
Adds secret/session config, persistent accounts and API keys, principal resolution and guards, app session/blueprint wiring, and optional env-file loading.
Auth routes and CLI
src/backend/routes/auth.py, src/backend/manage.py
Adds auth, account, and API-key endpoints plus account-management CLI commands.
Route authorization updates
src/backend/routes/history.py, src/backend/routes/hooks.py, src/backend/routes/logs.py, src/backend/routes/system.py, src/backend/routes/ups.py, src/backend/routes/upsmon.py, src/backend/routes/users.py, src/backend/routes/wol.py
Changes read endpoints to require_auth while keeping mutating routes on admin guards.
Backend tests
src/backend/tests/test_auth.py, src/backend/tests/test_manage_cli.py, src/backend/tests/test_routes.py, src/backend/tests/test_routes_auth.py, src/backend/tests/test_services_auth_db.py
Adds and updates coverage for auth resolution, route authorization, CLI behavior, and auth DB persistence.
Frontend auth core
src/frontend/src/types.ts, src/frontend/src/constants/index.ts, src/frontend/src/api.ts, src/frontend/src/AuthProvider.tsx, src/frontend/src/utils/alerts.ts, src/frontend/src/theme.tsx, src/frontend/src/useTheme.ts, src/frontend/src/useAuth.ts, src/frontend/src/components/useConfirm.ts, src/frontend/src/components/useModal.ts, src/frontend/src/components/ConfirmDialog.tsx, src/frontend/src/components/Modal.tsx
Adds account/API-key types and endpoints, bearer-token and 401 handling, auth/theme context extraction, and error-message unwrapping.
Frontend auth and account UI
src/frontend/src/App.tsx, src/frontend/src/components/Setup.tsx, src/frontend/src/components/Login.tsx, src/frontend/src/components/LogoMark.tsx, src/frontend/src/components/Sidebar.tsx, src/frontend/src/components/AccountModal.tsx, src/frontend/src/components/Accounts.tsx, src/frontend/src/components/ApiKeyModal.tsx, src/frontend/src/components/ApiKeys.tsx, src/frontend/src/styles/components.css, src/frontend/src/styles/base.css
Adds auth-gated app routing, setup/login views, sidebar account state, and account/API-key management screens.
Role-based UI gating
src/frontend/src/components/ConfigFiles.tsx, src/frontend/src/components/Dashboard.tsx, src/frontend/src/components/HooksSection.tsx, src/frontend/src/components/Notifications.tsx, src/frontend/src/components/UpsCard.tsx, src/frontend/src/components/UpsDevices.tsx, src/frontend/src/components/Users.tsx, src/frontend/src/components/WakeOnLan.tsx, src/frontend/src/components/UpsModal.tsx, src/frontend/src/components/UserModal.tsx, src/frontend/src/components/HookEditor.tsx, src/frontend/src/components/RestartPromptModal.tsx, src/frontend/src/components/ThemeSettings.tsx
Hides admin-only controls across existing frontend views and updates dashboard action handling.
Frontend tests
src/frontend/src/__tests__/components/*
Updates component tests for auth flows, account/API-key UI, and admin versus viewer behavior.
Docs and deployment updates
.coderabbit.yaml, AGENTS.md, README.md, CONTRIBUTING.md, Makefile, docs/auth-plan.md, docs/modularization-plan.md, scripts/setup.sh, .gitignore
Updates review guidance, project docs, packaging, and setup scripts for the new auth model.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AuthProvider
  participant api.ts
  participant routes/auth.py
  participant auth_db.py

  Browser->>AuthProvider: mount
  AuthProvider->>api.ts: GET AUTH_STATUS
  api.ts->>routes/auth.py: /api/auth/status
  routes/auth.py->>auth_db.py: count_accounts()
  auth_db.py-->>routes/auth.py: bootstrapped state
  routes/auth.py-->>AuthProvider: bootstrapped/authenticated
  alt setup required
    AuthProvider-->>Browser: render Setup
    Browser->>AuthProvider: setupAdmin()
    AuthProvider->>api.ts: POST AUTH_SETUP
    api.ts->>routes/auth.py: /api/auth/setup
    routes/auth.py->>auth_db.py: create_initial_admin()
    auth_db.py-->>routes/auth.py: admin account
    routes/auth.py-->>AuthProvider: account + session
  else login required
    AuthProvider-->>Browser: render Login
    Browser->>AuthProvider: login()
    AuthProvider->>api.ts: POST AUTH_LOGIN
    api.ts->>routes/auth.py: /api/auth/login
    routes/auth.py->>auth_db.py: verify_login()
    auth_db.py-->>routes/auth.py: account
    routes/auth.py-->>AuthProvider: account + session
  else authenticated
    AuthProvider-->>Browser: render AppLayout
  end
Loading

Possibly related PRs

  • JuanCF/nutwatch#26: Touches the earlier src/backend/auth.py auth-guard change that this PR replaces with principal resolution.
  • JuanCF/nutwatch#36: Relates to src/backend/routes/history.py, which this PR reclassifies from admin-only to authenticated access.
  • JuanCF/nutwatch#48: Also updates require_admin_strict-adjacent auth behavior in src/backend/auth.py, overlapping the same authorization path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 core change: session-based login plus per-user API keys replacing the old auth model.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth-system

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 25

Caution

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

⚠️ Outside diff range comments (3)
src/backend/routes/system.py (1)

24-31: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep raw config reads admin-only unless secrets are masked.

This endpoint returns content directly; broadening it to require_auth lets viewer accounts read sensitive NUT config content unless get_config() masks every secret-bearing file. Keep this route on require_admin or return a sanitized viewer-safe representation. As per path instructions, “Passwords are masked (••••••) in API responses.”

Proposed fix
-@require_auth
+@require_admin
 def get_config_handler(filename):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/routes/system.py` around lines 24 - 31, The get_config_handler
route currently uses require_auth while returning raw content from get_config,
which can expose sensitive config data to non-admin users. Update the route to
use require_admin for admin-only access, or ensure get_config returns a
sanitized viewer-safe version before exposing it. Keep the check in
get_config_handler aligned with ALLOWED_CONFIGS and the existing
require_auth/require_admin authorization symbols.

Source: Path instructions

src/frontend/src/components/WakeOnLan.tsx (2)

355-368: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Same colSpan mismatch in the Event Mappings empty state.

Header now has 4 or 5 <th> depending on isAdmin (Line 364), but the empty-state row hardcodes colSpan={5} (Line 368).

🩹 Proposed fix
-                ? <tr><td colSpan={5} className="empty">No event mappings configured.</td></tr>
+                ? <tr><td colSpan={isAdmin ? 5 : 4} className="empty">No event mappings configured.</td></tr>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/WakeOnLan.tsx` around lines 355 - 368, The Event
Mappings empty-state row in WakeOnLan.tsx uses a hardcoded colspan that does not
match the number of table headers when isAdmin changes. Update the empty-state
cell in the mappings table to compute its colSpan from the same isAdmin
condition used for the Actions column, matching the header in the table render
so the layout stays aligned.

317-328: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty-state colSpan doesn't account for the now-conditional Actions column.

The "Targets" header row renders 4 <th> for viewers and 5 for admins (Line 324), but the empty-state row still hardcodes colSpan={5} (Line 328), leaving a stray implicit column for viewer accounts.

🩹 Proposed fix
-                ? <tr><td colSpan={5} className="empty">No WOL targets configured.</td></tr>
+                ? <tr><td colSpan={isAdmin ? 5 : 4} className="empty">No WOL targets configured.</td></tr>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/WakeOnLan.tsx` around lines 317 - 328, The
empty-state row in WakeOnLan’s targets table hardcodes a column span that no
longer matches the conditional Actions column in the table header. Update the
empty-state <td> in the table rendering logic so its colSpan is derived from
whether isAdmin is true, matching the <thead> and keeping viewer/admin layouts
aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/modularization-plan.md`:
- Around line 83-84: The `### auth.py` heading in the modularization plan is
missing the required blank line below it, triggering MD022. Update the markdown
around the `auth.py` section so the heading is separated from the following
paragraph by one blank line, keeping the content under that heading intact.

In `@src/backend/auth.py`:
- Line 50: The bootstrap gate in auth should not depend on total accounts; it
must stay open until an admin exists. Update the checks in the auth flow that
currently use count_accounts() in the bootstrap decision to use an
admin-specific helper such as admin_exists() or count_admin_accounts(), and
apply the same change in the related bootstrap/recovery branches referenced by
the auth logic so non-admin-only databases do not prematurely lock setup.
- Around line 21-34: The auth.resolve_principal flow currently returns None
immediately when session["account_id"] exists but does not resolve to an active
account, preventing valid Bearer authentication from being checked. Update
resolve_principal so it only returns the session account when
get_account_by_id() yields an active account, and otherwise falls through to the
Authorization header parsing and resolve_api_key() fallback. Keep the existing
session-first order, but ensure inactive, deleted, or missing session principals
do not block API key auth.

In `@src/backend/config.py`:
- Line 19: NUTWATCH_SECRET_KEY is currently accepted from the environment with
any value, which can leave Flask session signing too weak; update the config
loading in config.py so the secret key is validated before use and the app fails
fast when an override is present but not sufficiently strong. Add the check near
the NUTWATCH_SECRET_KEY assignment, and ensure the resulting setting used by
Flask only accepts a strong value or raises an explicit startup error.

In `@src/backend/manage.py`:
- Line 57: The cmd_list_accounts callback has an unused args parameter that
triggers the Ruff lint. Update the cmd_list_accounts function signature to use
_args instead of args so it still matches the callback interface while clearly
marking the parameter as intentionally unused.
- Around line 79-84: The admin CLI currently accepts sensitive passwords via the
--password option in the cmd_create_admin and reset-password flows, which should
be removed. Update the argument parsing in manage.py so these commands prompt
interactively by default, and if automation is needed, add a safer stdin-based
option such as --password-stdin instead of a command-line password flag. Make
sure the affected parser setup and the cmd_create_admin handler, along with the
reset-password command wiring, are updated consistently.

In `@src/backend/routes/auth.py`:
- Around line 38-47: The first-admin bootstrap flow in auth should be made
atomic so concurrent setup requests cannot both see zero accounts and create
multiple admins. Move the “no accounts exist” guard out of the route logic in
auth.py and into an auth_db.create_initial_admin() helper in auth_db.py that
performs the existence check and admin insert within the same SQLite
transaction. Update the setup route to call that helper instead of
count_accounts() followed by create_account(), and ensure the helper preserves
the bootstrap rule that once the first admin exists, auth is enforced.
- Around line 94-105: The create_account_handler route currently allows
unauthenticated bootstrap account creation through require_admin, which can
bypass the intended first-admin setup flow. Update the auth route to use
require_admin_strict for /api/accounts or add an explicit count_accounts() guard
in create_account_handler that rejects zero-account bootstrap attempts and
directs callers to /api/auth/setup for the initial admin. Keep the fix centered
in create_account_handler and align it with the bootstrap rule enforced by
auth_db.
- Around line 111-129: The account update/deactivate handlers allow changing an
admin to inactive or a non-admin role without verifying that at least one active
admin remains, which can lock out admin access. Add a guard in
update_account_handler and deactivate_account_handler around
auth_db.update_account() to reject role changes or deactivation that would
remove the last active admin, using the existing account/admin lookup logic in
auth_db to check current active admins before applying the update.

In `@src/backend/routes/hooks.py`:
- Around line 18-19: The hooks endpoint currently uses require_auth, which
allows non-admin users to read sensitive hook bodies. Update the hooks route in
hooks_bp so only admins can access full hook content by switching the protection
to require_admin, or alternatively keep require_auth but return sanitized
metadata instead of the script body for non-admin callers. Make the change in
the route handling for the hook fetch endpoint so the access control matches the
sensitivity of hook contents.

In `@src/backend/routes/logs.py`:
- Around line 12-13: The `/api/logs/stream` endpoint in `logs_bp` is currently
protected only by `require_auth`, which allows viewer/API key accounts to access
raw service logs. Update the authorization on the logs streaming route (and any
related log endpoints in the same handler) to require admin-only access using
the existing auth/role checks, or add redaction plus strict rate/connection
limits before allowing non-admins. Keep the change localized around the
`logs_bp.route` handlers so the log stream cannot be opened by viewer accounts.

In `@src/backend/routes/upsmon.py`:
- Around line 9-12: The get_upsmon_config_handler endpoint is returning raw
MONITOR credentials from get_upsmon_config(), exposing sensitive
username/password data to authenticated users. Update this handler to either
restrict access to admins only or sanitize the returned config before jsonify by
redacting the password field (and any other secrets) while keeping the rest of
the UPSMon config intact.

In `@src/backend/routes/wol.py`:
- Around line 132-134: The `list_network_hosts` route is exposing
`wol_service.scan_network_hosts()` to any authenticated viewer, but this
operation should remain admin-only. Update the route in `list_network_hosts` to
use the admin authorization guard instead of `require_auth`, or change it to
return a cached scan result for non-admins. Keep the fix scoped to the
`list_network_hosts` endpoint and its auth decorator.

In `@src/backend/services/auth_db.py`:
- Around line 180-204: update_account() in auth_db must block changes that would
leave the system with no active admin account. Before applying role or is_active
updates, check whether the target account is the last active admin and reject
demotion or deactivation in that case, while still allowing other fields like
password to change. Use the existing update_account() flow, ROLES validation,
and the accounts lookup/query logic to enforce the bootstrap rule without
affecting the zero-accounts/open-access path.
- Around line 73-80: The secret-key initialization in the auth DB lookup is
vulnerable to a race when multiple processes hit the fresh database at once.
Update the secret-key path in the function that reads from meta and writes the
generated value to use an idempotent insert such as INSERT OR IGNORE, then
re-query the stored secret_key and return that value instead of assuming the
insert succeeded. Keep the fix localized to the secret-key creation logic in
auth_db.py so concurrent startups cannot fail with IntegrityError.
- Around line 58-67: The get_db() initializer currently relies on
sqlite3.connect(AUTH_DB), which can create the auth database with permissions
inherited from the process umask. Update get_db() to ensure the AUTH_DB file is
pre-created and chmod’d to 0600 before opening the sqlite3 connection, and make
sure the containing directory is also created with private permissions; use the
existing get_db(), AUTH_DB, and _ensure_schema() flow as the place to apply
these permission checks.

In `@src/backend/tests/test_auth.py`:
- Around line 16-20: Move the shared autouse auth DB patching fixture into a
common conftest.py so it is applied consistently across tests; the duplicated
_patch_auth_db logic in test_auth.py, test_manage_cli.py, and
test_routes_auth.py, plus the equivalent no_auth fixture in test_routes.py,
should be removed and replaced by the centralized fixture. Keep the behavior the
same by redirecting services.auth_db.AUTH_DB to a temp file and resetting
services.auth_db._schema_ready_for to None inside the shared fixture.

In `@src/frontend/src/__tests__/components/WakeOnLan.test.tsx`:
- Around line 234-254: The viewer-account test is duplicating auth setup instead
of reusing the shared withAuth helper. Update withAuth in the WakeOnLan test
suite to accept an optional account override, then use it in the viewer case so
AUTH_STATUS and AUTH_ME mocking stays centralized and consistent with the
existing helper pattern.

In `@src/frontend/src/App.tsx`:
- Around line 82-84: The /apikeys route in App.tsx is currently exposed even
when there is no account, while the Sidebar only links to it when an account
exists. Update App/AppLayout to destructure account from useAuth() alongside
isAdmin and gate the Route for ApiKeys the same way as the nav item, so direct
navigation without an account no longer renders the empty key page.

In `@src/frontend/src/AuthProvider.tsx`:
- Around line 104-108: Move the useAuth hook out of AuthProvider.tsx into a
separate module so the AuthProvider component file only exports the provider;
this preserves Fast Refresh behavior during edits. Keep AuthProvider and
AuthContext in the current module, define useAuth in its own file, and re-export
it from the appropriate barrel or entry point if needed so callers still import
the same hook name.

In `@src/frontend/src/components/AccountModal.tsx`:
- Around line 76-82: Prevent self-demotion in AccountModal: the Role selector in
AccountModal currently allows the signed-in user to change their own AccountRole
to viewer, which can immediately affect the active session. Update the Role
field logic in AccountModal (and its role state handling) so editing the current
account either disables role changes for self-edits or requires an explicit
confirmation before allowing a self-role downgrade.

In `@src/frontend/src/components/Accounts.tsx`:
- Around line 41-54: The account deactivation flow in handleDeactivate and the
self-role editing path in AccountModal need a guard against admin self-lockout.
Hide or disable actions that target the currently logged-in admin, and in the
backend reject any request that would deactivate or demote the last active admin
account. Use the existing identifiers handleDeactivate, AccountModal, and the
account mutation/API handlers to locate and enforce the check in both UI and
server-side validation.

In `@src/frontend/src/components/ApiKeyModal.tsx`:
- Around line 21-38: The ApiKeyModal.tsx handleCreate flow uses its own
try/catch with alert instead of the shared tryAlert pattern used in
Accounts.tsx, AccountModal.tsx, and ApiKeys.tsx. Refactor handleCreate to route
the POST API_KEYS mutation through tryAlert, keeping the same success path with
setCreated(result) and preserving the existing error message behavior via
errorMessage(err).

In `@src/frontend/src/components/Login.tsx`:
- Around line 37-44: The Login form fields need proper accessibility and
autofill support. Update the Username and Password controls in Login to connect
each label to its input using matching htmlFor and id values, and add
appropriate autoComplete hints to the inputs (for example, username and
current-password) so screen readers and password managers work correctly.

In `@src/frontend/src/components/Setup.tsx`:
- Around line 48-59: The Setup form fields are missing proper label-to-input
associations and autocomplete hints. Update the input elements in Setup to use
matching id/htmlFor pairs for the Username, Password, and Confirm Password
fields, and add appropriate autoComplete attributes to the related inputs. Keep
the changes localized to the Setup component’s field markup so the labels and
browser autofill behave consistently with Login.tsx.

---

Outside diff comments:
In `@src/backend/routes/system.py`:
- Around line 24-31: The get_config_handler route currently uses require_auth
while returning raw content from get_config, which can expose sensitive config
data to non-admin users. Update the route to use require_admin for admin-only
access, or ensure get_config returns a sanitized viewer-safe version before
exposing it. Keep the check in get_config_handler aligned with ALLOWED_CONFIGS
and the existing require_auth/require_admin authorization symbols.

In `@src/frontend/src/components/WakeOnLan.tsx`:
- Around line 355-368: The Event Mappings empty-state row in WakeOnLan.tsx uses
a hardcoded colspan that does not match the number of table headers when isAdmin
changes. Update the empty-state cell in the mappings table to compute its
colSpan from the same isAdmin condition used for the Actions column, matching
the header in the table render so the layout stays aligned.
- Around line 317-328: The empty-state row in WakeOnLan’s targets table
hardcodes a column span that no longer matches the conditional Actions column in
the table header. Update the empty-state <td> in the table rendering logic so
its colSpan is derived from whether isAdmin is true, matching the <thead> and
keeping viewer/admin layouts aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03510ab9-361d-498b-8cec-68617d69c583

📥 Commits

Reviewing files that changed from the base of the PR and between 7998862 and 146c658.

📒 Files selected for processing (61)
  • .coderabbit.yaml
  • .gitignore
  • AGENTS.md
  • CONTRIBUTING.md
  • Makefile
  • README.md
  • docs/auth-plan.md
  • docs/modularization-plan.md
  • scripts/setup.sh
  • src/backend/app.py
  • src/backend/auth.py
  • src/backend/config.py
  • src/backend/manage.py
  • src/backend/nutwatch.service
  • src/backend/routes/__init__.py
  • src/backend/routes/auth.py
  • src/backend/routes/history.py
  • src/backend/routes/hooks.py
  • src/backend/routes/logs.py
  • src/backend/routes/system.py
  • src/backend/routes/ups.py
  • src/backend/routes/upsmon.py
  • src/backend/routes/users.py
  • src/backend/routes/wol.py
  • src/backend/services/auth_db.py
  • src/backend/tests/test_auth.py
  • src/backend/tests/test_manage_cli.py
  • src/backend/tests/test_routes.py
  • src/backend/tests/test_routes_auth.py
  • src/backend/tests/test_services_auth_db.py
  • src/frontend/src/App.tsx
  • src/frontend/src/AuthProvider.tsx
  • src/frontend/src/__tests__/components/Accounts.test.tsx
  • src/frontend/src/__tests__/components/ApiKeys.test.tsx
  • src/frontend/src/__tests__/components/AuthProvider.test.tsx
  • src/frontend/src/__tests__/components/Dashboard.test.tsx
  • src/frontend/src/__tests__/components/Login.test.tsx
  • src/frontend/src/__tests__/components/Setup.test.tsx
  • src/frontend/src/__tests__/components/UpsCard.test.tsx
  • src/frontend/src/__tests__/components/WakeOnLan.test.tsx
  • src/frontend/src/api.ts
  • src/frontend/src/components/AccountModal.tsx
  • src/frontend/src/components/Accounts.tsx
  • src/frontend/src/components/ApiKeyModal.tsx
  • src/frontend/src/components/ApiKeys.tsx
  • src/frontend/src/components/ConfigFiles.tsx
  • src/frontend/src/components/Dashboard.tsx
  • src/frontend/src/components/HooksSection.tsx
  • src/frontend/src/components/Login.tsx
  • src/frontend/src/components/LogoMark.tsx
  • src/frontend/src/components/Notifications.tsx
  • src/frontend/src/components/Setup.tsx
  • src/frontend/src/components/Sidebar.tsx
  • src/frontend/src/components/UpsCard.tsx
  • src/frontend/src/components/UpsDevices.tsx
  • src/frontend/src/components/Users.tsx
  • src/frontend/src/components/WakeOnLan.tsx
  • src/frontend/src/constants/index.ts
  • src/frontend/src/styles/components.css
  • src/frontend/src/types.ts
  • src/frontend/src/utils/alerts.ts

Comment thread docs/modularization-plan.md
Comment thread src/backend/auth.py
Comment thread src/backend/auth.py
Comment thread src/backend/config.py
Comment thread src/backend/manage.py Outdated
Comment thread src/frontend/src/components/AccountModal.tsx
Comment thread src/frontend/src/components/Accounts.tsx
Comment thread src/frontend/src/components/ApiKeyModal.tsx
Comment thread src/frontend/src/components/Login.tsx
Comment thread src/frontend/src/components/Setup.tsx
Close several security and reliability gaps in the authentication system:

Backend:
- Atomic admin creation: BEGIN IMMEDIATE + count-and-insert in one transaction
  prevents concurrent /api/auth/setup requests from creating multiple admins.
- Last-admin guard: update_account raises ValueError if the only remaining
  active admin is being demoted or deactivated, preventing lockout.
- Secret key: validate NUTWATCH_SECRET_KEY >= 32 chars at startup; use
  INSERT OR IGNORE for the auto-generated key so concurrent process starts
  converge on one value instead of racing.
- resolve_principal: on stale session (account deleted/deactivated), fall
  through to Authorization header resolution instead of returning None.
- Password hygiene: replace --password CLI arg with --password-stdin to
  prevent secrets from appearing in process listings.
- File permissions: auth DB directory created 0700, DB file 0600.

Frontend:
- Prevent self-role-change in AccountModal (locking out your own admin
  access mid-session).
- Hide the Deactivate button for the logged-in account in the Accounts list.
- Gate /apikeys route on a logged-in account (not just admin check).
- Add htmlFor + autoComplete attributes on Login and Setup form fields.
- Fix colSpan mismatch on WOL empty-state rows when the admin Action column
  is hidden for viewers.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/frontend/src/App.tsx (1)

95-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a loading indicator instead of a blank screen.

if (loading) return null; renders nothing while auth state resolves, which can appear as a blank white flash on load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/App.tsx` around lines 95 - 102, The AuthGate loading path
currently returns nothing, causing a blank screen while auth state resolves.
Update the AuthGate component to render a loading indicator or splash UI instead
of returning null when loading is true, and keep the existing
bootstrapped/skipped/account branching intact so the flow still routes to Setup,
Login, or AppLayout using useAuth.
♻️ Duplicate comments (1)
src/backend/services/auth_db.py (1)

162-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Base setup completion on admin existence, not total accounts.

SELECT COUNT(*) FROM accounts makes /api/auth/setup report “setup already completed” for a viewer-only DB, even though no admin exists yet. Use an admin-specific check so setup remains available until the first admin account is created. As per coding guidelines, “Keep the bootstrap rule that zero accounts means everything is open; once the first admin exists, enforce auth.”

Proposed fix
-        row = conn.execute("SELECT COUNT(*) AS n FROM accounts").fetchone()
+        row = conn.execute(
+            "SELECT COUNT(*) AS n FROM accounts WHERE role = 'admin'"
+        ).fetchone()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/auth_db.py` around lines 162 - 165, The setup completion
check in auth_db should be based on whether an admin account exists, not whether
any account exists. Update the setup flow in the logic around the SELECT
COUNT(*) check so it looks specifically for an admin record before returning
“setup already completed,” keeping `/api/auth/setup` available when only viewer
accounts exist. Use the existing setup/bootstrap path in this service and adjust
the condition so the first admin creation is the trigger that closes bootstrap
access.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/services/auth_db.py`:
- Around line 239-245: The last-admin check in auth_db.py is not atomic because
count_active_admins() runs on a separate connection before the update commits,
allowing concurrent demotions/deactivations to bypass the guard. Update the
admin update flow around this check so the count and the role/is_active change
happen on the same connection and transaction under a write lock, and keep the
guard logic tied to the update path that handles demoting/deactivating the
current admin row.

In `@src/backend/tests/test_services_auth_db.py`:
- Around line 73-76: The broad pytest.raises(ValueError) checks in
test_update_account are too generic and can pass on unrelated errors; tighten
them by adding match assertions for the last-admin invariant. Update the two
update_account() assertions to verify the specific failure message in the
auth_db test so the intent is explicit and Ruff PT011 is satisfied.

---

Outside diff comments:
In `@src/frontend/src/App.tsx`:
- Around line 95-102: The AuthGate loading path currently returns nothing,
causing a blank screen while auth state resolves. Update the AuthGate component
to render a loading indicator or splash UI instead of returning null when
loading is true, and keep the existing bootstrapped/skipped/account branching
intact so the flow still routes to Setup, Login, or AppLayout using useAuth.

---

Duplicate comments:
In `@src/backend/services/auth_db.py`:
- Around line 162-165: The setup completion check in auth_db should be based on
whether an admin account exists, not whether any account exists. Update the
setup flow in the logic around the SELECT COUNT(*) check so it looks
specifically for an admin record before returning “setup already completed,”
keeping `/api/auth/setup` available when only viewer accounts exist. Use the
existing setup/bootstrap path in this service and adjust the condition so the
first admin creation is the trigger that closes bootstrap access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98521fe7-e04f-47c5-8229-353c0ed9f7d9

📥 Commits

Reviewing files that changed from the base of the PR and between 146c658 and f26a553.

📒 Files selected for processing (14)
  • docs/modularization-plan.md
  • src/backend/auth.py
  • src/backend/config.py
  • src/backend/manage.py
  • src/backend/routes/auth.py
  • src/backend/services/auth_db.py
  • src/backend/tests/test_auth.py
  • src/backend/tests/test_services_auth_db.py
  • src/frontend/src/App.tsx
  • src/frontend/src/components/AccountModal.tsx
  • src/frontend/src/components/Accounts.tsx
  • src/frontend/src/components/Login.tsx
  • src/frontend/src/components/Setup.tsx
  • src/frontend/src/components/WakeOnLan.tsx

Comment thread src/backend/services/auth_db.py
Comment thread src/backend/tests/test_services_auth_db.py Outdated
Lock down hook content read and network-host scan to admin-only.
Fix a race in update_account where concurrent admin demotions could
leave zero active admins — use BEGIN IMMEDIATE to atomically guard
the admin-count check and the update it protects.

Pull React context definitions (Auth, Theme, Confirm, Modal) into
separate use*.ts modules alongside their hooks so consumers can
import types and hooks without pulling in the full provider tree.
Centralize the duplicated _patch_auth_db pytest fixture in conftest.
Add a Loading… fallback in AuthGate instead of rendering null when
auth status is resolving.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/tests/conftest.py`:
- Around line 7-13: Add the missing return type annotation on _patch_auth_db in
conftest.py; this helper only patches services.auth_db.AUTH_DB and
services.auth_db._schema_ready_for via monkeypatch and does not return a value,
so annotate it explicitly as returning None to satisfy static analysis.

In `@src/backend/tests/test_routes_auth.py`:
- Around line 17-18: The private test helper _clear_login_attempts is missing an
explicit return type annotation, which Ruff flags via ANN202. Update the
function signature for _clear_login_attempts to include the appropriate return
type, and keep the implementation unchanged since it only clears _login_attempts
and returns nothing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f1e5035-b580-49cc-8e3d-03c12772c9be

📥 Commits

Reviewing files that changed from the base of the PR and between f26a553 and 457488c.

📒 Files selected for processing (43)
  • src/backend/routes/hooks.py
  • src/backend/routes/wol.py
  • src/backend/services/auth_db.py
  • src/backend/tests/conftest.py
  • src/backend/tests/test_auth.py
  • src/backend/tests/test_manage_cli.py
  • src/backend/tests/test_routes.py
  • src/backend/tests/test_routes_auth.py
  • src/backend/tests/test_services_auth_db.py
  • src/frontend/src/App.tsx
  • src/frontend/src/AuthProvider.tsx
  • src/frontend/src/__tests__/components/AuthProvider.test.tsx
  • src/frontend/src/__tests__/components/ConfirmDialog.test.tsx
  • src/frontend/src/__tests__/components/Modal.test.tsx
  • src/frontend/src/__tests__/components/WakeOnLan.test.tsx
  • src/frontend/src/__tests__/components/theme.test.tsx
  • src/frontend/src/components/AccountModal.tsx
  • src/frontend/src/components/Accounts.tsx
  • src/frontend/src/components/ApiKeyModal.tsx
  • src/frontend/src/components/ApiKeys.tsx
  • src/frontend/src/components/ConfigFiles.tsx
  • src/frontend/src/components/ConfirmDialog.tsx
  • src/frontend/src/components/Dashboard.tsx
  • src/frontend/src/components/HookEditor.tsx
  • src/frontend/src/components/HooksSection.tsx
  • src/frontend/src/components/Login.tsx
  • src/frontend/src/components/Modal.tsx
  • src/frontend/src/components/Notifications.tsx
  • src/frontend/src/components/RestartPromptModal.tsx
  • src/frontend/src/components/Setup.tsx
  • src/frontend/src/components/Sidebar.tsx
  • src/frontend/src/components/ThemeSettings.tsx
  • src/frontend/src/components/UpsDevices.tsx
  • src/frontend/src/components/UpsModal.tsx
  • src/frontend/src/components/UserModal.tsx
  • src/frontend/src/components/Users.tsx
  • src/frontend/src/components/WakeOnLan.tsx
  • src/frontend/src/components/useConfirm.ts
  • src/frontend/src/components/useModal.ts
  • src/frontend/src/styles/base.css
  • src/frontend/src/theme.tsx
  • src/frontend/src/useAuth.ts
  • src/frontend/src/useTheme.ts
💤 Files with no reviewable changes (2)
  • src/backend/tests/test_manage_cli.py
  • src/backend/tests/test_auth.py

Comment thread src/backend/tests/conftest.py Outdated
Comment thread src/backend/tests/test_routes_auth.py Outdated
Added `-> None` return type annotations to pytest fixture functions
in `conftest.py` and `test_routes_auth.py` for consistency with
type-checked code style throughout the backend tests.
@JuanCF
JuanCF merged commit 60a2f9d into main Jul 4, 2026
4 checks passed
@JuanCF
JuanCF deleted the feat/auth-system branch July 4, 2026 06:18
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