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:
Khalid Abdi
2026-06-08 19:12:07 +03:00
parent ab2f10bffc
commit 6213da9477
24 changed files with 3338 additions and 180 deletions
+5 -3
View File
@@ -27,7 +27,7 @@ import {
CommandPanel,
} from "@/components/ui/command";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import { navItems } from "@/lib/nav";
import { useActiveRole, visibleNavItems } from "@/lib/roles";
type CommandPaletteContextValue = { open: () => void };
@@ -50,6 +50,7 @@ export function useCommandPalette(): CommandPaletteContextValue {
export function CommandPaletteProvider({ children }: { children: ReactNode }) {
const router = useRouter();
const { t } = useTranslation();
const role = useActiveRole();
const [open, setOpen] = useState(false);
useEffect(() => {
@@ -70,7 +71,8 @@ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
value: "pages",
label: t("nav.commandGroup"),
// Flatten sub-pages so e.g. "Appointments & Schedule" is reachable.
items: navItems.flatMap((item) =>
// Filtered by role so reception can't jump to clinical pages.
items: visibleNavItems(role).flatMap((item) =>
item.subs?.length
? item.subs.map((sub) => ({
id: sub.id,
@@ -89,7 +91,7 @@ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
),
},
],
[t],
[t, role],
);
type Group = (typeof groups)[number];