mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
Demo mode
This commit is contained in:
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/LogicLabs-OU/OpenArchiver/tree/main/.github/CLA.md'
|
||||
branch: 'main'
|
||||
allowlist: ''
|
||||
allowlist: 'wayneshn'
|
||||
|
||||
remote-organization-name: 'LogicLabs-OU'
|
||||
remote-repository-name: 'cla-db'
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { IngestionService } from '../../services/IngestionService';
|
||||
import { CreateIngestionSourceDto, UpdateIngestionSourceDto } from '@open-archiver/types';
|
||||
import {
|
||||
CreateIngestionSourceDto,
|
||||
UpdateIngestionSourceDto,
|
||||
IngestionSource,
|
||||
SafeIngestionSource
|
||||
} from '@open-archiver/types';
|
||||
import { logger } from '../../config/logger';
|
||||
import { config } from '../../config';
|
||||
|
||||
export class IngestionController {
|
||||
/**
|
||||
* Converts an IngestionSource object to a safe version for client-side consumption
|
||||
* by removing the credentials.
|
||||
* @param source The full IngestionSource object.
|
||||
* @returns An object conforming to the SafeIngestionSource type.
|
||||
*/
|
||||
private toSafeIngestionSource(source: IngestionSource): SafeIngestionSource {
|
||||
const { credentials, ...safeSource } = source;
|
||||
return safeSource;
|
||||
}
|
||||
|
||||
public create = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const dto: CreateIngestionSourceDto = req.body;
|
||||
const newSource = await IngestionService.create(dto);
|
||||
return res.status(201).json(newSource);
|
||||
const safeSource = this.toSafeIngestionSource(newSource);
|
||||
return res.status(201).json(safeSource);
|
||||
} catch (error: any) {
|
||||
logger.error({ err: error }, 'Create ingestion source error');
|
||||
// Return a 400 Bad Request for connection errors
|
||||
@@ -19,7 +40,8 @@ export class IngestionController {
|
||||
public findAll = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const sources = await IngestionService.findAll();
|
||||
return res.status(200).json(sources);
|
||||
const safeSources = sources.map(this.toSafeIngestionSource);
|
||||
return res.status(200).json(safeSources);
|
||||
} catch (error) {
|
||||
console.error('Find all ingestion sources error:', error);
|
||||
return res.status(500).json({ message: 'An internal server error occurred' });
|
||||
@@ -30,7 +52,8 @@ export class IngestionController {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const source = await IngestionService.findById(id);
|
||||
return res.status(200).json(source);
|
||||
const safeSource = this.toSafeIngestionSource(source);
|
||||
return res.status(200).json(safeSource);
|
||||
} catch (error) {
|
||||
console.error(`Find ingestion source by id ${req.params.id} error:`, error);
|
||||
if (error instanceof Error && error.message === 'Ingestion source not found') {
|
||||
@@ -41,11 +64,15 @@ export class IngestionController {
|
||||
};
|
||||
|
||||
public update = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const dto: UpdateIngestionSourceDto = req.body;
|
||||
const updatedSource = await IngestionService.update(id, dto);
|
||||
return res.status(200).json(updatedSource);
|
||||
const safeSource = this.toSafeIngestionSource(updatedSource);
|
||||
return res.status(200).json(safeSource);
|
||||
} catch (error) {
|
||||
console.error(`Update ingestion source ${req.params.id} error:`, error);
|
||||
if (error instanceof Error && error.message === 'Ingestion source not found') {
|
||||
@@ -56,6 +83,9 @@ export class IngestionController {
|
||||
};
|
||||
|
||||
public delete = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await IngestionService.delete(id);
|
||||
@@ -70,6 +100,9 @@ export class IngestionController {
|
||||
};
|
||||
|
||||
public triggerInitialImport = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await IngestionService.triggerInitialImport(id);
|
||||
@@ -84,10 +117,14 @@ export class IngestionController {
|
||||
};
|
||||
|
||||
public pause = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updatedSource = await IngestionService.update(id, { status: 'paused' });
|
||||
return res.status(200).json(updatedSource);
|
||||
const safeSource = this.toSafeIngestionSource(updatedSource);
|
||||
return res.status(200).json(safeSource);
|
||||
} catch (error) {
|
||||
console.error(`Pause ingestion source ${req.params.id} error:`, error);
|
||||
if (error instanceof Error && error.message === 'Ingestion source not found') {
|
||||
@@ -98,6 +135,9 @@ export class IngestionController {
|
||||
};
|
||||
|
||||
public triggerForceSync = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await IngestionService.triggerForceSync(id);
|
||||
|
||||
@@ -4,4 +4,5 @@ export const app = {
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND, 10) : 4000,
|
||||
encryptionKey: process.env.ENCRYPTION_KEY,
|
||||
isDemo: process.env.IS_DEMO === 'true',
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import 'dotenv/config';
|
||||
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
return {
|
||||
user: locals.user,
|
||||
accessToken: locals.accessToken
|
||||
accessToken: locals.accessToken,
|
||||
isDemo: process.env.IS_DEMO === 'true'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
};
|
||||
|
||||
const openEditDialog = (source: IngestionSource) => {
|
||||
if (data.isDemo) {
|
||||
setAlert({
|
||||
type: 'warning',
|
||||
title: 'Demo mode',
|
||||
message: 'Editing is now allowed in demo mode.',
|
||||
duration: 5000,
|
||||
show: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
selectedSource = source;
|
||||
isDialogOpen = true;
|
||||
};
|
||||
@@ -40,7 +50,18 @@
|
||||
if (!sourceToDelete) return;
|
||||
isDeleting = true;
|
||||
try {
|
||||
await api(`/ingestion-sources/${sourceToDelete.id}`, { method: 'DELETE' });
|
||||
const res = await api(`/ingestion-sources/${sourceToDelete.id}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.json();
|
||||
setAlert({
|
||||
type: 'error',
|
||||
title: 'Failed to delete ingestion',
|
||||
message: errorBody.message || JSON.stringify(errorBody),
|
||||
duration: 5000,
|
||||
show: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
ingestionSources = ingestionSources.filter((s) => s.id !== sourceToDelete!.id);
|
||||
isDeleteDialogOpen = false;
|
||||
sourceToDelete = null;
|
||||
@@ -50,7 +71,18 @@
|
||||
};
|
||||
|
||||
const handleSync = async (id: string) => {
|
||||
await api(`/ingestion-sources/${id}/sync`, { method: 'POST' });
|
||||
const res = await api(`/ingestion-sources/${id}/sync`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.json();
|
||||
setAlert({
|
||||
type: 'error',
|
||||
title: 'Failed to trigger force sync ingestion',
|
||||
message: errorBody.message || JSON.stringify(errorBody),
|
||||
duration: 5000,
|
||||
show: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
const updatedSources = ingestionSources.map((s) => {
|
||||
if (s.id === id) {
|
||||
return { ...s, status: 'syncing' as const };
|
||||
@@ -61,24 +93,36 @@
|
||||
};
|
||||
|
||||
const handleToggle = async (source: IngestionSource) => {
|
||||
const isPaused = source.status === 'paused';
|
||||
const newStatus = isPaused ? 'active' : 'paused';
|
||||
try {
|
||||
const isPaused = source.status === 'paused';
|
||||
const newStatus = isPaused ? 'active' : 'paused';
|
||||
if (data.isDemo) {
|
||||
throw Error('This operation is not allowed in demo mode.');
|
||||
}
|
||||
if (newStatus === 'paused') {
|
||||
await api(`/ingestion-sources/${source.id}/pause`, { method: 'POST' });
|
||||
} else {
|
||||
await api(`/ingestion-sources/${source.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'active' })
|
||||
});
|
||||
}
|
||||
|
||||
if (newStatus === 'paused') {
|
||||
await api(`/ingestion-sources/${source.id}/pause`, { method: 'POST' });
|
||||
} else {
|
||||
await api(`/ingestion-sources/${source.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'active' })
|
||||
ingestionSources = ingestionSources.map((s) => {
|
||||
if (s.id === source.id) {
|
||||
return { ...s, status: newStatus };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
} catch (e) {
|
||||
setAlert({
|
||||
type: 'error',
|
||||
title: 'Failed to trigger force sync ingestion',
|
||||
message: e instanceof Error ? e.message : JSON.stringify(e),
|
||||
duration: 5000,
|
||||
show: true
|
||||
});
|
||||
}
|
||||
|
||||
ingestionSources = ingestionSources.map((s) => {
|
||||
if (s.id === source.id) {
|
||||
return { ...s, status: newStatus };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: CreateIngestionSourceDto) => {
|
||||
@@ -155,7 +199,7 @@
|
||||
<div class="">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">Ingestion Sources</h1>
|
||||
<Button onclick={openCreateDialog}>Create New</Button>
|
||||
<Button onclick={openCreateDialog} disabled={data.isDemo}>Create New</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
|
||||
@@ -81,6 +81,13 @@ export interface IngestionSource {
|
||||
syncState?: SyncState | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an ingestion source with sensitive credential information removed.
|
||||
* This type is safe to use in client-side applications or API responses
|
||||
* where exposing credentials would be a security risk.
|
||||
*/
|
||||
export type SafeIngestionSource = Omit<IngestionSource, 'credentials'>;
|
||||
|
||||
export interface CreateIngestionSourceDto {
|
||||
name: string;
|
||||
provider: IngestionProvider;
|
||||
@@ -121,4 +128,4 @@ export type MailboxUser = {
|
||||
export type ProcessMailboxError = {
|
||||
error: boolean;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user