Demo mode

This commit is contained in:
Wayne
2025-08-04 14:42:06 +03:00
parent 4156abcdfa
commit 5a2ca3bf19
6 changed files with 122 additions and 27 deletions

View File

@@ -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'

View File

@@ -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);

View File

@@ -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',
};

View File

@@ -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'
};
};

View File

@@ -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">

View File

@@ -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;
};
};