Productionize WP Suite: auth, security hardening, sync, dashboard, PWA, email
Brings the Work Package Suite from a browser-local prototype to a multi-tenant, SQL-backed deployment hardened for customer IP. Auth & access control - Local username/password login (bcrypt + JWT in an HttpOnly cookie), admin-managed users, per-project membership, and project-scoped API access. - Admin console: change user roles, view the audit trail, manage settings. Security hardening - CSP / HSTS / X-Frame-Options / nosniff headers in nginx; Secure cookie via X-Forwarded-Proto; CSRF Origin check; attribute-safe output escaping. - Login lockout, token_version session revocation, stronger password policy, fail-closed secret loading, encrypted (AES-256) database backups. Persistence & schema - SOPs and Work Packages are now DB-backed and shared across users, written through a durable client sync outbox that queues offline edits. - Alembic migrations applied automatically on container start. New capabilities - Phase 2 dashboard (progress, gating, pagination, archive). - Phase 3 PWA "Field View" with offline caching and auth fallback. - WP owner assignment with OPTIONAL email notifications, OFF by default and toggled from the admin console. SMTP password is read only from the SMTP_PASSWORD env var (never stored); emails carry a WP number + deep link, never customer IP. Also: IBM Carbon restyle, Help section, and DEPLOYMENT.md brought up to date. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,15 +30,16 @@ def _gen_id() -> str:
|
||||
return f"user_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
|
||||
def _prompt_password(provided: str | None) -> str:
|
||||
def _prompt_password(provided: str | None, username: str = "") -> str:
|
||||
pw = provided
|
||||
if not pw:
|
||||
pw = getpass.getpass("New password: ")
|
||||
confirm = getpass.getpass("Confirm password: ")
|
||||
if pw != confirm:
|
||||
sys.exit("Passwords do not match.")
|
||||
if len(pw) < 8:
|
||||
sys.exit("Password must be at least 8 characters.")
|
||||
problem = auth.password_problem(pw, username)
|
||||
if problem:
|
||||
sys.exit(problem)
|
||||
return pw
|
||||
|
||||
|
||||
@@ -46,7 +47,7 @@ def cmd_create(args, role: str | None = None) -> None:
|
||||
role = role or args.role
|
||||
if role not in ("admin", "user"):
|
||||
sys.exit("role must be 'admin' or 'user'")
|
||||
pw = _prompt_password(getattr(args, "password", None))
|
||||
pw = _prompt_password(getattr(args, "password", None), args.username)
|
||||
with SessionLocal() as db:
|
||||
if auth.find_user(db, args.username):
|
||||
sys.exit(f"A user named '{args.username}' already exists.")
|
||||
@@ -75,7 +76,7 @@ def cmd_list(args) -> None:
|
||||
|
||||
|
||||
def cmd_reset_password(args) -> None:
|
||||
pw = _prompt_password(getattr(args, "password", None))
|
||||
pw = _prompt_password(getattr(args, "password", None), args.username)
|
||||
with SessionLocal() as db:
|
||||
u = auth.find_user(db, args.username)
|
||||
if not u:
|
||||
|
||||
Reference in New Issue
Block a user