mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
feat: admin-provisioned staff, username login & role-based access
Replace the email-invitation flow with admin-provisioned staff accounts and
add role-based access that changes what each member sees.
Backend:
- Enable Better Auth `username` plugin (staff sign in by username); regenerate
auth schema (+ username/displayUsername on user) and migration 0007.
- Add `doctor` and `reception` roles to the access-control RBAC. `reception` is
scoped to scheduling + registration (no `prescription` statement).
- New `/api/staff` route: POST creates a user (auth.api.signUpEmail) and adds
them to the active clinic (auth.api.addMember); GET lists members + usernames.
Gated by requirePermission({ member: ["create"] }).
- Redact clinical PHI for the reception role in the patients service (read,
create and update) so demographics-only is enforced server-side.
Frontend:
- usernameClient + Email|Username tabs on the login form.
- lib/roles.ts: useActiveRole + Better-Auth-permission-driven nav visibility,
default landing, and a route guard (reception -> /appointments, blocked from
clinical routes). Applied to the sidebar, command palette and auth guard.
- Care team page now provisions staff via a two-step Add-team-member dialog
(details -> username/password) hitting /api/staff; removes the email-invite
and pending-invitation UI. New members are contactable from Messages
automatically (they become org members).
- Hide clinical sections of the patient form and the admin-only settings tabs
for non-clinical/non-admin roles.
All permission management stays in Better Auth (per the better-auth skills now
referenced in backend/CLAUDE.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
type Patient,
|
||||
updatePatient,
|
||||
} from "@/lib/patients";
|
||||
import { hasClinicalAccess, useActiveRole } from "@/lib/roles";
|
||||
import { notify } from "@/lib/toast";
|
||||
|
||||
type PatientFormDialogProps = {
|
||||
@@ -203,6 +204,11 @@ export function PatientFormDialog({
|
||||
}: PatientFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = mode === "edit";
|
||||
// Reception registers demographics only — clinical sections are hidden (the
|
||||
// backend also redacts/ignores clinical data for this role). Show everything
|
||||
// while the role is still loading to avoid a flash for clinical users.
|
||||
const role = useActiveRole();
|
||||
const showClinical = role == null || hasClinicalAccess(role);
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -416,6 +422,8 @@ export function PatientFormDialog({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{showClinical && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{t("patientForm.currentVitals")}
|
||||
@@ -615,6 +623,8 @@ export function PatientFormDialog({
|
||||
)}
|
||||
rows={visits}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DialogPanel>
|
||||
|
||||
<DialogFooter className="flex-col items-stretch gap-2 sm:flex-row sm:items-center">
|
||||
|
||||
Reference in New Issue
Block a user