From 043eab423fa55fc2bcfe9e7667952a2f631a3a7d Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 3 May 2025 21:28:41 +0200 Subject: [PATCH] Migrating from services to dedicated API files --- .../{services/adminApi.js => api/admin.js} | 2 +- app/src/api/api.ts | 47 ++++++ app/src/api/auth.ts | 93 ++++++++++++ app/src/{services/api.js => api/notes.js} | 2 +- app/src/components/editor/ContentView.jsx | 2 +- .../modals/workspace/CreateWorkspaceModal.jsx | 2 +- .../navigation/WorkspaceSwitcher.jsx | 2 +- app/src/contexts/AuthContext.jsx | 2 +- app/src/contexts/WorkspaceContext.jsx | 2 +- app/src/hooks/useAdminData.js | 2 +- app/src/hooks/useFileContent.js | 2 +- app/src/hooks/useFileList.js | 2 +- app/src/hooks/useFileOperations.js | 2 +- app/src/hooks/useGitOperations.js | 2 +- app/src/hooks/useLastOpenedFile.js | 2 +- app/src/hooks/useProfileSettings.js | 2 +- app/src/hooks/useUserAdmin.js | 2 +- app/src/services/authApi.ts | 138 ------------------ app/src/types/api.ts | 59 -------- app/src/types/authApi.ts | 105 +++++++++++++ app/src/types/markdown.ts | 4 - app/src/utils/remarkWikiLinks.ts | 17 +-- 22 files changed, 265 insertions(+), 228 deletions(-) rename app/src/{services/adminApi.js => api/admin.js} (97%) create mode 100644 app/src/api/api.ts create mode 100644 app/src/api/auth.ts rename app/src/{services/api.js => api/notes.js} (99%) delete mode 100644 app/src/services/authApi.ts delete mode 100644 app/src/types/api.ts create mode 100644 app/src/types/authApi.ts diff --git a/app/src/services/adminApi.js b/app/src/api/admin.js similarity index 97% rename from app/src/services/adminApi.js rename to app/src/api/admin.js index 3c011d0..d8b93cc 100644 --- a/app/src/services/adminApi.js +++ b/app/src/api/admin.js @@ -1,4 +1,4 @@ -import { apiCall } from './authApi'; +import { apiCall } from './auth'; import { API_BASE_URL } from '../utils/constants'; const ADMIN_BASE_URL = `${API_BASE_URL}/admin`; diff --git a/app/src/api/api.ts b/app/src/api/api.ts new file mode 100644 index 0000000..c56f23f --- /dev/null +++ b/app/src/api/api.ts @@ -0,0 +1,47 @@ +import { refreshToken } from './auth'; + +/** + * Makes an API call with proper cookie handling and error handling + */ +export const apiCall = async ( + url: string, + options: RequestInit = {} +): Promise => { + try { + const response = await fetch(url, { + ...options, + // Include credentials to send/receive cookies + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + + // Handle 401 responses + if (response.status === 401) { + const isRefreshEndpoint = url.endsWith('/auth/refresh'); + if (!isRefreshEndpoint) { + // Attempt token refresh and retry the request + const refreshSuccess = await refreshToken(); + if (refreshSuccess) { + // Retry the original request + return apiCall(url, options); + } + } + throw new Error('Authentication failed'); + } + + if (!response.ok && response.status !== 204) { + const errorData = (await response.json()) as { message: string }; + throw new Error( + errorData?.message || `HTTP error! status: ${response.status}` + ); + } + + return response; + } catch (error) { + console.error(`API call failed: ${(error as Error).message}`); + throw error; + } +}; diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts new file mode 100644 index 0000000..6eb0a05 --- /dev/null +++ b/app/src/api/auth.ts @@ -0,0 +1,93 @@ +import { + API_BASE_URL, + User, + LoginRequest, + LoginResponse, + isLoginResponse, + ErrorResponse, + isUser, +} from '../types/authApi'; +import { apiCall } from './api'; + +/** + * Logs in a user with email and password + */ +export const login = async ( + email: string, + password: string +): Promise => { + const loginData: LoginRequest = { email, password }; + const response = await apiCall(`${API_BASE_URL}/auth/login`, { + method: 'POST', + body: JSON.stringify(loginData), + }); + + if (!response.ok) { + const data = await response.json(); + const errorData = data as ErrorResponse; + throw new Error(errorData.message || 'Login failed'); + } + + const data = await response.json(); + if (!isLoginResponse(data)) { + throw new Error('Invalid login response received from API'); + } + + return data; +}; + +/** + * Logs out the current user + */ +export const logout = async (): Promise => { + const response = await apiCall(`${API_BASE_URL}/auth/logout`, { + method: 'POST', + }); + + if (!response.ok) { + const data = await response.json(); + const errorData = data as ErrorResponse; + throw new Error(errorData.message || 'Logout failed'); + } +}; + +/** + * Refreshes the auth token + * @returns true if refresh was successful, false otherwise + */ +export const refreshToken = async (): Promise => { + try { + const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { + method: 'POST', + }); + + if (!response.ok) { + const data = await response.json(); + const errorData = data as ErrorResponse; + throw new Error(errorData.message || 'Token refresh failed'); + } + + return true; + } catch (error) { + return false; + } +}; + +/** + * Gets the currently authenticated user + */ +export const getCurrentUser = async (): Promise => { + const response = await apiCall(`${API_BASE_URL}/auth/me`); + const data = await response.json(); + + if (!response.ok) { + const errorData = data as ErrorResponse; + throw new Error(errorData.message || 'Failed to get current user'); + } + + if (!isUser(data)) { + throw new Error('Invalid user data received from API'); + } + + return data; +}; diff --git a/app/src/services/api.js b/app/src/api/notes.js similarity index 99% rename from app/src/services/api.js rename to app/src/api/notes.js index 626f684..a635da9 100644 --- a/app/src/services/api.js +++ b/app/src/api/notes.js @@ -1,5 +1,5 @@ import { API_BASE_URL } from '../utils/constants'; -import { apiCall } from './authApi'; +import { apiCall } from './auth'; export const updateProfile = async (updates) => { const response = await apiCall(`${API_BASE_URL}/profile`, { diff --git a/app/src/components/editor/ContentView.jsx b/app/src/components/editor/ContentView.jsx index 11eda30..7c075b5 100644 --- a/app/src/components/editor/ContentView.jsx +++ b/app/src/components/editor/ContentView.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Text, Center } from '@mantine/core'; import Editor from './Editor'; import MarkdownPreview from './MarkdownPreview'; -import { getFileUrl } from '../../services/api'; +import { getFileUrl } from '../../api/notes'; import { isImageFile } from '../../utils/fileHelpers'; const ContentView = ({ diff --git a/app/src/components/modals/workspace/CreateWorkspaceModal.jsx b/app/src/components/modals/workspace/CreateWorkspaceModal.jsx index d847af1..75048f2 100644 --- a/app/src/components/modals/workspace/CreateWorkspaceModal.jsx +++ b/app/src/components/modals/workspace/CreateWorkspaceModal.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Modal, TextInput, Button, Group, Box } from '@mantine/core'; import { useModalContext } from '../../../contexts/ModalContext'; -import { createWorkspace } from '../../../services/api'; +import { createWorkspace } from '../../../api/notes'; import { notifications } from '@mantine/notifications'; const CreateWorkspaceModal = ({ onWorkspaceCreated }) => { diff --git a/app/src/components/navigation/WorkspaceSwitcher.jsx b/app/src/components/navigation/WorkspaceSwitcher.jsx index 6ddc0af..77be4ee 100644 --- a/app/src/components/navigation/WorkspaceSwitcher.jsx +++ b/app/src/components/navigation/WorkspaceSwitcher.jsx @@ -17,7 +17,7 @@ import { import { IconFolders, IconSettings, IconFolderPlus } from '@tabler/icons-react'; import { useWorkspace } from '../../contexts/WorkspaceContext'; import { useModalContext } from '../../contexts/ModalContext'; -import { listWorkspaces } from '../../services/api'; +import { listWorkspaces } from '../../api/notes'; import CreateWorkspaceModal from '../modals/workspace/CreateWorkspaceModal'; const WorkspaceSwitcher = () => { diff --git a/app/src/contexts/AuthContext.jsx b/app/src/contexts/AuthContext.jsx index 4878e05..20faa34 100644 --- a/app/src/contexts/AuthContext.jsx +++ b/app/src/contexts/AuthContext.jsx @@ -6,7 +6,7 @@ import React, { useEffect, } from 'react'; import { notifications } from '@mantine/notifications'; -import * as authApi from '../services/authApi'; +import * as authApi from '../api/auth'; const AuthContext = createContext(null); diff --git a/app/src/contexts/WorkspaceContext.jsx b/app/src/contexts/WorkspaceContext.jsx index beddf4f..5fee428 100644 --- a/app/src/contexts/WorkspaceContext.jsx +++ b/app/src/contexts/WorkspaceContext.jsx @@ -14,7 +14,7 @@ import { updateLastWorkspaceName, deleteWorkspace, listWorkspaces, -} from '../services/api'; +} from '../api/notes'; import { DEFAULT_WORKSPACE_SETTINGS } from '../utils/constants'; const WorkspaceContext = createContext(); diff --git a/app/src/hooks/useAdminData.js b/app/src/hooks/useAdminData.js index 9669ad3..6357be3 100644 --- a/app/src/hooks/useAdminData.js +++ b/app/src/hooks/useAdminData.js @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { notifications } from '@mantine/notifications'; -import { getUsers, getWorkspaces, getSystemStats } from '../services/adminApi'; +import { getUsers, getWorkspaces, getSystemStats } from '../api/admin'; // Hook for admin data fetching (stats and workspaces) export const useAdminData = (type) => { diff --git a/app/src/hooks/useFileContent.js b/app/src/hooks/useFileContent.js index 21b8776..4c20937 100644 --- a/app/src/hooks/useFileContent.js +++ b/app/src/hooks/useFileContent.js @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect } from 'react'; -import { fetchFileContent } from '../services/api'; +import { fetchFileContent } from '../api/notes'; import { isImageFile } from '../utils/fileHelpers'; import { DEFAULT_FILE } from '../utils/constants'; import { useWorkspace } from '../contexts/WorkspaceContext'; diff --git a/app/src/hooks/useFileList.js b/app/src/hooks/useFileList.js index 4557f1f..2505f30 100644 --- a/app/src/hooks/useFileList.js +++ b/app/src/hooks/useFileList.js @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react'; -import { fetchFileList } from '../services/api'; +import { fetchFileList } from '../api/notes'; import { useWorkspace } from '../contexts/WorkspaceContext'; export const useFileList = () => { diff --git a/app/src/hooks/useFileOperations.js b/app/src/hooks/useFileOperations.js index 6df5dae..5f86469 100644 --- a/app/src/hooks/useFileOperations.js +++ b/app/src/hooks/useFileOperations.js @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { notifications } from '@mantine/notifications'; -import { saveFileContent, deleteFile } from '../services/api'; +import { saveFileContent, deleteFile } from '../api/notes'; import { useWorkspace } from '../contexts/WorkspaceContext'; import { useGitOperations } from './useGitOperations'; diff --git a/app/src/hooks/useGitOperations.js b/app/src/hooks/useGitOperations.js index 9a3e5b3..7399e28 100644 --- a/app/src/hooks/useGitOperations.js +++ b/app/src/hooks/useGitOperations.js @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { notifications } from '@mantine/notifications'; -import { pullChanges, commitAndPush } from '../services/api'; +import { pullChanges, commitAndPush } from '../api/notes'; import { useWorkspace } from '../contexts/WorkspaceContext'; export const useGitOperations = () => { diff --git a/app/src/hooks/useLastOpenedFile.js b/app/src/hooks/useLastOpenedFile.js index d1b8bef..e49ff02 100644 --- a/app/src/hooks/useLastOpenedFile.js +++ b/app/src/hooks/useLastOpenedFile.js @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { getLastOpenedFile, updateLastOpenedFile } from '../services/api'; +import { getLastOpenedFile, updateLastOpenedFile } from '../api/notes'; import { useWorkspace } from '../contexts/WorkspaceContext'; export const useLastOpenedFile = () => { diff --git a/app/src/hooks/useProfileSettings.js b/app/src/hooks/useProfileSettings.js index 381e784..27a1155 100644 --- a/app/src/hooks/useProfileSettings.js +++ b/app/src/hooks/useProfileSettings.js @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { notifications } from '@mantine/notifications'; -import { updateProfile, deleteProfile } from '../services/api'; +import { updateProfile, deleteProfile } from '../api/notes'; export function useProfileSettings() { const [loading, setLoading] = useState(false); diff --git a/app/src/hooks/useUserAdmin.js b/app/src/hooks/useUserAdmin.js index f8e0a1d..777a6e3 100644 --- a/app/src/hooks/useUserAdmin.js +++ b/app/src/hooks/useUserAdmin.js @@ -1,5 +1,5 @@ import { useAdminData } from './useAdminData'; -import { createUser, updateUser, deleteUser } from '../services/adminApi'; +import { createUser, updateUser, deleteUser } from '../api/admin'; import { notifications } from '@mantine/notifications'; export const useUserAdmin = () => { diff --git a/app/src/services/authApi.ts b/app/src/services/authApi.ts deleted file mode 100644 index 1bed5a9..0000000 --- a/app/src/services/authApi.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - API_BASE_URL, - User, - LoginRequest, - LoginResponse, - ApiCallOptions, - ErrorResponse -} from '../types/api'; - -let authToken: string | null = null; - -/** - * Sets the authentication token for API requests - */ -export const setAuthToken = (token: string): void => { - authToken = token; -}; - -/** - * Clears the authentication token - */ -export const clearAuthToken = (): void => { - authToken = null; -}; - -/** - * Gets headers for API requests including auth token if present - */ -export const getAuthHeaders = (): HeadersInit => { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (authToken) { - headers['Authorization'] = `Bearer ${authToken}`; - } - - return headers; -}; - -/** - * Makes an API call with authentication and error handling - */ -export const apiCall = async ( - url: string, - options: ApiCallOptions = {} -): Promise => { - try { - const headers = { - ...getAuthHeaders(), - ...options.headers, - }; - - const response = await fetch(url, { - ...options, - headers: headers as HeadersInit, - }); - - // Handle 401 responses - if (response.status === 401) { - const isRefreshEndpoint = url.endsWith('/auth/refresh'); - if (!isRefreshEndpoint) { - // Attempt token refresh and retry the request - const refreshSuccess = await refreshToken(); - if (refreshSuccess) { - // Retry the original request with the new token - return apiCall(url, options); - } - } - throw new Error('Authentication failed'); - } - - if (!response.ok && response.status !== 204) { - const errorData = await response.json() as ErrorResponse; - throw new Error( - errorData?.message || `HTTP error! status: ${response.status}` - ); - } - - return response; - } catch (error) { - console.error(`API call failed: ${(error as Error).message}`); - throw error; - } -}; - -/** - * Logs in a user with email and password - */ -export const login = async ( - email: string, - password: string -): Promise => { - const loginData: LoginRequest = { email, password }; - const response = await apiCall(`${API_BASE_URL}/auth/login`, { - method: 'POST', - body: JSON.stringify(loginData), - }); - return response.json(); -}; - -/** - * Logs out the current user - */ -export const logout = async (): Promise => { - const sessionId = localStorage.getItem('sessionId'); - await apiCall(`${API_BASE_URL}/auth/logout`, { - method: 'POST', - headers: { - 'X-Session-ID': sessionId || '', - }, - }); -}; - -/** - * Refreshes the auth token using a refresh token - */ -export const refreshToken = async (): Promise => { - const refreshToken = localStorage.getItem('refreshToken'); - try { - const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { - method: 'POST', - body: JSON.stringify({ refreshToken }), - }); - const data = await response.json(); - return !!data.accessToken; - } catch (error) { - return false; - } -}; - -/** - * Gets the currently authenticated user - */ -export const getCurrentUser = async (): Promise => { - const response = await apiCall(`${API_BASE_URL}/auth/me`); - return response.json(); -}; \ No newline at end of file diff --git a/app/src/types/api.ts b/app/src/types/api.ts deleted file mode 100644 index d556639..0000000 --- a/app/src/types/api.ts +++ /dev/null @@ -1,59 +0,0 @@ -declare global { - interface Window { - API_BASE_URL: string; - } -} - -export const API_BASE_URL = window.API_BASE_URL; - -/** - * User role in the system - */ -export enum UserRole { - Admin = 'admin', - Editor = 'editor', - Viewer = 'viewer' -} - -/** - * User model from the API - */ -export interface User { - id: number; - email: string; - displayName?: string; - role: UserRole; - createdAt: string; - lastWorkspaceId: number; -} - -/** - * Error response from the API - */ -export interface ErrorResponse { - message: string; -} - -/** - * Login request parameters - */ -export interface LoginRequest { - email: string; - password: string; -} - -/** - * Login response from the API - */ -export interface LoginResponse { - user: User; - sessionId: string; - expiresAt: string; -} - -/** - * API call options extending the standard RequestInit - */ -export interface ApiCallOptions extends RequestInit { - headers?: HeadersInit; -} \ No newline at end of file diff --git a/app/src/types/authApi.ts b/app/src/types/authApi.ts new file mode 100644 index 0000000..ca5337a --- /dev/null +++ b/app/src/types/authApi.ts @@ -0,0 +1,105 @@ +declare global { + interface Window { + API_BASE_URL: string; + } +} + +export const API_BASE_URL = window.API_BASE_URL; + +/** + * User role in the system + */ +export enum UserRole { + Admin = 'admin', + Editor = 'editor', + Viewer = 'viewer', +} + +/** + * Type guard to check if a value is a valid UserRole + */ +export function isUserRole(value: unknown): value is UserRole { + return typeof value === 'string' && value in UserRole; +} + +/** + * User model from the API + */ +export interface User { + id: number; + email: string; + displayName?: string; + role: UserRole; + createdAt: string; + lastWorkspaceId: number; +} + +/** + * Type guard to check if a value is a valid User + */ +export function isUser(value: unknown): value is User { + return ( + typeof value === 'object' && + value !== null && + 'id' in value && + typeof (value as User).id === 'number' && + 'email' in value && + typeof (value as User).email === 'string' && + ('displayName' in value + ? typeof (value as User).displayName === 'string' + : true) && + 'role' in value && + isUserRole((value as User).role) && + 'createdAt' in value && + typeof (value as User).createdAt === 'string' && + 'lastWorkspaceId' in value && + typeof (value as User).lastWorkspaceId === 'number' + ); +} + +/** + * Error response from the API + */ +export interface ErrorResponse { + message: string; +} + +/** + * Login request parameters + */ +export interface LoginRequest { + email: string; + password: string; +} + +/** + * Login response from the API + */ +export interface LoginResponse { + user: User; + sessionId: string; + expiresAt: string; +} + +/** + * Type guard to check if a value is a valid LoginResponse + */ +export function isLoginResponse(value: unknown): value is LoginResponse { + return ( + typeof value === 'object' && + value !== null && + 'user' in value && + isUser((value as LoginResponse).user) && + 'sessionId' in value && + typeof (value as LoginResponse).sessionId === 'string' && + 'expiresAt' in value && + typeof (value as LoginResponse).expiresAt === 'string' + ); +} + +/** + * API call options extending the standard RequestInit + */ +export interface ApiCallOptions extends RequestInit { + headers?: HeadersInit; +} diff --git a/app/src/types/markdown.ts b/app/src/types/markdown.ts index 96a9a77..e36c59a 100644 --- a/app/src/types/markdown.ts +++ b/app/src/types/markdown.ts @@ -9,10 +9,6 @@ export enum InlineContainerType { Delete = 'delete', } -export const INLINE_CONTAINER_TYPES = new Set( - Object.values(InlineContainerType) -); - export const MARKDOWN_REGEX = { WIKILINK: /(!?)\[\[(.*?)\]\]/g, } as const; diff --git a/app/src/utils/remarkWikiLinks.ts b/app/src/utils/remarkWikiLinks.ts index 9b7cf6f..569501e 100644 --- a/app/src/utils/remarkWikiLinks.ts +++ b/app/src/utils/remarkWikiLinks.ts @@ -1,6 +1,6 @@ import { visit } from 'unist-util-visit'; -import { lookupFileByName, getFileUrl } from '../services/api'; -import { MARKDOWN_REGEX } from '../types/markdown'; +import { lookupFileByName, getFileUrl } from '../api/notes'; +import { InlineContainerType, MARKDOWN_REGEX } from '../types/markdown'; import { Node } from 'unist'; import { Parent } from 'unist'; import { Text } from 'mdast'; @@ -146,16 +146,9 @@ function addMarkdownExtension(fileName: string): string { * Determines if a node type can contain inline content */ function canContainInline(type: string): boolean { - return [ - 'paragraph', - 'listItem', - 'tableCell', - 'blockquote', - 'heading', - 'emphasis', - 'strong', - 'delete', - ].includes(type); + return Object.values(InlineContainerType).includes( + type as InlineContainerType + ); } /**