Employees with the extension enabled get a 403 page instead of their payslips #1

Open
opened 2026-08-31 14:16:56 +00:00 by padreug · 1 comment
Owner

Problem

views.py serves exactly one page — the operator console — and 403s anyone who is not the super user:

if not user.super_user:
    raise HTTPException(HTTPStatus.FORBIDDEN, "Payroll is restricted to the instance super user.")

But employees now need payroll among their active extensions, otherwise LNbits' per-user route gate (check_user_extension_access, lnbits/decorators.py:420) blocks /payroll/api/v1/my/* with {"detail": "Extension 'payroll' not enabled."} regardless of their invoice key. See 36e1643 for why the extension therefore cannot live in LNBITS_ADMIN_EXTENSIONS.

The result: an employee sees Payroll in their menu, clicks it, and gets a 403. The /my/* endpoints they can now reach have no UI in front of them.

Proposal

Render the page by role rather than refusing it:

  • super user → the operator console as today
  • anyone else → a read-only payslip view over /my/contracts and /my/payouts: what they are owed, when the next payday lands, what has been paid, and the CSV button pointed at /my/payouts.csv

The endpoints already exist and are wallet-key scoped, so this is a template/JS change plus a branch in views.py. It probably wants a wallet picker, since the invoice key scopes payslips to one wallet and a user may have several.

Not in scope

The auth model is unchanged — check_super_user on the operator router, invoice key on the employee router. This only stops an employee-facing route from being a dead end.

Found by

Smoke-testing v0.1.0 on bohm: a daily 10 EUR contract paid correctly (14878 sat, both legs settled, ledger row matched), and the employee endpoint was the one surface that had never been exercised.

🤖 Generated with Claude Code

https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj

## Problem `views.py` serves exactly one page — the operator console — and 403s anyone who is not the super user: ```python if not user.super_user: raise HTTPException(HTTPStatus.FORBIDDEN, "Payroll is restricted to the instance super user.") ``` But employees now need `payroll` among their active extensions, otherwise LNbits' per-user route gate (`check_user_extension_access`, `lnbits/decorators.py:420`) blocks `/payroll/api/v1/my/*` with `{"detail": "Extension 'payroll' not enabled."}` regardless of their invoice key. See 36e1643 for why the extension therefore cannot live in `LNBITS_ADMIN_EXTENSIONS`. The result: an employee sees **Payroll** in their menu, clicks it, and gets a 403. The `/my/*` endpoints they can now reach have no UI in front of them. ## Proposal Render the page by role rather than refusing it: - **super user** → the operator console as today - **anyone else** → a read-only payslip view over `/my/contracts` and `/my/payouts`: what they are owed, when the next payday lands, what has been paid, and the CSV button pointed at `/my/payouts.csv` The endpoints already exist and are wallet-key scoped, so this is a template/JS change plus a branch in `views.py`. It probably wants a wallet picker, since the invoice key scopes payslips to one wallet and a user may have several. ## Not in scope The auth model is unchanged — `check_super_user` on the operator router, invoice key on the employee router. This only stops an employee-facing route from being a dead end. ## Found by Smoke-testing v0.1.0 on bohm: a daily 10 EUR contract paid correctly (14878 sat, both legs settled, ledger row matched), and the employee endpoint was the one surface that had never been exercised. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
Author
Owner

Reproduced on bohm. Enabling payroll for a non-super-user account and opening the page gives:

403
Payroll is restricted to the instance super user.

So the extension is now reachable enough to appear in an employee's menu and still refuses them at the door. This was filed pre-emptively; it is now a live defect.

Confirms the shape of the fix too — the employee's /my/* endpoints have still never been exercised, because until the extension is enabled for them the per-user gate answers {"detail": "Extension 'payroll' not enabled."}, and once it is enabled the only page route 403s. Both halves have to move together: enabling the extension per account is what unblocks the API, and the role-branched page is what makes it usable.

Worth noting for whoever picks this up: /my/payouts.csv cannot be a plain <a href> like the operator CSV button, because require_invoice_key needs the key and an anchor cannot set a header. LNbits does accept it as an api-key query parameter (APIKeyQuery(name="api-key"), lnbits/decorators.py:53), but putting a wallet key in a URL puts it in browser history — fetching with the header and triggering a Blob download is the better route.

Reproduced on bohm. Enabling `payroll` for a non-super-user account and opening the page gives: ``` 403 Payroll is restricted to the instance super user. ``` So the extension is now reachable enough to appear in an employee's menu and still refuses them at the door. This was filed pre-emptively; it is now a live defect. Confirms the shape of the fix too — the employee's `/my/*` endpoints have **still never been exercised**, because until the extension is enabled for them the per-user gate answers `{"detail": "Extension 'payroll' not enabled."}`, and once it *is* enabled the only page route 403s. Both halves have to move together: enabling the extension per account is what unblocks the API, and the role-branched page is what makes it usable. Worth noting for whoever picks this up: `/my/payouts.csv` cannot be a plain `<a href>` like the operator CSV button, because `require_invoice_key` needs the key and an anchor cannot set a header. LNbits does accept it as an `api-key` query parameter (`APIKeyQuery(name="api-key")`, `lnbits/decorators.py:53`), but putting a wallet key in a URL puts it in browser history — fetching with the header and triggering a Blob download is the better route.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/payroll#1
No description provided.