Commit graph

3 commits

Author SHA1 Message Date
ScreenTinker 6e31770cee feat(admin): admin-provisioned user creation + first-login gate (#10)
Adds POST /api/admin/users so an admin can create a user directly with a
known password and assign them to a workspace + role - for self-hosted
instances with no outbound email, where invites never deliver.

Server (routes/admin.js, mounted /api/admin with requireAuth + activityLogger):
- Gated by canAdminWorkspace(db, req.user, targetWorkspace): 404 if the
  workspace is missing, 403 if not an admin of it. This scopes org_admins
  to their own org and excludes platform_operator (no user/role mgmt, #13).
- Validates email (invite-create regex), role in WORKSPACE_ROLES, password
  min-8 (the /me rule). 409 on duplicate email - never overwrites.
- One transaction: global users row (auth_provider 'local',
  bcrypt.hashSync(pw,10), must_change_password from the flag) + a
  workspace_members row written inline (same footprint as an accepted
  invite; accept-invite left untouched).
- Explicit audit row admin_create_user; never logs the password; response
  excludes password/hash.
- HOSTED_INSTANCE: never calls sendSignupEmails and stamps both
  welcome_email_sent_at / activation_nudge_sent_at, so an admin-created
  user gets no welcome email and never enters the activation-nudge sweep.

must_change_password (frontend-first enforcement, per spec):
- Migration adds users.must_change_password INTEGER NOT NULL DEFAULT 0;
  surfaced via requireAuth + /me + login responses.
- route() in app.js forces users with the flag to a #/change-password
  screen (new force-password-change view, reuses PUT /api/auth/me) and
  blocks every other view until set. The /me update clears the flag.

Frontend: "Add User" button beside "Invite member" in the members view
(admin-only) opening a modal (email, name, password + generate, role,
must-change checkbox); invite and Add User coexist. api.adminCreateUser;
EN i18n only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:03:56 -05:00
ScreenTinker caa9fd0f40 feat(workspaces): mutation UI for members (slice 2B)
Completes P2 user-management. Adds the full admin surface for managing
workspace membership: invite modal, role change, member remove, cancel
pending invite. All admin-gated client-side via can_admin from /me,
server-gated via canAdminWorkspace.

Component additions:
- NEW workspace-members-invite-modal.js (~115 LOC). Mirrors
  workspace-rename-modal.js pattern (imperative open + listeners + close
  + esc/click-outside/enter). Two key differences: onSuccess callback
  instead of window.location.reload (allows targeted re-render of
  pending-invites section), and mapError callback so the parent's
  mapMutationError is the single regex-to-i18n source of truth (instead
  of duplicating in the modal).
- workspace-members.js: header invite button (can_admin gated), per-row
  affordances (role select + remove on direct members, cancel on invited
  rows, none on via_org rows), exported mapMutationError mapper,
  re-render on both success AND error for role-select to resync state
  when the server rejects.
- 4 api.js helpers (inviteWorkspaceMember, cancelWorkspaceInvite,
  updateWorkspaceMemberRole, removeWorkspaceMember).
- 24 i18n keys under members.modal.*, members.button.*,
  members.confirm.*, members.error.*, members.success.*
- CSS for .member-actions family (action buttons + role select + hover
  states).

UX decisions:
- Direct-member rows: role <select> replaces role text in same column;
  remove button right of detail
- via_org rows: no actions cell (server would 403; UI respects boundary)
- Invited rows: cancel button only (handoff rule was over-broad -
  cancel-invite IS a valid mutation on invited rows, refined during 2B
  survey)
- Role select fires on change, no Save button (matches teams.js pattern;
  mitigations for accidental clicks noted in handoff if reports come in)
- Mutations re-fetch + re-render rather than optimistic updates -
  simpler, no state-drift bugs, endpoints respond fast
- /invites endpoint skipped entirely when !can_admin (saves a request;
  server still enforces)

Verification: 21/21 Playwright assertions PASS across 6 cases (invite
happy path, invite collision, role change, remove member, last-admin
block, cancel invite). Test infrastructure stashed at
~/Documents/screentinker-2b-playwright-2026-05.py.

Closes P2 (user-management feature). Slice 1+3 backend landed c4fbd2b,
2A read-only view landed 8db171d, 2C accept-invite handler landed
399af54, 2B mutation UI landed here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:45:34 -05:00
ScreenTinker 8db171d979 feat(workspaces): members page read-only view (slice 2A)
Adds the workspace members page at #/workspace/:id/members.
Read-only listing only - mutations land in slice 2B,
accept-invite URL handler lands in slice 2C.

Three sections render based on access path:
- Members: direct workspace_members rows with role + join date
- Organization access: org_owner/org_admin who reach this
  workspace via org-level access (via_org=true). 75% opacity
  + italic "via organization" label to distinguish from direct
  membership. Section hidden if empty.
- Pending invites: workspace_invites rows (admin-only -
  section silently absent for non-admins via 403-suppress)

Switcher dropdown adds a "members" icon next to the rename
pencil, gated on can_admin (same predicate). Icon visible on
hover, mirrors the existing pencil pattern.

24 i18n keys added under members.* (read-only set; mutation
keys land in 2B).

Backend coverage from c4fbd2b unchanged; pre-flight curl
verification (13/13 cases) confirmed all 7 endpoints work as
documented before slice 2 first-exercised the four previously
untested ones (GET /invites, DELETE /invites/:id, PUT
/members/:userId, DELETE /members/:userId).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:00:51 -05:00