wip 13
This commit is contained in:
@@ -35,23 +35,31 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setAuthState(prev => {
|
||||
if (meLoading) {
|
||||
// Still checking authentication
|
||||
console.log('[AuthContext] Checking authentication status...');
|
||||
return { ...prev, isCheckingAuth: true };
|
||||
} else if (meData?.data) {
|
||||
// User is authenticated
|
||||
} else if (meData?.data && meData.status === 200) {
|
||||
// User is authenticated - check that response has valid data
|
||||
console.log('[AuthContext] User authenticated:', meData.data);
|
||||
return {
|
||||
isAuthenticated: true,
|
||||
user: meData.data,
|
||||
isCheckingAuth: false,
|
||||
};
|
||||
} else if (meError) {
|
||||
} else if (
|
||||
meError ||
|
||||
(meData && meData.status === 401) ||
|
||||
(meData && meData.status === 403)
|
||||
) {
|
||||
// User is not authenticated or error occurred
|
||||
console.log('[AuthContext] User not authenticated:', meError?.message || String(meError));
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
isCheckingAuth: false,
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
console.log('[AuthContext] Unexpected state - checking...');
|
||||
return { ...prev, isCheckingAuth: false }; // Assume not authenticated if we can't determine
|
||||
});
|
||||
}, [meData, meError, meLoading]);
|
||||
|
||||
@@ -75,6 +83,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
navigate('/');
|
||||
} catch (_error) {
|
||||
console.error('[AuthContext] Login failed:', _error);
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -77,11 +77,11 @@ function ToastContainer({ toasts }: ToastContainerProps) {
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2">
|
||||
<div className="pointer-events-auto">
|
||||
{toasts.map(toast => (
|
||||
<Toast key={toast.id} {...toast} />
|
||||
))}
|
||||
</div>
|
||||
{toasts.map(toast => (
|
||||
<div key={toast.id} className="pointer-events-auto">
|
||||
<Toast {...toast} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
LogsResponse,
|
||||
MessageResponse,
|
||||
PostAdminActionBody,
|
||||
PostImportBody,
|
||||
PostSearchBody,
|
||||
@@ -440,6 +441,279 @@ export function useGetDocument<TData = Awaited<ReturnType<typeof getDocument>>,
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get document cover image
|
||||
*/
|
||||
export type getDocumentCoverResponse200ImageJpeg = {
|
||||
data: Blob
|
||||
status: 200
|
||||
}
|
||||
|
||||
export type getDocumentCoverResponse200ImagePng = {
|
||||
data: Blob
|
||||
status: 200
|
||||
}
|
||||
|
||||
export type getDocumentCoverResponse401 = {
|
||||
data: ErrorResponse
|
||||
status: 401
|
||||
}
|
||||
|
||||
export type getDocumentCoverResponse404 = {
|
||||
data: ErrorResponse
|
||||
status: 404
|
||||
}
|
||||
|
||||
export type getDocumentCoverResponse500 = {
|
||||
data: ErrorResponse
|
||||
status: 500
|
||||
}
|
||||
|
||||
export type getDocumentCoverResponseSuccess = (getDocumentCoverResponse200ImageJpeg | getDocumentCoverResponse200ImagePng) & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type getDocumentCoverResponseError = (getDocumentCoverResponse401 | getDocumentCoverResponse404 | getDocumentCoverResponse500) & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type getDocumentCoverResponse = (getDocumentCoverResponseSuccess | getDocumentCoverResponseError)
|
||||
|
||||
export const getGetDocumentCoverUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/v1/documents/${id}/cover`
|
||||
}
|
||||
|
||||
export const getDocumentCover = async (id: string, options?: RequestInit): Promise<getDocumentCoverResponse> => {
|
||||
|
||||
const res = await fetch(getGetDocumentCoverUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
||||
|
||||
const data: getDocumentCoverResponse['data'] = body ? JSON.parse(body) : {}
|
||||
return { data, status: res.status, headers: res.headers } as getDocumentCoverResponse
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetDocumentCoverQueryKey = (id: string,) => {
|
||||
return [
|
||||
`/api/v1/documents/${id}/cover`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetDocumentCoverQueryOptions = <TData = Awaited<ReturnType<typeof getDocumentCover>>, TError = ErrorResponse>(id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData>>, fetch?: RequestInit}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, fetch: fetchOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetDocumentCoverQueryKey(id);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDocumentCover>>> = ({ signal }) => getDocumentCover(id, { signal, ...fetchOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetDocumentCoverQueryResult = NonNullable<Awaited<ReturnType<typeof getDocumentCover>>>
|
||||
export type GetDocumentCoverQueryError = ErrorResponse
|
||||
|
||||
|
||||
export function useGetDocumentCover<TData = Awaited<ReturnType<typeof getDocumentCover>>, TError = ErrorResponse>(
|
||||
id: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getDocumentCover>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getDocumentCover>>
|
||||
> , 'initialData'
|
||||
>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetDocumentCover<TData = Awaited<ReturnType<typeof getDocumentCover>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getDocumentCover>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getDocumentCover>>
|
||||
> , 'initialData'
|
||||
>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetDocumentCover<TData = Awaited<ReturnType<typeof getDocumentCover>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData>>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get document cover image
|
||||
*/
|
||||
|
||||
export function useGetDocumentCover<TData = Awaited<ReturnType<typeof getDocumentCover>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentCover>>, TError, TData>>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetDocumentCoverQueryOptions(id,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Download document file
|
||||
*/
|
||||
export type getDocumentFileResponse200 = {
|
||||
data: Blob
|
||||
status: 200
|
||||
}
|
||||
|
||||
export type getDocumentFileResponse401 = {
|
||||
data: ErrorResponse
|
||||
status: 401
|
||||
}
|
||||
|
||||
export type getDocumentFileResponse404 = {
|
||||
data: ErrorResponse
|
||||
status: 404
|
||||
}
|
||||
|
||||
export type getDocumentFileResponse500 = {
|
||||
data: ErrorResponse
|
||||
status: 500
|
||||
}
|
||||
|
||||
export type getDocumentFileResponseSuccess = (getDocumentFileResponse200) & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type getDocumentFileResponseError = (getDocumentFileResponse401 | getDocumentFileResponse404 | getDocumentFileResponse500) & {
|
||||
headers: Headers;
|
||||
};
|
||||
|
||||
export type getDocumentFileResponse = (getDocumentFileResponseSuccess | getDocumentFileResponseError)
|
||||
|
||||
export const getGetDocumentFileUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/v1/documents/${id}/file`
|
||||
}
|
||||
|
||||
export const getDocumentFile = async (id: string, options?: RequestInit): Promise<getDocumentFileResponse> => {
|
||||
|
||||
const res = await fetch(getGetDocumentFileUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
||||
|
||||
const data: getDocumentFileResponse['data'] = body ? JSON.parse(body) : {}
|
||||
return { data, status: res.status, headers: res.headers } as getDocumentFileResponse
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetDocumentFileQueryKey = (id: string,) => {
|
||||
return [
|
||||
`/api/v1/documents/${id}/file`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetDocumentFileQueryOptions = <TData = Awaited<ReturnType<typeof getDocumentFile>>, TError = ErrorResponse>(id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData>>, fetch?: RequestInit}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, fetch: fetchOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetDocumentFileQueryKey(id);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDocumentFile>>> = ({ signal }) => getDocumentFile(id, { signal, ...fetchOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetDocumentFileQueryResult = NonNullable<Awaited<ReturnType<typeof getDocumentFile>>>
|
||||
export type GetDocumentFileQueryError = ErrorResponse
|
||||
|
||||
|
||||
export function useGetDocumentFile<TData = Awaited<ReturnType<typeof getDocumentFile>>, TError = ErrorResponse>(
|
||||
id: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getDocumentFile>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getDocumentFile>>
|
||||
> , 'initialData'
|
||||
>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetDocumentFile<TData = Awaited<ReturnType<typeof getDocumentFile>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getDocumentFile>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getDocumentFile>>
|
||||
> , 'initialData'
|
||||
>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetDocumentFile<TData = Awaited<ReturnType<typeof getDocumentFile>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData>>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Download document file
|
||||
*/
|
||||
|
||||
export function useGetDocumentFile<TData = Awaited<ReturnType<typeof getDocumentFile>>, TError = ErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentFile>>, TError, TData>>, fetch?: RequestInit}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetDocumentFileQueryOptions(id,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary List progress records
|
||||
*/
|
||||
@@ -2296,7 +2570,12 @@ export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError
|
||||
/**
|
||||
* @summary Perform admin action (backup, restore, etc.)
|
||||
*/
|
||||
export type postAdminActionResponse200 = {
|
||||
export type postAdminActionResponse200ApplicationJson = {
|
||||
data: MessageResponse
|
||||
status: 200
|
||||
}
|
||||
|
||||
export type postAdminActionResponse200ApplicationOctetStream = {
|
||||
data: Blob
|
||||
status: 200
|
||||
}
|
||||
@@ -2316,7 +2595,7 @@ export type postAdminActionResponse500 = {
|
||||
status: 500
|
||||
}
|
||||
|
||||
export type postAdminActionResponseSuccess = (postAdminActionResponse200) & {
|
||||
export type postAdminActionResponseSuccess = (postAdminActionResponse200ApplicationJson | postAdminActionResponse200ApplicationOctetStream) & {
|
||||
headers: Headers;
|
||||
};
|
||||
export type postAdminActionResponseError = (postAdminActionResponse400 | postAdminActionResponse401 | postAdminActionResponse500) & {
|
||||
@@ -2334,22 +2613,22 @@ export const getPostAdminActionUrl = () => {
|
||||
}
|
||||
|
||||
export const postAdminAction = async (postAdminActionBody: PostAdminActionBody, options?: RequestInit): Promise<postAdminActionResponse> => {
|
||||
const formUrlEncoded = new URLSearchParams();
|
||||
formUrlEncoded.append(`action`, postAdminActionBody.action);
|
||||
const formData = new FormData();
|
||||
formData.append(`action`, postAdminActionBody.action);
|
||||
if(postAdminActionBody.backup_types !== undefined) {
|
||||
postAdminActionBody.backup_types.forEach(value => formUrlEncoded.append(`backup_types`, value));
|
||||
postAdminActionBody.backup_types.forEach(value => formData.append(`backup_types`, value));
|
||||
}
|
||||
if(postAdminActionBody.restore_file !== undefined) {
|
||||
formUrlEncoded.append(`restore_file`, postAdminActionBody.restore_file);
|
||||
formData.append(`restore_file`, postAdminActionBody.restore_file);
|
||||
}
|
||||
|
||||
const res = await fetch(getPostAdminActionUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options?.headers },
|
||||
method: 'POST'
|
||||
,
|
||||
body:
|
||||
formUrlEncoded,
|
||||
formData,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export * from './logEntry';
|
||||
export * from './loginRequest';
|
||||
export * from './loginResponse';
|
||||
export * from './logsResponse';
|
||||
export * from './messageResponse';
|
||||
export * from './operationType';
|
||||
export * from './postAdminActionBody';
|
||||
export * from './postAdminActionBodyAction';
|
||||
|
||||
11
frontend/src/generated/model/messageResponse.ts
Normal file
11
frontend/src/generated/model/messageResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
@@ -19,52 +19,79 @@ export default function AdminPage() {
|
||||
});
|
||||
const [restoreFile, setRestoreFile] = useState<File | null>(null);
|
||||
|
||||
const handleBackupSubmit = (e: FormEvent) => {
|
||||
const handleBackupSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const backupTypesList: string[] = [];
|
||||
if (backupTypes.covers) backupTypesList.push('COVERS');
|
||||
if (backupTypes.documents) backupTypesList.push('DOCUMENTS');
|
||||
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: {
|
||||
action: 'BACKUP',
|
||||
backup_types: backupTypesList as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: response => {
|
||||
// Handle file download
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute(
|
||||
'download',
|
||||
`AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`
|
||||
);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
showInfo('Backup completed successfully');
|
||||
},
|
||||
onError: error => {
|
||||
showError('Backup failed: ' + (error as any).message);
|
||||
},
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'BACKUP');
|
||||
backupTypesList.forEach(value => formData.append('backup_types', value));
|
||||
|
||||
const response = await fetch('/api/v1/admin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Backup failed: ' + response.statusText);
|
||||
}
|
||||
);
|
||||
|
||||
const filename = `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`;
|
||||
|
||||
// Stream the response directly to disk using File System Access API
|
||||
// This avoids loading multi-GB files into browser memory
|
||||
if (typeof (window as any).showSaveFilePicker === 'function') {
|
||||
try {
|
||||
// Modern browsers: Use File System Access API for direct disk writes
|
||||
const handle = await (window as any).showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }],
|
||||
});
|
||||
|
||||
const writable = await handle.createWritable();
|
||||
|
||||
// Stream response body directly to file without buffering
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('Unable to read response');
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
await writable.write(value);
|
||||
}
|
||||
|
||||
await writable.close();
|
||||
showInfo('Backup completed successfully');
|
||||
} catch (err) {
|
||||
// User cancelled or error
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
showError('Backup failed: ' + (err as Error).message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
showError(
|
||||
'Your browser does not support large file downloads. Please use Chrome, Edge, or Safari.'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Backup failed: ' + (error as any).message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!restoreFile) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('restore_file', restoreFile);
|
||||
formData.append('action', 'RESTORE');
|
||||
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: formData as any,
|
||||
data: {
|
||||
action: 'RESTORE',
|
||||
restore_file: restoreFile,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -76,9 +76,9 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
||||
<Activity size={20} />
|
||||
</Link>
|
||||
{doc.filepath ? (
|
||||
<Link to={`/documents/${doc.id}/file`}>
|
||||
<a href={`/api/v1/documents/${doc.id}/file`}>
|
||||
<Download size={20} />
|
||||
</Link>
|
||||
</a>
|
||||
) : (
|
||||
<Download size={20} className="text-gray-400" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user