Commit graph

3 commits

Author SHA1 Message Date
36e1643470 docs: correct how the employee payslip surface is gated
The README and operations notes claimed the /my/* endpoints "stay reachable
regardless of LNBITS_ADMIN_EXTENSIONS" because "the API routes are mounted
instance-wide". Both halves are wrong, and the smoke test on bohm caught it:
an invoice-key request from employee_1 came back with

    {"detail": "Extension 'payroll' not enabled."}

LNbits gates every extension path per user via check_user_extension_access
(lnbits/decorators.py:420), not just the UI listing. Two consequences the
docs now state instead of contradicting:

- An employee needs `payroll` among their active extensions before an
  invoice key gets them anywhere. Enable it through
  LNBITS_USER_DEFAULT_EXTENSIONS or per account in the Admin UI.
- `payroll` must stay OUT of LNBITS_ADMIN_EXTENSIONS. That list trips the
  earlier branch of the same check — "User not authorized for extension" —
  which a non-admin can never clear, so it would permanently lock employees
  out of their own payslips. It also buys nothing: check_super_user on the
  router and user.super_user on the page route are what actually restrict
  the console, and a non-admin key was verified to get 401 from
  /contracts and /users.

Also notes the wart this exposes: an employee with the extension enabled
sees "Payroll" in their menu and gets a 403 page, because the only page
route is the operator console. Filed as a follow-up rather than fixed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
2026-08-31 16:16:19 +02:00
4b6d647d23 feat: CSV export and employee payslip view
Two audiences the super-user API did not serve.

Accounting gets `GET /api/v1/payouts.csv`, filterable by contract, status
and date range. The range bounds the *payday* rather than the row
timestamp, so a period contains the paydays that belong to it even when one
of them took three days of retries to settle — otherwise a late retry lands
in the wrong month's export.

Every exported field is neutralised against spreadsheet formula injection.
`detail` carries exception text and the memo carries operator input, and a
cell beginning `=`, `+`, `-` or `@` executes when the file is opened. Worth
the eight lines: this file is written specifically to be opened in somebody
else's spreadsheet.

Employees get `/api/v1/my/payouts`, `/my/payouts.csv` and `/my/contracts`
on a **separate router** gated by a wallet invoice key rather than by
super-user rights. Separate router so it cannot inherit — or accidentally
shed — the wrong gate. Scoped to the wallet rather than the account because
an invoice key names exactly one wallet, leaving no lookup that could widen
the result to a sibling wallet the key does not cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
2026-08-31 13:53:12 +02:00
a211a8ab8b chore: scaffold payroll extension
Repo tooling and LNbits extension metadata only — no runtime code yet, so
this lands separately from the feature work per the workspace commit rules
(cross-cutting concerns commit first).

- MIT licence, .gitignore, README
- Makefile + pyproject wired for the standard aio fork lint pipeline
  (black + ruff + mypy) and pytest
- config.json declaring id/version/tile so the extension is installable
  from the aiolabs catalog once there is something to install

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
2026-08-31 13:36:32 +02:00