summaryrefslogtreecommitdiff
path: root/frontend/src/store
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/store')
-rw-r--r--frontend/src/store/authStore.ts157
-rw-r--r--frontend/src/store/flowStore.ts42
2 files changed, 190 insertions, 9 deletions
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
new file mode 100644
index 0000000..652256c
--- /dev/null
+++ b/frontend/src/store/authStore.ts
@@ -0,0 +1,157 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+const API_BASE = 'http://localhost:8000';
+
+interface UserInfo {
+ id: number;
+ username: string;
+ email: string;
+ created_at: string;
+}
+
+interface AuthState {
+ token: string | null;
+ user: UserInfo | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ error: string | null;
+
+ // Actions
+ login: (username: string, password: string) => Promise<void>;
+ register: (username: string, email: string, password: string) => Promise<void>;
+ logout: () => void;
+ checkAuth: () => Promise<boolean>;
+ clearError: () => void;
+ getAuthHeader: () => { Authorization: string } | {};
+}
+
+export const useAuthStore = create<AuthState>()(
+ persist(
+ (set, get) => ({
+ token: null,
+ user: null,
+ isAuthenticated: false,
+ isLoading: false,
+ error: null,
+
+ login: async (username: string, password: string) => {
+ set({ isLoading: true, error: null });
+
+ try {
+ // Use JSON login endpoint
+ const res = await fetch(`${API_BASE}/api/auth/login/json`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Login failed');
+ }
+
+ const data = await res.json();
+ set({
+ token: data.access_token,
+ isAuthenticated: true,
+ isLoading: false
+ });
+
+ // Fetch user info
+ await get().checkAuth();
+ } catch (err) {
+ set({
+ isLoading: false,
+ error: (err as Error).message,
+ isAuthenticated: false,
+ token: null,
+ user: null
+ });
+ throw err;
+ }
+ },
+
+ register: async (username: string, email: string, password: string) => {
+ set({ isLoading: true, error: null });
+
+ try {
+ const res = await fetch(`${API_BASE}/api/auth/register`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, email, password }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Registration failed');
+ }
+
+ set({ isLoading: false });
+ } catch (err) {
+ set({ isLoading: false, error: (err as Error).message });
+ throw err;
+ }
+ },
+
+ logout: () => {
+ set({
+ token: null,
+ user: null,
+ isAuthenticated: false,
+ error: null
+ });
+ },
+
+ checkAuth: async () => {
+ const token = get().token;
+ if (!token) {
+ set({ isAuthenticated: false, user: null });
+ return false;
+ }
+
+ try {
+ const res = await fetch(`${API_BASE}/api/auth/me`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+
+ if (res.ok) {
+ const user = await res.json();
+ set({ user, isAuthenticated: true });
+ return true;
+ } else {
+ // Token is invalid
+ set({ token: null, user: null, isAuthenticated: false });
+ return false;
+ }
+ } catch {
+ set({ token: null, user: null, isAuthenticated: false });
+ return false;
+ }
+ },
+
+ clearError: () => {
+ set({ error: null });
+ },
+
+ getAuthHeader: () => {
+ const token = get().token;
+ if (token) {
+ return { Authorization: `Bearer ${token}` };
+ }
+ return {};
+ },
+ }),
+ {
+ name: 'contextflow-auth',
+ partialize: (state) => ({
+ token: state.token,
+ user: state.user,
+ isAuthenticated: state.isAuthenticated
+ }),
+ }
+ )
+);
+
+export default useAuthStore;
+
diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts
index 498937e..de23c95 100644
--- a/frontend/src/store/flowStore.ts
+++ b/frontend/src/store/flowStore.ts
@@ -250,11 +250,35 @@ const getStableColor = (str: string) => {
return `hsl(${hue}, 70%, 60%)`;
};
+import { useAuthStore } from './authStore';
+
const API_BASE = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000';
-const DEFAULT_USER = 'test';
+const DEFAULT_USER = 'test'; // Fallback for unauthenticated requests
+
+// Get current username directly from authStore
+const getCurrentUser = () => {
+ const authState = useAuthStore.getState();
+ return authState.user?.username || DEFAULT_USER;
+};
+
+// Get auth headers directly from authStore
+const getAuthHeaders = (): Record<string, string> => {
+ const authState = useAuthStore.getState();
+ if (authState.token) {
+ return { Authorization: `Bearer ${authState.token}` };
+ }
+ return {};
+};
const jsonFetch = async <T>(url: string, options?: RequestInit): Promise<T> => {
- const res = await fetch(url, options);
+ const authHeaders = getAuthHeaders();
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ ...options?.headers,
+ ...authHeaders,
+ },
+ });
if (!res.ok) {
const detail = await res.text();
throw new Error(detail || `Request failed: ${res.status}`);
@@ -1474,7 +1498,7 @@ const useFlowStore = create<FlowState>((set, get) => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- user: DEFAULT_USER,
+ user: getCurrentUser(),
path,
content: payload,
}),
@@ -1485,14 +1509,14 @@ const useFlowStore = create<FlowState>((set, get) => {
readBlueprintFile: async (path: string): Promise<BlueprintDocument> => {
const res = await jsonFetch<{ content: BlueprintDocument }>(
- `${API_BASE}/api/projects/file?user=${encodeURIComponent(DEFAULT_USER)}&path=${encodeURIComponent(path)}`
+ `${API_BASE}/api/projects/file?user=${encodeURIComponent(getCurrentUser())}&path=${encodeURIComponent(path)}`
);
return validateBlueprint(res.content);
},
refreshProjectTree: async () => {
const tree = await jsonFetch<FSItem[]>(
- `${API_BASE}/api/projects/tree?user=${encodeURIComponent(DEFAULT_USER)}`
+ `${API_BASE}/api/projects/tree?user=${encodeURIComponent(getCurrentUser())}`
);
set({ projectTree: tree });
return tree;
@@ -1557,7 +1581,7 @@ const useFlowStore = create<FlowState>((set, get) => {
loadArchivedNodes: async () => {
const res = await jsonFetch<{ archived: ArchivedNode[] }>(
- `${API_BASE}/api/projects/archived?user=${encodeURIComponent(DEFAULT_USER)}`
+ `${API_BASE}/api/projects/archived?user=${encodeURIComponent(getCurrentUser())}`
);
set({ archivedNodes: res.archived || [] });
},
@@ -1574,7 +1598,7 @@ const useFlowStore = create<FlowState>((set, get) => {
// Files management
refreshFiles: async () => {
const res = await jsonFetch<{ files: FileMeta[] }>(
- `${API_BASE}/api/files?user=${encodeURIComponent(DEFAULT_USER)}`
+ `${API_BASE}/api/files?user=${encodeURIComponent(getCurrentUser())}`
);
set({ files: res.files || [] });
},
@@ -1592,7 +1616,7 @@ const useFlowStore = create<FlowState>((set, get) => {
form.append('purpose', purpose);
}
try {
- const res = await fetch(`${API_BASE}/api/files/upload?user=${encodeURIComponent(DEFAULT_USER)}`, {
+ const res = await fetch(`${API_BASE}/api/files/upload?user=${encodeURIComponent(getCurrentUser())}`, {
method: 'POST',
body: form,
});
@@ -1611,7 +1635,7 @@ const useFlowStore = create<FlowState>((set, get) => {
},
deleteFile: async (fileId: string) => {
- const res = await fetch(`${API_BASE}/api/files/delete?user=${encodeURIComponent(DEFAULT_USER)}&file_id=${encodeURIComponent(fileId)}`, {
+ const res = await fetch(`${API_BASE}/api/files/delete?user=${encodeURIComponent(getCurrentUser())}&file_id=${encodeURIComponent(fileId)}`, {
method: 'POST',
});
if (!res.ok) {