HR Studio Admin — Tenant Management Console
React project reserved for superuser users (usr_user_type_id = 1) to manage platform tenants.
Location in the monorepo
- Sources:
admin/ - Related project:
frontend/(main application for tenant users).
Shares the same stack as frontend/ (React 19 + Vite 7 + TypeScript 5.8 + Redux Toolkit + Reactstrap + i18next) and communicates with the same backend (backend/HrStudio2.Api).
Stack
- Build: Vite 7, dev port 3001
- UI: Reactstrap 9 + Bootstrap 5 + SCSS
- State: Redux Toolkit (only
authslice) - Form: react-hook-form + yup
- HTTP: axios with JWT bearer interceptor
- i18n: i18next (EN/IT/FR/ES)
Setup
cd admin
npm install
npm run dev
The app starts at http://localhost:3001.
Environment variables
File admin/.env.development (committed for development, overrides .env.example):
VITE_API_URL=https://localhost:7204
VITE_TENANT_CREATE_API_KEY=pippo
| Variable | Usage |
|---|---|
VITE_API_URL | API base URL. In dev points to https://localhost:7204 (backend HTTPS launchSettings.json). |
VITE_TENANT_CREATE_API_KEY | Value of the X-Api-Key header required only by POST /api/cloud/tenant/create ([AllowAnonymous] endpoint protected by API key, see tenant API docs). In dev must match ApiKeys:TenantCreate in appsettings.json. |
In production use a non-committed .env.production and regenerate the API key.
Runtime prerequisites
- Backend running (
dotnet runfrombackend/HrStudio2.Api) with CORS includinghttp://localhost:3001. Theappsettings.Development.jsonconfiguration already includes bothhttp://localhost:3000(frontend) andhttp://localhost:3001(admin). - Trusted HTTPS dev certificate:
dotnet dev-certs https --trust. Without it, the browser blocks calls tohttps://localhost:7204. - A superuser account in
cloud.userswithusr_user_type_id = 1andusr_status_code = 1.
Architecture
admin/src/
├── index.tsx entry point (redux, router, toast, i18n, css)
├── index.scss global console styles
├── auth/Login.tsx login form (superuser-only)
├── layout/ shell (header with Profile + LanguageSwitcher, sidebar with Dashboard/Tenants entries)
├── pages/
│ ├── dashboard/ landing page
│ └── tenants/ TenantList, TenantForm, status badge / confirm / created-manager components
├── redux/
│ ├── store.ts single store with auth slice
│ └── reducers/authSlice.ts
├── router/
│ ├── index.tsx BrowserRouter + PrivateRoute + SuperUserGuard
│ └── routes.ts route constants
├── services/
│ ├── api.ts axios instance + Authorization interceptor
│ ├── authService.ts POST /api/auth/login
│ └── tenantService.ts POST /api/cloud/tenant/{list,get,create,update,suspend,delete,undelete,resume,purge}
├── types/tenant.ts TS DTOs aligned to C# records
└── i18n/ i18next + 4 locales (EN/IT/FR/ES)
Superuser gating
PrivateRouteredirects to/loginif not authenticated (based onstate.auth.isAuthenticated).SuperUserGuardchecksstate.auth.user.userTypeId === 1; if not, forceslogout()and redirect with toast.- The backend still applies
[Authorize(Policy = "SuperAdmin")]to all tenant endpoints (defense in depth — if someone bypasses the UI, the backend returns 403).
Token storage
The JWT token is stored in localStorage with the key hr-admin-auth-token (different from frontend/ which uses hr-auth-token): the two apps can coexist in the same browser without collisions.
Tenant creation
The Create form sends two headers:
Authorization: Bearer <jwt-superuser>(injected by the interceptor)X-Api-Key: <VITE_TENANT_CREATE_API_KEY>(injected only for thecreateroute intenantService.create)
The response contains managerUserId and managerTempPassword: the CreatedManagerModal displays them once only. The operator must copy them and transmit them to the new manager out-of-band; the password expires after 7 days.
Tenant actions (dropdown in the list)
Each row of TenantList exposes a dropdown with the available actions based on the tenant status:
| Tenant status | Available actions |
|---|---|
Active (statusCode=1, deleted=false) | Edit, Suspend, Delete |
Suspended (statusCode=2, deleted=false) | Edit, Reactivate, Permanently delete |
Deleted (deleted=true) | Restore |
The TenantAction type in TenantList.tsx enumerates all actions: "suspend" | "delete" | "undelete" | "resume" | "purge".
Confirmation modal
Destructive actions show a ConfirmModal before proceeding. The component accepts a danger?: boolean prop that activates the high-risk visual variant:
danger=false(default) — standard modal with neutral header and primary or danger confirmation button.danger=true— activated only forpurge: red header (bg-danger text-white) withAlertTriangleicon, message in body on translucent red background, danger confirmation button. Visually communicates the irreversibility of the operation.
<ConfirmModal
danger={confirm?.action === "purge"}
confirmColor={confirm?.action === "delete" || confirm?.action === "purge" ? "danger" : "primary"}
...
/>
Production build
npm run build
Output in admin/dist/. To serve locally: npm run preview (Vite default preview port).
Updating the APIs
When tenant contracts change in the backend:
- Update the TS types in
admin/src/types/tenant.ts. - Update
admin/src/services/tenantService.tsif endpoints or payloads change. - Update the API docs in
docs/docs-dev-api/api/cloud/tenant.md.