Files
FrenoCorp/src/lib/auth/clerk-provider.tsx
Michael Freno 7c684a42cc FRE-600: Fix code review blockers
- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 00:08:01 -04:00

162 lines
4.0 KiB
TypeScript

import { createContext, createSignal, useContext, onMount, Accessor, JSX } from 'solid-js';
import { getClerk, loadClerk, getClerkUrls } from './clerk-client';
import { User, UserRole, AuthState } from './types';
type ClerkUser = any;
interface ClerkSession {
getId: () => string;
getUser: () => ClerkUser;
}
interface ClerkClient {
user: () => ClerkUser | null;
session: () => ClerkSession | null;
isLoading: boolean;
signOut: () => Promise<void>;
}
const AuthContext = createContext<Accessor<AuthState> | undefined>(undefined);
const AuthActionsContext = createContext<{
signIn: () => void;
signOut: () => Promise<void>;
updateUser: (data: Partial<User>) => Promise<void>;
clerkClient: Accessor<ClerkClient | null>;
} | undefined>(undefined);
export { AuthContext, AuthActionsContext };
function clerkUserToUser(clerkUser: ClerkUser): User {
const primaryEmail = clerkUser.primaryEmailAddress?.emailAddress || '';
const firstName = clerkUser.firstName || '';
const lastName = clerkUser.lastName || '';
const name = [firstName, lastName].filter(Boolean).join(' ') || primaryEmail.split('@')[0] || 'User';
return {
id: clerkUser.id,
email: primaryEmail,
name,
avatarUrl: clerkUser.imageUrl,
role: 'owner' as UserRole,
};
}
export function ClerkProvider(props: { children: JSX.Element }) {
const [state, setState] = createSignal<AuthState>({
user: null,
isLoading: true,
isAuthenticated: false,
error: null,
});
const [clerkClient, setClerkClient] = createSignal<ClerkClient | null>(null);
onMount(async () => {
try {
const client = await loadClerk();
if (!client) {
setState({
user: null,
isLoading: false,
isAuthenticated: false,
error: 'Authentication service unavailable',
});
return;
}
const wrappedClient: ClerkClient = {
user: () => client.user,
session: () => (client.session as any) || null,
isLoading: false,
signOut: async () => {
await client.signOut();
setState({
user: null,
isLoading: false,
isAuthenticated: false,
error: null,
});
},
};
setClerkClient(wrappedClient);
if (client.user) {
setState({
user: clerkUserToUser(client.user),
isLoading: false,
isAuthenticated: true,
error: null,
});
} else {
setState((prev) => ({ ...prev, isLoading: false }));
}
} catch (err) {
setState({
user: null,
isLoading: false,
isAuthenticated: false,
error: err instanceof Error ? err.message : 'Failed to initialize auth',
});
}
});
const signIn = () => {
const urls = getClerkUrls();
window.location.href = urls.signInUrl;
};
const signOut = async () => {
const client = getClerk();
if (client) {
await client.signOut();
}
setState({
user: null,
isLoading: false,
isAuthenticated: false,
error: null,
});
};
const updateUser = async (data: Partial<User>) => {
setState((prev) => ({
...prev,
user: prev.user ? { ...prev.user, ...data } : null,
}));
};
return (
<AuthContext.Provider value={state}>
<AuthActionsContext.Provider value={{ signIn, signOut, updateUser, clerkClient }}>
{props.children}
</AuthActionsContext.Provider>
</AuthContext.Provider>
);
}
export function useAuth(): Accessor<AuthState> {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within a ClerkProvider');
}
return context;
}
export function useAuthActions() {
const context = useContext(AuthActionsContext);
if (!context) {
throw new Error('useAuthActions must be used within a ClerkProvider');
}
return context;
}
export function requireAuth() {
const auth = useAuth();
const authState = auth();
if (!authState.isAuthenticated) {
throw new Error('Authentication required');
}
return authState.user!;
}