- Internal AI chat: schedule_appointment intent books earliest available slot in Column A using office hours; claim_only intent looks up latest past appointment for service date, asks user when two appointments are within 7 days, auto-triggers correct Selenium worker with mapped prices - Gemini model updated to gemini-flash-latest; conversation history (15 messages) passed for pronoun/reference resolution; history trimmed to start with user turn so Gemini doesn't reject the context - Insurance alias file (insuranceAliases.json) replaces hardcoded siteKey matching; "tufs" now resolves to TUFTS_SCO - DOB format normalized (MM/DD/YYYY → YYYY-MM-DD) before parseLocalDate; autoCheck now fires for all insurance types, not just MH/CMSP - Claim form auto-submit: all handlers (MH, CCA, DDMA, UnitedDH, Tufts) accept formToUse and receive fee-schedule-priced form; prefillDone set after chatbot code prefill so autoSubmit gate opens correctly - Chatbot: chat history persisted in sessionStorage, cleared on logout and auto-logout; Clear button writes fresh state synchronously; message history window increased to 15 - DentaQuest/TuftsSCO Selenium: "Remember me" checkbox clicked before sign-in to persist OTP trust cookie across sessions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
203 lines
5.6 KiB
TypeScript
Executable File
203 lines
5.6 KiB
TypeScript
Executable File
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
|
|
import {
|
|
useQuery,
|
|
useMutation,
|
|
UseMutationResult,
|
|
} from "@tanstack/react-query";
|
|
import { UserUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|
import { z } from "zod";
|
|
import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
type SelectUser = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
|
|
|
|
const insertUserSchema = (
|
|
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<{
|
|
username: z.ZodString;
|
|
password: z.ZodString;
|
|
}>
|
|
).pick({
|
|
username: true,
|
|
password: true,
|
|
});
|
|
|
|
type InsertUser = z.infer<typeof insertUserSchema>;
|
|
|
|
type AuthContextType = {
|
|
user: SelectUser | null;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
loginMutation: UseMutationResult<SelectUser, Error, LoginData>;
|
|
logoutMutation: UseMutationResult<void, Error, void>;
|
|
registerMutation: UseMutationResult<SelectUser, Error, InsertUser>;
|
|
};
|
|
|
|
// type LoginData = Pick<InsertUser, "username" | "password">;
|
|
type LoginData = {
|
|
username: InsertUser["username"];
|
|
password: InsertUser["password"];
|
|
};
|
|
|
|
export const AuthContext = createContext<AuthContextType | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const { toast } = useToast();
|
|
const {
|
|
data: user,
|
|
error,
|
|
isLoading,
|
|
} = useQuery<SelectUser | undefined, Error>({
|
|
queryKey: ["/api/users/"],
|
|
queryFn: getQueryFn({ on401: "returnNull" }),
|
|
});
|
|
|
|
const loginMutation = useMutation({
|
|
mutationFn: async (credentials: LoginData) => {
|
|
const res = await apiRequest("POST", "/api/auth/login", credentials);
|
|
|
|
const contentType = res.headers.get("content-type") || "";
|
|
const isJson = contentType.includes("application/json");
|
|
const data = isJson ? await res.json() : {};
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data?.error || "Login failed. Please try again.");
|
|
}
|
|
if (!data?.token || !data?.user) {
|
|
throw new Error("Invalid response from server");
|
|
}
|
|
|
|
localStorage.setItem("token", data.token);
|
|
return data.user;
|
|
},
|
|
onSuccess: (user: SelectUser) => {
|
|
queryClient.setQueryData(["/api/users/"], user);
|
|
},
|
|
onError: (error: Error) => {
|
|
console.error("Login error:", error);
|
|
toast({
|
|
title: "Login failed",
|
|
description: error.message,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const registerMutation = useMutation({
|
|
mutationFn: async (credentials: InsertUser) => {
|
|
const res = await apiRequest("POST", "/api/auth/register", credentials);
|
|
|
|
const contentType = res.headers.get("content-type") || "";
|
|
const isJson = contentType.includes("application/json");
|
|
const data = isJson ? await res.json() : {};
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data?.error || "Registration failed.");
|
|
}
|
|
|
|
if (!data?.token || !data?.user) {
|
|
throw new Error("Invalid response from server");
|
|
}
|
|
|
|
localStorage.setItem("token", data.token);
|
|
return data.user;
|
|
},
|
|
onSuccess: (user: SelectUser) => {
|
|
queryClient.setQueryData(["/api/users/"], user);
|
|
},
|
|
onError: (error: Error) => {
|
|
console.error("Registration error:", error);
|
|
toast({
|
|
title: "Registration failed",
|
|
description: error.message,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const logoutMutation = useMutation({
|
|
mutationFn: async () => {
|
|
localStorage.removeItem("token");
|
|
sessionStorage.removeItem("chatbot_messages");
|
|
await apiRequest("POST", "/api/auth/logout");
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.setQueryData(["/api/users/"], null);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast({
|
|
title: "Logout failed",
|
|
description: error.message,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const IDLE_TIMEOUT = 60 * 60 * 1000;
|
|
const WARN_BEFORE = 5 * 60 * 1000;
|
|
|
|
const logoutTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const warnTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const warnedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
|
|
const clearTimers = () => {
|
|
if (logoutTimer.current) clearTimeout(logoutTimer.current);
|
|
if (warnTimer.current) clearTimeout(warnTimer.current);
|
|
};
|
|
|
|
const resetTimers = () => {
|
|
clearTimers();
|
|
warnedRef.current = false;
|
|
|
|
warnTimer.current = setTimeout(() => {
|
|
if (!warnedRef.current) {
|
|
warnedRef.current = true;
|
|
toast({
|
|
title: "Session expiring soon",
|
|
description: "You will be logged out in 5 minutes due to inactivity.",
|
|
duration: 10000,
|
|
});
|
|
}
|
|
}, IDLE_TIMEOUT - WARN_BEFORE);
|
|
|
|
logoutTimer.current = setTimeout(() => {
|
|
logoutMutation.mutate();
|
|
}, IDLE_TIMEOUT);
|
|
};
|
|
|
|
const events = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
|
|
events.forEach((e) => window.addEventListener(e, resetTimers, { passive: true }));
|
|
resetTimers();
|
|
|
|
return () => {
|
|
clearTimers();
|
|
events.forEach((e) => window.removeEventListener(e, resetTimers));
|
|
};
|
|
}, [user]);
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{
|
|
user: user ?? null,
|
|
isLoading,
|
|
error,
|
|
loginMutation,
|
|
logoutMutation,
|
|
registerMutation,
|
|
}}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error("useAuth must be used within an AuthProvider");
|
|
}
|
|
return context;
|
|
}
|