mirror of
https://github.com/willchen96/mike.git
synced 2026-06-10 20:35:12 +02:00
- Refactor user profile loading and updating logic to improve state management and reduce unnecessary checks. - Update MFA login gate to streamline verification checks and improve user experience. - Ensure consistent handling of user ID across profile context and components. - Improve error handling and loading states in user-related API calls.
290 lines
8.3 KiB
TypeScript
290 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
ReactNode,
|
|
useCallback,
|
|
} from "react";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import {
|
|
type ApiKeyState,
|
|
type ApiKeyProvider,
|
|
type UserProfile as ApiUserProfile,
|
|
getUserProfile,
|
|
isMfaRequiredError,
|
|
saveApiKey,
|
|
updateUserMfaOnLogin,
|
|
updateUserProfile,
|
|
} from "@/app/lib/mikeApi";
|
|
|
|
interface UserProfile {
|
|
displayName: string | null;
|
|
organisation: string | null;
|
|
messageCreditsUsed: number;
|
|
creditsResetDate: string;
|
|
creditsRemaining: number;
|
|
tier: string;
|
|
titleModel: string;
|
|
tabularModel: string;
|
|
mfaOnLogin: boolean;
|
|
apiKeys: ApiKeyState;
|
|
}
|
|
|
|
interface UserProfileContextType {
|
|
profile: UserProfile | null;
|
|
loading: boolean;
|
|
updateDisplayName: (name: string) => Promise<boolean>;
|
|
updateOrganisation: (organisation: string) => Promise<boolean>;
|
|
updateModelPreference: (
|
|
field: "titleModel" | "tabularModel",
|
|
value: string,
|
|
) => Promise<boolean>;
|
|
updateMfaOnLogin: (enabled: boolean) => Promise<boolean>;
|
|
updateApiKey: (
|
|
provider: ApiKeyProvider,
|
|
value: string | null,
|
|
) => Promise<boolean>;
|
|
reloadProfile: () => Promise<void>;
|
|
incrementMessageCredits: () => Promise<boolean>;
|
|
}
|
|
|
|
const UserProfileContext = createContext<UserProfileContextType | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const API_KEY_PROVIDERS: ApiKeyProvider[] = [
|
|
"claude",
|
|
"gemini",
|
|
"openai",
|
|
"openrouter",
|
|
"courtlistener",
|
|
];
|
|
|
|
function emptyApiKeys(): ApiKeyState {
|
|
return {
|
|
claude: { configured: false, source: null },
|
|
gemini: { configured: false, source: null },
|
|
openai: { configured: false, source: null },
|
|
openrouter: { configured: false, source: null },
|
|
courtlistener: { configured: false, source: null },
|
|
};
|
|
}
|
|
|
|
function toProfile(data: ApiUserProfile): UserProfile {
|
|
const { apiKeyStatus, ...profile } = data;
|
|
const apiKeys = emptyApiKeys();
|
|
for (const provider of API_KEY_PROVIDERS) {
|
|
apiKeys[provider] = {
|
|
configured: !!apiKeyStatus[provider],
|
|
source:
|
|
apiKeyStatus.sources?.[provider] ??
|
|
(apiKeyStatus[provider] ? "user" : null),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
mfaOnLogin: profile.mfaOnLogin === true,
|
|
apiKeys,
|
|
};
|
|
}
|
|
|
|
export function UserProfileProvider({ children }: { children: ReactNode }) {
|
|
const { user, isAuthenticated } = useAuth();
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const userId = user?.id ?? null;
|
|
|
|
const loadProfile = useCallback(async () => {
|
|
try {
|
|
const profileData = await getUserProfile();
|
|
setProfile(toProfile(profileData));
|
|
} catch {
|
|
// Calculate a default future reset date for fallback
|
|
const futureResetDate = new Date();
|
|
futureResetDate.setDate(futureResetDate.getDate() + 30);
|
|
|
|
// Set fallback profile data on exception
|
|
setProfile({
|
|
displayName: null,
|
|
organisation: null,
|
|
messageCreditsUsed: 0,
|
|
creditsResetDate: futureResetDate.toISOString(),
|
|
creditsRemaining: 999999, // temporarily unlimited
|
|
tier: "Free",
|
|
titleModel: "gemini-3.1-flash-lite-preview",
|
|
tabularModel: "gemini-3-flash-preview",
|
|
mfaOnLogin: false,
|
|
apiKeys: emptyApiKeys(),
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated && userId) {
|
|
setLoading(true);
|
|
loadProfile();
|
|
} else {
|
|
setProfile(null);
|
|
setLoading(false);
|
|
}
|
|
}, [isAuthenticated, userId, loadProfile]);
|
|
|
|
const updateDisplayName = useCallback(
|
|
async (displayName: string): Promise<boolean> => {
|
|
if (!user) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const updated = await updateUserProfile({ displayName });
|
|
setProfile((prev) =>
|
|
prev ? { ...prev, ...toProfile(updated) } : null,
|
|
);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
[user],
|
|
);
|
|
|
|
const updateOrganisation = useCallback(
|
|
async (organisation: string): Promise<boolean> => {
|
|
if (!user) return false;
|
|
try {
|
|
const updated = await updateUserProfile({ organisation });
|
|
setProfile((prev) =>
|
|
prev ? { ...prev, ...toProfile(updated) } : null,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
if (isMfaRequiredError(error)) throw error;
|
|
return false;
|
|
}
|
|
},
|
|
[user],
|
|
);
|
|
|
|
const updateModelPreference = useCallback(
|
|
async (
|
|
field: "titleModel" | "tabularModel",
|
|
value: string,
|
|
): Promise<boolean> => {
|
|
if (!user) return false;
|
|
try {
|
|
const updated = await updateUserProfile({
|
|
[field]: value,
|
|
});
|
|
setProfile((prev) =>
|
|
prev ? { ...prev, ...toProfile(updated) } : null,
|
|
);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
[user],
|
|
);
|
|
|
|
const updateMfaOnLogin = useCallback(
|
|
async (enabled: boolean): Promise<boolean> => {
|
|
if (!user) return false;
|
|
try {
|
|
const updated = await updateUserMfaOnLogin(enabled);
|
|
setProfile((prev) =>
|
|
prev ? { ...prev, ...toProfile(updated) } : null,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
if (isMfaRequiredError(error)) throw error;
|
|
return false;
|
|
}
|
|
},
|
|
[user],
|
|
);
|
|
|
|
const updateApiKey = useCallback(
|
|
async (
|
|
provider: ApiKeyProvider,
|
|
value: string | null,
|
|
): Promise<boolean> => {
|
|
if (!user) return false;
|
|
const normalized = value?.trim() ? value.trim() : null;
|
|
try {
|
|
await saveApiKey(provider, normalized);
|
|
setProfile((prev) =>
|
|
prev
|
|
? {
|
|
...prev,
|
|
apiKeys: {
|
|
...prev.apiKeys,
|
|
[provider]: {
|
|
configured: !!normalized,
|
|
source: normalized ? "user" : null,
|
|
},
|
|
},
|
|
}
|
|
: null,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
if (isMfaRequiredError(error)) throw error;
|
|
return false;
|
|
}
|
|
},
|
|
[user],
|
|
);
|
|
|
|
const reloadProfile = useCallback(async () => {
|
|
if (userId) {
|
|
await loadProfile();
|
|
}
|
|
}, [userId, loadProfile]);
|
|
|
|
const incrementMessageCredits = useCallback(async (): Promise<boolean> => {
|
|
if (!user || !profile) {
|
|
return false;
|
|
}
|
|
|
|
// Check if user has credits remaining
|
|
if (profile.creditsRemaining <= 0) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}, [user, profile]);
|
|
|
|
return (
|
|
<UserProfileContext.Provider
|
|
value={{
|
|
profile,
|
|
loading,
|
|
updateDisplayName,
|
|
updateOrganisation,
|
|
updateModelPreference,
|
|
updateMfaOnLogin,
|
|
updateApiKey,
|
|
reloadProfile,
|
|
incrementMessageCredits,
|
|
}}
|
|
>
|
|
{children}
|
|
</UserProfileContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useUserProfile() {
|
|
const context = useContext(UserProfileContext);
|
|
if (context === undefined) {
|
|
throw new Error(
|
|
"useUserProfile must be used within a UserProfileProvider",
|
|
);
|
|
}
|
|
return context;
|
|
}
|