Wire frontend to the backend: auth, organizations, real patient API

Connects the UI-only app to the new backend over Better Auth + a small
patient API client, replacing the in-memory fixture and placeholder identity.

- Better Auth React client (lib/auth-client.ts) + shared access-control
  roles; API client (lib/api-client.ts) sending credentials cross-origin.
- Designed (auth) route group: login, signup, verify-email, forgot/reset
  password, clinic onboarding, and accept-invite.
- proxy.ts (this Next's renamed middleware) optimistic redirect + an
  authoritative client AppAuthGuard requiring a session and active clinic.
- Real identity in nav-user (useSession + sign out); the unused team
  switcher repurposed as an organization (clinic) switcher; Care-team
  settings tab manages members and invitations.
- lib/patients.ts now calls the org-scoped API (types unchanged); patients
  table and create/edit dialog updated for async create/update.
- next.config: standalone output + Dockerfile for containerized runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-02 21:28:02 +03:00
parent dec150c77d
commit 1ecbd16404
31 changed files with 1853 additions and 286 deletions
+45 -3
View File
@@ -2,7 +2,7 @@
import { Plus, Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import { Badge } from "@/components/ui/badge";
@@ -25,8 +25,35 @@ export function PatientsView() {
// Bumped on open so the create dialog remounts with a fresh file # / form.
const [addKey, setAddKey] = useState(0);
const [allPatients, setAllPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
useEffect(() => {
let active = true;
setLoading(true);
listPatients()
.then((data) => {
if (!active) return;
setAllPatients(data);
setLoadError(null);
})
.catch((err) => {
if (!active) return;
setLoadError(
err instanceof Error ? err.message : "Failed to load patients."
);
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, []);
const q = query.trim().toLowerCase();
const patients = listPatients().filter(
const patients = allPatients.filter(
(p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q)
);
@@ -73,7 +100,22 @@ export function PatientsView() {
</tr>
</thead>
<tbody>
{patients.length === 0 ? (
{loading ? (
<tr>
<td
className="px-4 py-10 text-center text-muted-foreground"
colSpan={6}
>
Loading patients
</td>
</tr>
) : loadError ? (
<tr>
<td className="px-4 py-10 text-center text-destructive" colSpan={6}>
{loadError}
</td>
</tr>
) : patients.length === 0 ? (
<tr>
<td
className="px-4 py-10 text-center text-muted-foreground"