Security
Authentication
JWT is delivered via an HTTP-only cookie — never stored in localStorage,
so it isn’t reachable from client-side JavaScript (mitigates XSS-driven token
theft). CSRF protection uses a double-submit cookie pattern
(XSRF-TOKEN cookie + matching request header on mutating requests).
Login is rate-limited: repeated failed attempts lock the account temporarily.
Password reset
Password reset is OTP-verified, not link-based:
POST /auth/forgot-passwordrequires both the email and the account’s mobile number to match — a single leaked email address isn’t enough to trigger a reset.- On a match, an OTP is emailed, and every
owneraccount is alerted immediately (even before the OTP is verified). POST /auth/forgot-password/verify-otpchecks the code and only then issues a short-lived reset token.POST /auth/reset-passwordconsumes that token once.
Authorization (RBAC)
Permissions are database-backed (roles / role_permissions tables), checked
via @PreAuthorize("hasAuthority('X')") on service methods — not a hardcoded
Java enum. This means new permissions can be granted or revoked without a
deploy.
Ownership checks — for “my own resource” style access (e.g. a staff member
acting on a customer assigned to them), an @RequireOwnership aspect compares
the resource’s assigned owner against the acting staff member, with admin
roles bypassing the check. If a resource has no owner assigned yet, the check
is deliberately skipped, not denied, so unassigned work stays visible and
claimable.
Role escalation is separately guarded — granting the owner role to
another account requires the acting user to already be an owner, even
though the same base permission (STAFF_MANAGE) is also held by manager for
unrelated reasons.
Dev-only bypass flags
A small number of environment flags exist purely for local development and
are refused at application startup if left true outside a local/dev
profile:
| Flag | Effect |
|---|---|
app.security.disable-authz | Bypasses all @PreAuthorize checks (still requires login) |
app.security.disable-authn | Skips login entirely, auto-authenticates as the first active staff member |
app.scheduler.dev-fast-mode | Re-runs scheduled jobs every 10s instead of waiting for the daily cron |
Each is backed by a @PostConstruct guard that throws on startup if the flag
is enabled outside an allowed profile — this is a hard stop, not just a
warning.
Email content
Every value substituted into an HTML email template is HTML-escaped by
default — a staff-entered name or note can’t inject markup into an email
rendered by a real mail client. The one exception is server-generated URLs
(e.g. resetLink), which skip escaping so & in the query string isn’t
corrupted. Server-built HTML fragments (not user input) that legitimately need
to render as markup go through a separate, explicitly-named method rather than
being smuggled through the escaped path.
Reporting a vulnerability
This project does not yet have a public security disclosure process. If you’ve found an issue, contact the maintainer directly rather than opening a public issue.