Skip to main content

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

PathRole
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/webPublic staff, permission, and statistics controllers
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/event/OwnerRegisteredListener.javaOwner bootstrap listener
lumie-backend/modules/staff/src/main/java/com/lumie/staff/adapter/in/internal/StaffServiceAdapter.javaInternal monolith API implementation
lumie-backend/modules/staff/src/main/java/com/lumie/staff/application/serviceStaff command, query, permission, and dashboard services
lumie-backend/modules/staff/src/main/java/com/lumie/staff/domain/entityStaff, Permission, and StaffPermission
lumie-backend/libs/internal-api/src/main/java/com/lumie/staff/api/StaffService.javaInternal contract used by class, lecture, content, and other modules
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlBaseline pre-rename admin and permission-side tables
lumie-backend/app/src/main/resources/db/migration/public/V26__rename_admin_tables_to_staff.sqlRenames admins to staff and admin_permissions to staff_permissions

Public Surface

SurfaceEntrypoints
Staff CRUDPOST /v1/staff, GET /v1/staff, GET /v1/staff/{id}, PATCH /v1/staff/{id}, DELETE /v1/staff/{id}
Credentials and lifecyclePOST /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
PermissionsGET /v1/staff/{staffId}/permissions, PUT /v1/staff/{staffId}/permissions, GET /v1/permissions, GET /v1/permissions/categories
DashboardGET /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

AggregateTableNotes
StaffstaffLinks a tenant-scoped staff row to an auth user
PermissionpermissionsPermission catalog entry
StaffPermissionstaff_permissionsComposite-key assignment of permission code to staff ID

Two implementation details matter here:

  • Staff.role is not stored directly on the staff row. It is read through a Hibernate @Formula from the auth module's users table.
  • StaffPermission does not inherit TenantScopedEntity; it writes tenant_id in @PrePersist through TenantContextHolder.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 MANAGER and INSTRUCTOR can be created through the public staff API. OWNER is 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 AuthService rather 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

DependencyWhy it exists
AuthServiceCreate auth users, reset passwords, change login IDs, and delete auth users
BillingServiceAdmin quota checks via MetricType.ADMINS
ClassServicePrevent deletion of staff who still own classes

Failure Modes

  • Creation and update can fail with DUPLICATE_PHONE, INVALID_ROLE, INSUFFICIENT_PERMISSION, or AUTH_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=true with 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.
  • StaffCommandServiceTest passes without failures.
  • Docusaurus finishes without MDX or broken-link errors for backend/admin-svc.