Adds user management

This commit is contained in:
2026-06-22 21:14:22 +02:00
parent 5331be4279
commit 6976eb4337
40 changed files with 3392 additions and 146 deletions
@@ -0,0 +1,86 @@
# Unit 5 Code Generation Summary
## What Was Built
Unit 5 implements the **User Management** feature: listing users, inviting new users, and changing roles.
---
## Backend Changes
### Bug Fixes
- **UsersController**: Fixed invite link URL from `/setup/complete?token=…` to `/invite/complete?token=…`
### New Interface Method
- `IInvitationService.GetPendingInvitationByEmailAsync(string email)` — returns `(bool IsPending, string? Token)`
### New Service Implementation
- `InvitationService.GetPendingInvitationByEmailAsync` — queries `Invitations` for a non-accepted, non-expired record by email
### New Models (`IdentityRequests.cs`)
- `UserDto` — maps a user to the API response including `InvitationPending` and `InviteLink`
- `ChangeRoleRequest` — carries `NewRole`
### Updated Controller (`UsersController`)
- Now injects `UserManager<ApplicationUser>` alongside `IInvitationService`
- `GET /api/v1/Users` — returns all users with role, status, and pending invite info
- `PUT /api/v1/Users/{userId}/role` — enforces RBAC rules (Owner constraint, Admin can only promote User→Admin)
### Database Migration
- **No migration needed** — all tables (`Users`, `Roles`, `UserRoles`, `Invitations`) already exist
---
## Frontend Changes
### New UI Components (`frontend/src/components/ui/`)
| File | Component |
|------|-----------|
| `dialog.tsx` | Dialog, DialogContent, DialogHeader, DialogTitle, etc. |
| `select.tsx` | Select, SelectTrigger, SelectContent, SelectItem, etc. |
| `table.tsx` | Table, TableHeader, TableBody, TableRow, TableHead, TableCell |
| `badge.tsx` | Badge with `default / secondary / destructive / outline` variants |
### New API Hook (`useUsers.ts`)
- `useUsers()``GET /api/v1/Users`, staleTime 30s, queryKey `['users']`
- `useInviteUser()``POST /api/v1/Users/invite`, invalidates `['users']`
- `useChangeRole()``PUT /api/v1/Users/{userId}/role`, invalidates `['users']`
### Migrated Hook (`useInvitation.ts`)
- Converted from `useState/useEffect` to TanStack Query
- Uses `InvitationValidation` from `types.ts` (field `isValid`, not the old local `valid`)
### Updated Files
| File | Change |
|------|--------|
| `types.ts` | Added `UserListItem`, `InviteUserPayload`, `InviteUserResponse`, `ChangeRolePayload` |
| `InviteCompletePage.tsx` | `validationQuery.data?.valid``validationQuery.data?.isValid` (2 places) |
| `mocks/invitation/handlers.ts` | Response shape updated to `{ isValid, email, name, errorCode }` |
| `mocks/users/handlers.ts` | Full user list + invite + change-role mock handlers |
### New Page & Component
- `UsersPage.tsx` — table with role/status badges, per-row dropdown (copy link, change role)
- `components/users/InviteUserDialog.tsx` — 2-step dialog: email+role form → invite link display
### i18n
- `en/translation.json` and `nl/translation.json``users.*` key section added (title, table columns, status labels, role labels, invite dialog, action labels)
---
## Tests
| File | Coverage |
|------|---------|
| `useUsers.test.ts` | `useUsers`, `useInviteUser`, `useChangeRole` — happy path + error |
| `UsersPage.test.tsx` | Render, table rows, pending badge, copy link action, API error, dialog open |
| `InviteUserDialog.test.tsx` | Form validation, successful flow (step 2), API error, dialog reset |
| `InviteCompletePage.test.tsx` | No changes needed — mock now returns correct `isValid` shape |
---
## Key Design Decisions
- **`isValid` vs `valid`**: The existing `InvitationValidation` type in `types.ts` already uses `isValid`. The old `useInvitation.ts` had a local interface with `valid`. Migration aligns the hook to the canonical type.
- **No server-side invite email**: Invite flow is link-only (per functional design). The link is shown in the dialog step 2 and copied to clipboard.
- **Role RBAC in frontend**: `availableRolesFor()` in `UsersPage` restricts the dropdown options; backend enforces the same rules server-side.
- **Pending invite for accepted users**: `GetPendingInvitationByEmailAsync` only returns a token if `!IsAccepted && !IsExpired` — accepted users correctly show no pending invite.