Skip to main content

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 auth slice)
  • 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
VariableUsage
VITE_API_URLAPI base URL. In dev points to https://localhost:7204 (backend HTTPS launchSettings.json).
VITE_TENANT_CREATE_API_KEYValue 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

  1. Backend running (dotnet run from backend/HrStudio2.Api) with CORS including http://localhost:3001. The appsettings.Development.json configuration already includes both http://localhost:3000 (frontend) and http://localhost:3001 (admin).
  2. Trusted HTTPS dev certificate: dotnet dev-certs https --trust. Without it, the browser blocks calls to https://localhost:7204.
  3. A superuser account in cloud.users with usr_user_type_id = 1 and usr_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

  • PrivateRoute redirects to /login if not authenticated (based on state.auth.isAuthenticated).
  • SuperUserGuard checks state.auth.user.userTypeId === 1; if not, forces logout() 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 the create route in tenantService.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 statusAvailable 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 for purge: red header (bg-danger text-white) with AlertTriangle icon, 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:

  1. Update the TS types in admin/src/types/tenant.ts.
  2. Update admin/src/services/tenantService.ts if endpoints or payloads change.
  3. Update the API docs in docs/docs-dev-api/api/cloud/tenant.md.