Staff Service
This page covers lumie-backend/modules/staff. The documentation route remains admin-svc.md, but the Gradle module, Java package, and internal API use staff.
Responsibility
The staff module owns:
- tenant-scoped staff rows linked to auth users;
- permission catalog lookups;
- per-staff permission assignments;
- staff lifecycle actions such as deactivate, reactivate, terminate, delete, reset password, and login ID change;
- owner bootstrap after tenant owner registration commits.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/web | Public staff, permission, and statistics controllers |
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/event/OwnerRegisteredListener.java | Owner bootstrap listener |
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/internal/StaffServiceAdapter.java | Internal monolith API implementation |
lumie-backend/modules/staff/src/main/java/com/lumie/staff/application/service | Staff command, query, permission, and dashboard services |
lumie-backend/modules/staff/src/main/java/com/lumie/staff/domain/entity | Staff, Permission, and StaffPermission |
lumie-backend/libs/internal-api/src/main/java/com/lumie/staff/api/StaffService.java | Internal contract used by class, lecture, content, and other modules |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | Baseline pre-rename admin and permission-side tables |
lumie-backend/app/src/main/resources/db/migration/public/V26__rename_admin_tables_to_staff.sql | Renames admins to staff and admin_permissions to staff_permissions |
Public Surface
| Surface | Entrypoints |
|---|---|
| Staff CRUD | POST /v1/staff, GET /v1/staff, GET /v1/staff/{id}, PATCH /v1/staff/{id}, DELETE /v1/staff/{id} |
| Credentials and lifecycle | POST /v1/staff/{id}/reset-password, PATCH /v1/staff/{id}/login-id, POST /v1/staff/{id}/deactivate, POST /v1/staff/{id}/reactivate, POST /v1/staff/{id}/terminate |
| Permissions | GET /v1/staff/{staffId}/permissions, PUT /v1/staff/{staffId}/permissions, GET /v1/permissions, GET /v1/permissions/categories |
| Dashboard | GET /v1/staff/statistics/dashboard |
Internal Surface
StaffService exports:
- staff lookup by staff ID or auth user ID;
- validation of a staff ID inside a tenant;
- staff permission lookup;
- full staff listing;
createOwnerStaff(...)for owner bootstrap.
This internal API is used heavily by the class, lecture, and content modules for teacher or author resolution.
Aggregates And Tables
| Aggregate | Table | Notes |
|---|---|---|
Staff | staff | Links a tenant-scoped staff row to an auth user |
Permission | permissions | Permission catalog entry |
StaffPermission | staff_permissions | Composite-key assignment of permission code to staff ID |
Two implementation details matter here:
Staff.roleis not stored directly on the staff row. It is read through a Hibernate@Formulafrom the auth module'suserstable.StaffPermissiondoes not inheritTenantScopedEntity; it writestenant_idin@PrePersistthroughTenantContextHolder.getRequiredTenantId().
Runtime Flow
Owner bootstrap after registration
Why owner bootstrap is idempotent
return staffRepository.findByUserId(userId)
.map(existing -> {
log.info("OWNER staff already exists for userId={}, tenant={} — idempotent skip",
userId, tenantSlug);
return toStaffDataFromEntity(existing);
})
This protects the listener from duplicate delivery after restart or partial publication replay.
Key Behaviors
- Only
MANAGERandINSTRUCTORcan be created through the public staff API.OWNERis system-managed. - Staff cannot manage themselves, and role hierarchy is enforced through
Role.canManage(...). - Delete is permanent only for inactive staff and only after the module verifies there are no assigned classes.
- Permission writes fully replace existing assignments by deleting current rows and inserting the new set.
- Password reset and login-ID change delegate to
AuthServicerather than modifying credentials locally.
Representative Contract Example
This example matches SetPermissionsRequest, StaffController, StaffCommandService.setStaffPermissions(...), and StaffPermissionResponse.
PUT /v1/staff/42/permissions
{
"permissions": {
"STUDENT_WRITE": "WRITE",
"CLASS_READ": "READ"
}
}
200 OK returns an empty body. A follow-up GET /v1/staff/42/permissions returns the replacement set:
[
{
"permissionCode": "STUDENT_WRITE",
"accessLevel": "WRITE"
},
{
"permissionCode": "CLASS_READ",
"accessLevel": "READ"
}
]
Because setStaffPermissions(...) calls deleteByStaffId(...) before saveAll(...), omitted permission codes are removed rather than merged or preserved.
Dependencies And Boundaries
| Dependency | Why it exists |
|---|---|
AuthService | Create auth users, reset passwords, change login IDs, and delete auth users |
BillingService | Admin quota checks via MetricType.ADMINS |
ClassService | Prevent deletion of staff who still own classes |
Failure Modes
- Creation and update can fail with
DUPLICATE_PHONE,INVALID_ROLE,INSUFFICIENT_PERMISSION, orAUTH_OP_FAILED. - Delete can fail when the actor targets themselves, tries to manage a higher role, tries to delete an active staff row, or the staff row still has assigned classes.
- Owner bootstrap failures are logged but do not roll back the already-committed owner registration.
Observability And Quota Behavior
- Normal lifecycle and permission writes log through
StaffCommandService. - Owner bootstrap logs success and failure in
OwnerRegisteredListener. - The module is quota-aware, but the current billing internal adapter returns
allowed=truewith an unlimited limit placeholder. In other words, the staff module calls quota checks, but the current monolith-side billing contract is permissive until usage enforcement is reintroduced.
Verification
./gradlew :modules:staff:test
./gradlew :app:test --tests '*Staff*'
cd lumie-document/docusaurus && npm run build
Expected success signals:
- Gradle finishes with
BUILD SUCCESSFUL. StaffCommandServiceTestpasses without failures.- Docusaurus finishes without MDX or broken-link errors for
backend/admin-svc.