fix(auth): resolve redirect loop and mount Better-Auth as sub-app (#144)

## Changes
- Replace toNodeHandler with auth.handler(c.req.raw) sub-app mount for Hono compatibility
- Add /api/auth/ path skip in authMiddleware and resolveStaffMiddleware
- Add OIDC_INTERNAL_BASE env var for split-horizon (hairpin NAT) URL resolution
- Replace render-time signIn.social() with LoginPage component (fixes redirect loop)
- Change auth-client baseURL to relative (empty string) for deployed environments
- Add POST /api/portal/appointments/:id/reschedule endpoint with session auth
- Add RescheduleFlow modal, PetForm component, and wire Dashboard/Appointments UI

## CTO Note
Auth fix is P0-critical. Portal mock data (UAT blocker) predates this PR and is tracked separately in GRO-218.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #144.
This commit is contained in:
groombook-engineer[bot]
2026-03-28 22:10:50 +00:00
committed by GitHub
parent 3a31ad71c2
commit 6872342d8f
13 changed files with 480 additions and 53 deletions
@@ -1,6 +1,7 @@
import { useState } from "react";
import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react";
import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js";
import { PetForm } from "./PetForm.js";
interface Props {
readOnly: boolean;
@@ -112,6 +113,20 @@ function PasswordChange({ readOnly }: { readOnly: boolean }) {
}
function ManagePets({ readOnly }: { readOnly: boolean }) {
const [editingPetId, setEditingPetId] = useState<string | null>(null);
const [showAddForm, setShowAddForm] = useState(false);
const editingPet = editingPetId ? PETS.find(p => p.id === editingPetId) ?? undefined : undefined;
if (editingPet || showAddForm) {
return (
<PetForm
pet={editingPet ?? undefined}
onSave={() => { setEditingPetId(null); setShowAddForm(false); }}
onCancel={() => { setEditingPetId(null); setShowAddForm(false); }}
/>
);
}
return (
<div className="space-y-4">
{PETS.map(pet => (
@@ -126,17 +141,12 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
{!readOnly && (
<div className="flex gap-2">
<button
disabled
title="Pet editing coming soon"
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-400 cursor-not-allowed"
onClick={() => setEditingPetId(pet.id)}
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-600 hover:bg-stone-50"
>
Edit
</button>
<button
disabled
title="Pet archiving coming soon"
className="p-1.5 border border-stone-200 rounded-lg text-stone-300 cursor-not-allowed"
>
<button className="p-1.5 border border-stone-200 rounded-lg text-stone-400 hover:text-amber-600 hover:border-amber-200">
<Archive size={14} />
</button>
</div>
@@ -145,9 +155,8 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
))}
{!readOnly && (
<button
disabled
title="Adding pets coming soon"
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-400 cursor-not-allowed"
onClick={() => setShowAddForm(true)}
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-500 hover:border-(--color-accent) hover:text-(--color-accent-dark) transition-colors"
>
<Plus size={16} />
Add New Pet