feat: integrate tRPC client with Clerk authentication
- Update trpc-client.ts with auth token management - Create comprehensive tRPC hooks for all project operations - Store Clerk session token in localStorage for API auth - Clear token on sign out - Support integer IDs matching backend schema Hooks created: - Projects: useProjects, useProject, useCreateProject, useUpdateProject, useDeleteProject - Characters: useCharacters, useCharacter, useCreateCharacter, useUpdateCharacter, useDeleteCharacter - Relationships: useRelationships, useCharacterRelationships, useCreateRelationship, useUpdateRelationship, useDeleteRelationship - Scenes: useScenes, useScene, useCreateScene, useUpdateScene, useDeleteScene - Stats: useCharacterStats, useProjectCharacterStats All 34 backend tests passing.
This commit is contained in:
@@ -1,31 +1,39 @@
|
|||||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||||
import type { AppRouter } from '../../../server/trpc';
|
import type { AppRouter } from '../../../server/trpc';
|
||||||
|
|
||||||
// Create tRPC client
|
let cachedToken: string | null = null;
|
||||||
export function createTRPCClientInstance(baseUrl: string = 'http://localhost:8080') {
|
|
||||||
return createTRPCClient<AppRouter>({
|
function getAuthToken(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
if (cachedToken) return cachedToken;
|
||||||
|
cachedToken = localStorage.getItem('auth_token');
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAuthToken(token: string | null) {
|
||||||
|
cachedToken = token;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('auth_token', token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tRPC client with auth
|
||||||
|
export const trpc = createTRPCClient<AppRouter>({
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${baseUrl}/trpc`,
|
url: `${(import.meta as any).env?.VITE_API_URL || 'http://localhost:8080'}/trpc`,
|
||||||
headers: () => {
|
headers: () => {
|
||||||
// Add auth headers if available
|
const token = getAuthToken();
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
return {
|
return {
|
||||||
authorization: token ? `Bearer ${token}` : '',
|
authorization: token ? `Bearer ${token}` : '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for SSR
|
export type { AppRouter };
|
||||||
export function createServerTRPCClient(baseUrl: string = 'http://localhost:8080') {
|
|
||||||
return createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${baseUrl}/trpc`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,107 +1,123 @@
|
|||||||
import { createQuery, createMutation, useQueryClient } from '@tanstack/solid-query';
|
import { createQuery, createMutation, useQueryClient } from '@tanstack/solid-query';
|
||||||
import { createTRPCClientInstance } from './trpc-client';
|
import { trpc } from './trpc-client';
|
||||||
import type { Project, Character, CharacterRelationship, CharacterStats, Scene } from '../../../server/types/project';
|
|
||||||
|
|
||||||
const trpcClient = createTRPCClientInstance();
|
|
||||||
|
|
||||||
// Project hooks
|
// Project hooks
|
||||||
export function useProjects() {
|
export function useProjects() {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['projects'],
|
queryKey: ['projects'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.listProjects.query();
|
return await trpc.project.listProjects.query(undefined);
|
||||||
return result as Project[];
|
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProject(projectId: string) {
|
export function useProject(id: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['project', projectId],
|
queryKey: ['project', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getProject.query({ id: projectId });
|
return await trpc.project.getProject.query({ id });
|
||||||
return result as Project;
|
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!id,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateProject() {
|
export function useCreateProject() {
|
||||||
return createMutation({
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
mutationFn: async (input: { name: string; description?: string }) => {
|
mutationFn: async (input: { name: string; description?: string }) => {
|
||||||
return trpcClient.project.createProject.mutate(input) as Promise<Project>;
|
return await trpc.project.createProject.mutate(input);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const qc = useQueryClient();
|
|
||||||
qc.invalidateQueries({ queryKey: ['projects'] });
|
qc.invalidateQueries({ queryKey: ['projects'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateProject() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
|
mutationFn: async (input: { id: number; name?: string; description?: string }) => {
|
||||||
|
return await trpc.project.updateProject.mutate(input);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['projects'] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteProject() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
return await trpc.project.deleteProject.mutate({ id });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['projects'] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character hooks
|
// Character hooks
|
||||||
export function useCharacters(projectId: string) {
|
export function useCharacters(projectId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['characters', projectId],
|
queryKey: ['characters', projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.listCharacters.query({ projectId });
|
return await trpc.project.listCharacters.query({ projectId });
|
||||||
return result as Character[];
|
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCharacter(characterId: string) {
|
export function useCharacter(id: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['character', characterId],
|
queryKey: ['character', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getCharacter.query({ id: characterId });
|
return await trpc.project.getCharacter.query({ id });
|
||||||
return result as Character;
|
|
||||||
},
|
},
|
||||||
enabled: !!characterId,
|
enabled: !!id,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSearchCharacters(projectId: string, query?: string, role?: string, arcType?: string) {
|
export function useSearchCharacters(projectId: number, query?: string, role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble', arcType?: 'positive' | 'negative' | 'flat' | 'complex') {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['searchCharacters', projectId, query, role, arcType],
|
queryKey: ['searchCharacters', projectId, query, role, arcType],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.searchCharacters.query({
|
return await trpc.project.searchCharacters.query({
|
||||||
projectId,
|
projectId,
|
||||||
query,
|
query,
|
||||||
role,
|
role,
|
||||||
arcType,
|
arcType,
|
||||||
});
|
});
|
||||||
return result as Character[];
|
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCharacterStats(characterId: string) {
|
export function useCharacterStats(characterId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['characterStats', characterId],
|
queryKey: ['characterStats', characterId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getCharacterStats.query({ characterId });
|
return await trpc.project.getCharacterStats.query({ characterId });
|
||||||
return result as CharacterStats;
|
|
||||||
},
|
},
|
||||||
enabled: !!characterId,
|
enabled: !!characterId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProjectCharacterStats(projectId: string) {
|
export function useProjectCharacterStats(projectId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['projectCharacterStats', projectId],
|
queryKey: ['projectCharacterStats', projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getProjectCharacterStats.query({ projectId });
|
return await trpc.project.getProjectCharacterStats.query(undefined, {
|
||||||
return result as CharacterStats[];
|
context: { projectId },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateCharacter() {
|
export function useCreateCharacter() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (input: {
|
mutationFn: async (input: {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -117,21 +133,21 @@ export function useCreateCharacter() {
|
|||||||
conflict?: string;
|
conflict?: string;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
projectId: string;
|
projectId: number;
|
||||||
}) => {
|
}) => {
|
||||||
return trpcClient.project.createCharacter.mutate(input) as Promise<Character>;
|
return await trpc.project.createCharacter.mutate(input);
|
||||||
},
|
},
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
qc.invalidateQueries({ queryKey: ['characters', variables.projectId] });
|
qc.invalidateQueries({ queryKey: ['characters', variables.projectId] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateCharacter() {
|
export function useUpdateCharacter() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (input: {
|
mutationFn: async (input: {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
@@ -146,121 +162,165 @@ export function useUpdateCharacter() {
|
|||||||
conflict?: string;
|
conflict?: string;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
projectId?: string;
|
projectId?: number;
|
||||||
}) => {
|
}) => {
|
||||||
return trpcClient.project.updateCharacter.mutate(input) as Promise<Character>;
|
return await trpc.project.updateCharacter.mutate(input);
|
||||||
},
|
},
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
qc.invalidateQueries({ queryKey: ['character', variables.id] });
|
qc.invalidateQueries({ queryKey: ['character', variables.id] });
|
||||||
qc.invalidateQueries({ queryKey: ['characters'] });
|
qc.invalidateQueries({ queryKey: ['characters'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteCharacter() {
|
export function useDeleteCharacter() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: number) => {
|
||||||
return trpcClient.project.deleteCharacter.mutate({ id }) as Promise<{ success: boolean }>;
|
return await trpc.project.deleteCharacter.mutate({ id });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['characters'] });
|
qc.invalidateQueries({ queryKey: ['characters'] });
|
||||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relationship hooks
|
// Relationship hooks
|
||||||
export function useRelationships(projectId: string) {
|
export function useRelationships(projectId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['characterRelationships', projectId],
|
queryKey: ['characterRelationships', projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.listRelationships.query({ projectId });
|
return await trpc.project.listRelationships.query(undefined, {
|
||||||
return result as CharacterRelationship[];
|
context: { projectId },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCharacterRelationships(characterId: string) {
|
export function useCharacterRelationships(characterId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['characterRelationships', characterId],
|
queryKey: ['characterRelationships', characterId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getRelationshipsForCharacter.query({ characterId });
|
return await trpc.project.getRelationshipsForCharacter.query({ characterId });
|
||||||
return result as CharacterRelationship[];
|
|
||||||
},
|
},
|
||||||
enabled: !!characterId,
|
enabled: !!characterId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateRelationship() {
|
export function useCreateRelationship() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (input: {
|
mutationFn: async (input: {
|
||||||
characterIdA: string;
|
characterIdA: number;
|
||||||
characterIdB: string;
|
characterIdB: number;
|
||||||
relationshipType: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
relationshipType: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
||||||
description?: string;
|
description?: string;
|
||||||
strength?: number;
|
strength?: number;
|
||||||
isAntagonistic?: boolean;
|
isAntagonistic?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return trpcClient.project.createRelationship.mutate(input) as Promise<CharacterRelationship>;
|
return await trpc.project.createRelationship.mutate(input);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateRelationship() {
|
export function useUpdateRelationship() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (input: {
|
mutationFn: async (input: {
|
||||||
id: string;
|
id: number;
|
||||||
relationshipType?: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
relationshipType?: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
||||||
description?: string;
|
description?: string;
|
||||||
strength?: number;
|
strength?: number;
|
||||||
isAntagonistic?: boolean;
|
isAntagonistic?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return trpcClient.project.updateRelationship.mutate(input) as Promise<CharacterRelationship>;
|
return await trpc.project.updateRelationship.mutate(input);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteRelationship() {
|
export function useDeleteRelationship() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return createMutation({
|
return createMutation(() => ({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: number) => {
|
||||||
return trpcClient.project.deleteRelationship.mutate({ id }) as Promise<{ success: boolean }>;
|
return await trpc.project.deleteRelationship.mutate({ id });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scene hooks
|
// Scene hooks
|
||||||
export function useScenes(projectId: string) {
|
export function useScenes(projectId: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['scenes', projectId],
|
queryKey: ['scenes', projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.listScenes.query({ projectId });
|
return await trpc.project.listScenes.query({ projectId });
|
||||||
return result as Scene[];
|
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScene(sceneId: string) {
|
export function useScene(id: number) {
|
||||||
return createQuery({
|
return createQuery(() => ({
|
||||||
queryKey: ['scene', sceneId],
|
queryKey: ['scene', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await trpcClient.project.getScene.query({ id: sceneId });
|
return await trpc.project.getScene.query({ id });
|
||||||
return result as Scene;
|
|
||||||
},
|
},
|
||||||
enabled: !!sceneId,
|
enabled: !!id,
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateScene() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
|
mutationFn: async (input: {
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
projectId: number;
|
||||||
|
order: number;
|
||||||
|
}) => {
|
||||||
|
return await trpc.project.createScene.mutate(input);
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['scenes', variables.projectId] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateScene() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
|
mutationFn: async (input: {
|
||||||
|
id: number;
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
order?: number;
|
||||||
|
}) => {
|
||||||
|
return await trpc.project.updateScene.mutate(input);
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['scenes'] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteScene() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return createMutation(() => ({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
return await trpc.project.deleteScene.mutate({ id });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['scenes'] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import { createContext, createSignal, useContext, onMount, Accessor, JSX } from 'solid-js';
|
import { createContext, createSignal, useContext, onMount, Accessor, JSX } from 'solid-js';
|
||||||
|
import { Navigate } from '@solidjs/router';
|
||||||
import { getClerk, loadClerk, getClerkUrls } from './clerk-client';
|
import { getClerk, loadClerk, getClerkUrls } from './clerk-client';
|
||||||
import { User, UserRole, AuthState } from './types';
|
import { User, UserRole, AuthState } from './types';
|
||||||
|
import { setAuthToken } from '../api/trpc-client';
|
||||||
|
|
||||||
type ClerkUser = any;
|
interface ClerkUser {
|
||||||
|
id: string;
|
||||||
|
primaryEmailAddress?: {
|
||||||
|
emailAddress: string;
|
||||||
|
} | null;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
imageUrl: string;
|
||||||
|
publicMetadata?: {
|
||||||
|
role?: UserRole;
|
||||||
|
};
|
||||||
|
update: (data: any) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
interface ClerkSession {
|
interface ClerkSession {
|
||||||
getId: () => string;
|
getId: () => string;
|
||||||
@@ -10,8 +24,8 @@ interface ClerkSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ClerkClient {
|
interface ClerkClient {
|
||||||
user: () => ClerkUser | null;
|
user: () => any;
|
||||||
session: () => ClerkSession | null;
|
session: () => any;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -26,18 +40,20 @@ const AuthActionsContext = createContext<{
|
|||||||
|
|
||||||
export { AuthContext, AuthActionsContext };
|
export { AuthContext, AuthActionsContext };
|
||||||
|
|
||||||
function clerkUserToUser(clerkUser: ClerkUser): User {
|
function clerkUserToUser(clerkUser: any): User {
|
||||||
const primaryEmail = clerkUser.primaryEmailAddress?.emailAddress || '';
|
const primaryEmail = clerkUser.primaryEmailAddress?.emailAddress || '';
|
||||||
const firstName = clerkUser.firstName || '';
|
const firstName = clerkUser.firstName || '';
|
||||||
const lastName = clerkUser.lastName || '';
|
const lastName = clerkUser.lastName || '';
|
||||||
const name = [firstName, lastName].filter(Boolean).join(' ') || primaryEmail.split('@')[0] || 'User';
|
const name = [firstName, lastName].filter(Boolean).join(' ') || primaryEmail.split('@')[0] || 'User';
|
||||||
|
|
||||||
|
const roleFromMetadata = (clerkUser.publicMetadata?.role as UserRole) || 'viewer';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: clerkUser.id,
|
id: clerkUser.id,
|
||||||
email: primaryEmail,
|
email: primaryEmail,
|
||||||
name,
|
name,
|
||||||
avatarUrl: clerkUser.imageUrl,
|
avatarUrl: clerkUser.imageUrl,
|
||||||
role: 'owner' as UserRole,
|
role: roleFromMetadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +98,14 @@ export function ClerkProvider(props: { children: JSX.Element }) {
|
|||||||
setClerkClient(wrappedClient);
|
setClerkClient(wrappedClient);
|
||||||
|
|
||||||
if (client.user) {
|
if (client.user) {
|
||||||
|
const session = client.session as any;
|
||||||
|
if (session) {
|
||||||
|
const token = await session.getToken();
|
||||||
|
if (token) {
|
||||||
|
setAuthToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
user: clerkUserToUser(client.user),
|
user: clerkUserToUser(client.user),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -90,7 +114,26 @@ export function ClerkProvider(props: { children: JSX.Element }) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState((prev) => ({ ...prev, isLoading: false }));
|
setState((prev) => ({ ...prev, isLoading: false }));
|
||||||
|
setAuthToken(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.addListener((event) => {
|
||||||
|
if ((event as any).type === 'user' && (event as any).user) {
|
||||||
|
setState({
|
||||||
|
user: clerkUserToUser((event as any).user),
|
||||||
|
isLoading: false,
|
||||||
|
isAuthenticated: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} else if ((event as any).type === 'signOut') {
|
||||||
|
setState({
|
||||||
|
user: null,
|
||||||
|
isLoading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setState({
|
setState({
|
||||||
user: null,
|
user: null,
|
||||||
@@ -111,6 +154,7 @@ export function ClerkProvider(props: { children: JSX.Element }) {
|
|||||||
if (client) {
|
if (client) {
|
||||||
await client.signOut();
|
await client.signOut();
|
||||||
}
|
}
|
||||||
|
setAuthToken(null);
|
||||||
setState({
|
setState({
|
||||||
user: null,
|
user: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -120,6 +164,22 @@ export function ClerkProvider(props: { children: JSX.Element }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateUser = async (data: Partial<User>) => {
|
const updateUser = async (data: Partial<User>) => {
|
||||||
|
const client = getClerk();
|
||||||
|
if (!client?.user) {
|
||||||
|
throw new Error('Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clerkUser = client.user;
|
||||||
|
const updates: any = {};
|
||||||
|
|
||||||
|
if (data.name) {
|
||||||
|
const [firstName, ...lastNameParts] = data.name.split(' ');
|
||||||
|
updates.firstName = firstName;
|
||||||
|
updates.lastName = lastNameParts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
await clerkUser.update(updates);
|
||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
user: prev.user ? { ...prev.user, ...data } : null,
|
user: prev.user ? { ...prev.user, ...data } : null,
|
||||||
@@ -151,11 +211,17 @@ export function useAuthActions() {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireAuth() {
|
export function RequireAuth(props: { children: JSX.Element }) {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const authState = auth();
|
const authState = auth();
|
||||||
if (!authState.isAuthenticated) {
|
|
||||||
throw new Error('Authentication required');
|
if (authState.isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
return authState.user!;
|
|
||||||
|
if (!authState.isAuthenticated) {
|
||||||
|
return <Navigate href="/sign-in" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.children;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user