Phase 1 complete
This commit is contained in:
131
apps/frontend/src/stores/authStore.ts
Normal file
131
apps/frontend/src/stores/authStore.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { authControllerLogin } from '../api/generated/auth/auth';
|
||||
import type { LoginDto } from '../api/generated/model';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
login: (credentials: LoginDto) => Promise<void>;
|
||||
setTokens: (accessToken: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
clearError: () => void;
|
||||
init: () => void;
|
||||
}
|
||||
|
||||
// Parse JWT token to get user info
|
||||
const parseJwt = (token: string): { sub: string; email: string; role: string } | null => {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join(''),
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
init: () => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
const payload = parseJwt(token);
|
||||
if (payload) {
|
||||
set({
|
||||
user: {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
username: payload.email.split('@')[0],
|
||||
role: payload.role,
|
||||
},
|
||||
isAuthenticated: true,
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
login: async (credentials) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await authControllerLogin(credentials);
|
||||
localStorage.setItem('accessToken', response.accessToken);
|
||||
localStorage.setItem('refreshToken', response.refreshToken);
|
||||
set({
|
||||
user: response.user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Login failed',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setTokens: (accessToken, refreshToken) => {
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
localStorage.setItem('refreshToken', refreshToken);
|
||||
|
||||
const payload = parseJwt(accessToken);
|
||||
if (payload) {
|
||||
set({
|
||||
user: {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
username: payload.email.split('@')[0],
|
||||
role: payload.role,
|
||||
},
|
||||
isAuthenticated: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
120
apps/frontend/src/stores/characterStore.ts
Normal file
120
apps/frontend/src/stores/characterStore.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
characterControllerCreate,
|
||||
characterControllerFindAll,
|
||||
characterControllerFindOne,
|
||||
characterControllerUpdate,
|
||||
characterControllerDelete,
|
||||
} from '../api/generated/characters/characters';
|
||||
import type { CreateCharacterDto, UpdateCharacterDto } from '../api/generated/model';
|
||||
import type { Character } from '../types';
|
||||
|
||||
interface CharacterState {
|
||||
characters: Character[];
|
||||
currentCharacter: Character | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchCharacters: () => Promise<void>;
|
||||
getCharacter: (id: string) => Promise<Character>;
|
||||
createCharacter: (data: CreateCharacterDto) => Promise<Character>;
|
||||
updateCharacter: (id: string, data: UpdateCharacterDto) => Promise<Character>;
|
||||
deleteCharacter: (id: string) => Promise<void>;
|
||||
setCurrentCharacter: (character: Character | null) => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useCharacterStore = create<CharacterState>()((set) => ({
|
||||
characters: [],
|
||||
currentCharacter: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchCharacters: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
// The generated API returns void, we need to cast the response
|
||||
const characters = await characterControllerFindAll() as unknown as Character[];
|
||||
set({ characters, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch characters',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getCharacter: async (id) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const character = await characterControllerFindOne(id) as unknown as Character;
|
||||
set({ currentCharacter: character, isLoading: false });
|
||||
return character;
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch character',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
createCharacter: async (data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const character = await characterControllerCreate(data) as unknown as Character;
|
||||
set((state) => ({
|
||||
characters: [character, ...state.characters],
|
||||
isLoading: false,
|
||||
}));
|
||||
return character;
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to create character',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateCharacter: async (id, data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const character = await characterControllerUpdate(id, data) as unknown as Character;
|
||||
set((state) => ({
|
||||
characters: state.characters.map((c) => (c.id === id ? character : c)),
|
||||
currentCharacter: state.currentCharacter?.id === id ? character : state.currentCharacter,
|
||||
isLoading: false,
|
||||
}));
|
||||
return character;
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to update character',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteCharacter: async (id) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await characterControllerDelete(id);
|
||||
set((state) => ({
|
||||
characters: state.characters.filter((c) => c.id !== id),
|
||||
currentCharacter: state.currentCharacter?.id === id ? null : state.currentCharacter,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to delete character',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentCharacter: (character) => set({ currentCharacter: character }),
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
144
apps/frontend/src/stores/chatStore.ts
Normal file
144
apps/frontend/src/stores/chatStore.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
chatControllerCreateConversation,
|
||||
chatControllerGetConversations,
|
||||
chatControllerGetConversation,
|
||||
chatControllerDeleteConversation,
|
||||
chatControllerSendMessage,
|
||||
} from '../api/generated/conversations/conversations';
|
||||
import type { CreateConversationDto, SendMessageDto } from '../api/generated/model';
|
||||
import type { Conversation, ConversationWithMessages, Message } from '../types';
|
||||
|
||||
interface ChatState {
|
||||
conversations: Conversation[];
|
||||
currentConversation: ConversationWithMessages | null;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchConversations: () => Promise<void>;
|
||||
getConversation: (id: string) => Promise<void>;
|
||||
createConversation: (data: CreateConversationDto) => Promise<Conversation>;
|
||||
deleteConversation: (id: string) => Promise<void>;
|
||||
sendMessage: (conversationId: string, data: SendMessageDto) => Promise<void>;
|
||||
addMessage: (message: Message) => void;
|
||||
setCurrentConversation: (conversation: ConversationWithMessages | null) => void;
|
||||
setStreaming: (isStreaming: boolean) => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>()((set) => ({
|
||||
conversations: [],
|
||||
currentConversation: null,
|
||||
isLoading: false,
|
||||
isStreaming: false,
|
||||
error: null,
|
||||
|
||||
fetchConversations: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const conversations = await chatControllerGetConversations() as unknown as Conversation[];
|
||||
set({ conversations, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch conversations',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getConversation: async (id) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const conversation = await chatControllerGetConversation(id) as unknown as ConversationWithMessages;
|
||||
set({ currentConversation: conversation, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch conversation',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
createConversation: async (data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const conversation = await chatControllerCreateConversation(data) as unknown as Conversation;
|
||||
set((state) => ({
|
||||
conversations: [conversation, ...state.conversations],
|
||||
isLoading: false,
|
||||
}));
|
||||
return conversation;
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to create conversation',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteConversation: async (id) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await chatControllerDeleteConversation(id);
|
||||
set((state) => ({
|
||||
conversations: state.conversations.filter((c) => c.id !== id),
|
||||
currentConversation: state.currentConversation?.id === id ? null : state.currentConversation,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to delete conversation',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (conversationId, data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const result = await chatControllerSendMessage(conversationId, data);
|
||||
|
||||
set((state) => {
|
||||
if (!state.currentConversation) return state;
|
||||
|
||||
const newMessages: Message[] = [
|
||||
result.userMessage as Message,
|
||||
result.assistantMessage as Message,
|
||||
];
|
||||
|
||||
return {
|
||||
currentConversation: {
|
||||
...state.currentConversation,
|
||||
messages: [...state.currentConversation.messages, ...newMessages],
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to send message',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addMessage: (message) => {
|
||||
set((state) => {
|
||||
if (!state.currentConversation) return state;
|
||||
|
||||
return {
|
||||
currentConversation: {
|
||||
...state.currentConversation,
|
||||
messages: [...state.currentConversation.messages, message],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setCurrentConversation: (conversation) => set({ currentConversation: conversation }),
|
||||
setStreaming: (isStreaming) => set({ isStreaming }),
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
Reference in New Issue
Block a user