open-core setup, adding enterprise package

This commit is contained in:
Wayne
2025-09-28 23:29:46 +02:00
parent 1e048fdbc1
commit d20fe8badb
36 changed files with 660 additions and 167 deletions

7
LICENSE.txt Normal file
View File

@@ -0,0 +1,7 @@
Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the Open Archiver Commercial License.
* Outside of the top-level "packages/enterprise" directories, source code in a given file is licensed under the AGPL. The full text of this license can be found in the file LICENSE-AGPL.txt.
* Within the top-level "packages/enterprise" directories, source code in a given file is licensed under the Open Archiver Commercial License, unless otherwise noted.
When built, binary files are generated for the AGPL source code and the Open Archiver Commercial License source code. Binaries located at hub.docker.com/logiclabs/open-archiver with an "-enterprise" tag are released under the Open Archiver Commercial License. All other binaries are released under the AGPL.

View File

@@ -0,0 +1,59 @@
# Dockerfile for the Enterprise version of Open Archiver
ARG BASE_IMAGE=node:22-alpine
# 0. Base Stage: Define all common dependencies and setup
FROM ${BASE_IMAGE} AS base
WORKDIR /app
# Install pnpm
RUN --mount=type=cache,target=/root/.npm \
npm install -g pnpm
# Copy manifests and lockfile
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY packages/backend/package.json ./packages/backend/
COPY packages/frontend/package.json ./packages/frontend/
COPY packages/types/package.json ./packages/types/
COPY packages/enterprise/package.json ./packages/enterprise/
COPY packages/frontend-enterprise/package.json ./packages/frontend-enterprise/
COPY apps/open-archiver-enterprise/package.json ./apps/open-archiver-enterprise/
# 1. Build Stage: Install all dependencies and build the project
FROM base AS build
COPY packages/frontend/svelte.config.js ./packages/frontend/
# Install all dependencies.
ENV PNPM_HOME="/pnpm"
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prod=false
# Copy the rest of the source code
COPY . .
# Build the Enterprise packages.
RUN pnpm build:enterprise
# 2. Production Stage: Install only production dependencies and copy built artifacts
FROM base AS production
# Copy built application from build stage
COPY --from=build /app/packages/backend/dist ./packages/backend/dist
COPY --from=build /app/packages/frontend/build ./packages/frontend/build
COPY --from=build /app/packages/types/dist ./packages/types/dist
COPY --from=build /app/packages/enterprise/dist ./packages/enterprise/dist
COPY --from=build /app/packages/frontend-enterprise/dist ./packages/frontend-enterprise/dist
COPY --from=build /app/apps/open-archiver-enterprise/dist ./apps/open-archiver-enterprise/dist
# Copy the entrypoint script and make it executable
COPY docker/docker-entrypoint.sh /usr/local/bin/
# Expose the port the app runs on
EXPOSE 4000
EXPOSE 3000
# Set the entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
# Start the application
CMD ["pnpm", "start:enterprise"]

View File

@@ -0,0 +1,28 @@
import { createServer, logger } from '@open-archiver/backend';
import { enterpriseModules } from '@open-archiver/enterprise';
import * as dotenv from 'dotenv';
dotenv.config();
async function start() {
// --- Environment Variable Validation ---
const { PORT_BACKEND } = process.env;
if (!PORT_BACKEND) {
throw new Error(
'Missing required environment variables for the backend: PORT_BACKEND.'
);
}
// Create the server instance (passing no modules for the default OSS version)
const app = await createServer(enterpriseModules);
app.listen(PORT_BACKEND, () => {
logger.info({}, `🏢 Open Archiver (Enterprise) running on port ${PORT_BACKEND}`);
});
}
start().catch(error => {
logger.error({ error }, 'Failed to start the server:', error);
process.exit(1);
});

View File

@@ -0,0 +1,19 @@
{
"name": "open-archiver-enterprise-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@open-archiver/backend": "workspace:*",
"@open-archiver/enterprise": "workspace:*",
"dotenv": "^17.2.0"
},
"devDependencies": {
"@types/dotenv": "^8.2.3",
"ts-node-dev": "^2.0.0"
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["./**/*.ts"],
"references": [{ "path": "../../packages/backend" }, { "path": "../../packages/enterprise" }]
}

View File

@@ -0,0 +1,55 @@
# Dockerfile for the OSS version of Open Archiver
ARG BASE_IMAGE=node:22-alpine
# 0. Base Stage: Define all common dependencies and setup
FROM ${BASE_IMAGE} AS base
WORKDIR /app
# Install pnpm
RUN --mount=type=cache,target=/root/.npm \
npm install -g pnpm
# Copy manifests and lockfile
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY packages/backend/package.json ./packages/backend/
COPY packages/frontend/package.json ./packages/frontend/
COPY packages/types/package.json ./packages/types/
COPY apps/open-archiver/package.json ./apps/open-archiver/
# 1. Build Stage: Install all dependencies and build the project
FROM base AS build
COPY packages/frontend/svelte.config.js ./packages/frontend/
# Install all dependencies.
ENV PNPM_HOME="/pnpm"
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prod=false
# Copy the rest of the source code
COPY . .
# Build the OSS packages.
RUN pnpm build:oss
# 2. Production Stage: Install only production dependencies and copy built artifacts
FROM base AS production
# Copy built application from build stage
COPY --from=build /app/packages/backend/dist ./packages/backend/dist
COPY --from=build /app/packages/frontend/build ./packages/frontend/build
COPY --from=build /app/packages/types/dist ./packages/types/dist
COPY --from=build /app/apps/open-archiver/dist ./apps/open-archiver/dist
# Copy the entrypoint script and make it executable
COPY docker/docker-entrypoint.sh /usr/local/bin/
# Expose the port the app runs on
EXPOSE 4000
EXPOSE 3000
# Set the entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
# Start the application
CMD ["pnpm", "start:oss"]

View File

@@ -0,0 +1,26 @@
import { createServer, logger } from '@open-archiver/backend';
import * as dotenv from 'dotenv';
dotenv.config();
async function start() {
// --- Environment Variable Validation ---
const { PORT_BACKEND } = process.env;
if (!PORT_BACKEND) {
throw new Error(
'Missing required environment variables for the backend: PORT_BACKEND.'
);
}
// Create the server instance (passing no modules for the default OSS version)
const app = await createServer([]);
app.listen(PORT_BACKEND, () => {
logger.info({}, `✅ Open Archiver (OSS) running on port ${PORT_BACKEND}`);
});
}
start().catch(error => {
logger.error({ error }, 'Failed to start the server:', error);
process.exit(1);
});

View File

@@ -0,0 +1,18 @@
{
"name": "open-archiver-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@open-archiver/backend": "workspace:*",
"dotenv": "^17.2.0"
},
"devDependencies": {
"@types/dotenv": "^8.2.3",
"ts-node-dev": "^2.0.0"
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["./**/*.ts"],
"references": [{ "path": "../../packages/backend" }]
}

View File

@@ -0,0 +1,18 @@
version: '3.8'
services:
open-archiver:
build:
context: .
dockerfile: apps/open-archiver-enterprise/Dockerfile
args:
VITE_ENTERPRISE_MODE: 'true'
image: logiclabshq/open-archiver:enterprise-latest
ports:
- '3000:3000' # Frontend
networks:
- open-archiver-net
networks:
open-archiver-net:
external: true

View File

@@ -2,10 +2,14 @@
"name": "open-archiver",
"version": "0.3.4",
"private": true,
"license": "SEE LICENSE IN LICENSE-AGPL.txt",
"scripts": {
"dev": "dotenv -- pnpm --filter \"./packages/*\" --parallel dev",
"build": "pnpm --filter \"./packages/*\" build",
"start": "dotenv -- pnpm --filter \"./packages/*\" --parallel start",
"build:oss": "pnpm --filter \"./packages/*\" --filter \"!./packages/enterprise\" --filter \"./apps/open-archiver\" build",
"build:enterprise": "cross-env VITE_ENTERPRISE_MODE=true pnpm build",
"start:oss": "dotenv -- concurrently \"node apps/open-archiver/dist/index.js\" \"pnpm --filter @open-archiver/frontend start\"",
"start:enterprise": "dotenv -- concurrently \"node apps/open-archiver-enterprise/dist/index.js\" \"pnpm --filter @open-archiver/frontend start\"",
"dev:enterprise": "cross-env VITE_ENTERPRISE_MODE=true dotenv -- pnpm --filter \"@open-archiver/frontend\" --filter \"open-archiver-enterprise-app\" --parallel dev",
"dev:oss": "dotenv -- pnpm --filter \"@open-archiver/frontend\" --filter \"open-archiver-app\" --parallel dev",
"start:workers": "dotenv -- concurrently \"pnpm --filter @open-archiver/backend start:ingestion-worker\" \"pnpm --filter @open-archiver/backend start:indexing-worker\" \"pnpm --filter @open-archiver/backend start:sync-scheduler\"",
"start:workers:dev": "dotenv -- concurrently \"pnpm --filter @open-archiver/backend start:ingestion-worker:dev\" \"pnpm --filter @open-archiver/backend start:indexing-worker:dev\" \"pnpm --filter @open-archiver/backend start:sync-scheduler:dev\"",
"db:generate": "dotenv -- pnpm --filter @open-archiver/backend db:generate",
@@ -23,6 +27,7 @@
"dotenv-cli": "8.0.0"
},
"devDependencies": {
"cross-env": "^10.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",

View File

@@ -2,12 +2,12 @@
"name": "@open-archiver/backend",
"version": "0.1.0",
"private": true,
"license": "SEE LICENSE IN LICENSE-AGPL.txt",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts ",
"build": "tsc && pnpm copy-assets",
"copy-assets": "cp -r src/locales dist/locales",
"start": "node dist/index.js",
"start:ingestion-worker": "node dist/workers/ingestion.worker.js",
"start:indexing-worker": "node dist/workers/indexing.worker.js",
"start:sync-scheduler": "node dist/jobs/schedulers/sync-scheduler.js",

View File

@@ -3,7 +3,7 @@ import { ApiKeyController } from '../controllers/api-key.controller';
import { requireAuth } from '../middleware/requireAuth';
import { AuthService } from '../../services/AuthService';
export const apiKeyRoutes = (authService: AuthService) => {
export const apiKeyRoutes = (authService: AuthService): Router => {
const router = Router();
const controller = new ApiKeyController();

View File

@@ -0,0 +1,150 @@
import express, { Express } from 'express';
import dotenv from 'dotenv';
import { AuthController } from './controllers/auth.controller';
import { IngestionController } from './controllers/ingestion.controller';
import { ArchivedEmailController } from './controllers/archived-email.controller';
import { StorageController } from './controllers/storage.controller';
import { SearchController } from './controllers/search.controller';
import { IamController } from './controllers/iam.controller';
import { requireAuth } from './middleware/requireAuth';
import { createAuthRouter } from './routes/auth.routes';
import { createIamRouter } from './routes/iam.routes';
import { createIngestionRouter } from './routes/ingestion.routes';
import { createArchivedEmailRouter } from './routes/archived-email.routes';
import { createStorageRouter } from './routes/storage.routes';
import { createSearchRouter } from './routes/search.routes';
import { createDashboardRouter } from './routes/dashboard.routes';
import { createUploadRouter } from './routes/upload.routes';
import { createUserRouter } from './routes/user.routes';
import { createSettingsRouter } from './routes/settings.routes';
import { apiKeyRoutes } from './routes/api-key.routes';
import { AuthService } from '../services/AuthService';
import { UserService } from '../services/UserService';
import { IamService } from '../services/IamService';
import { StorageService } from '../services/StorageService';
import { SearchService } from '../services/SearchService';
import { SettingsService } from '../services/SettingsService';
import i18next from 'i18next';
import FsBackend from 'i18next-fs-backend';
import i18nextMiddleware from 'i18next-http-middleware';
import path from 'path';
import { logger } from '../config/logger';
import { rateLimiter } from './middleware/rateLimiter';
import { config } from '../config';
// Define the "plugin" interface
export interface ArchiverModule {
initialize: (app: Express) => Promise<void>;
name: string;
}
export async function createServer(modules: ArchiverModule[] = []): Promise<Express> {
// Load environment variables
dotenv.config();
// --- Environment Variable Validation ---
const { JWT_SECRET, JWT_EXPIRES_IN } = process.env;
if (!JWT_SECRET || !JWT_EXPIRES_IN) {
throw new Error(
'Missing required environment variables for the backend: JWT_SECRET, JWT_EXPIRES_IN.'
);
}
// --- Dependency Injection Setup ---
const userService = new UserService();
const authService = new AuthService(userService, JWT_SECRET, JWT_EXPIRES_IN);
const authController = new AuthController(authService, userService);
const ingestionController = new IngestionController();
const archivedEmailController = new ArchivedEmailController();
const storageService = new StorageService();
const storageController = new StorageController(storageService);
const searchService = new SearchService();
const searchController = new SearchController();
const iamService = new IamService();
const iamController = new IamController(iamService);
const settingsService = new SettingsService();
// --- i18next Initialization ---
const initializeI18next = async () => {
const systemSettings = await settingsService.getSystemSettings();
const defaultLanguage = systemSettings?.language || 'en';
logger.info({ language: defaultLanguage }, 'Default language');
await i18next.use(FsBackend).init({
lng: defaultLanguage,
fallbackLng: defaultLanguage,
ns: ['translation'],
defaultNS: 'translation',
backend: {
loadPath: path.resolve(__dirname, '../locales/{{lng}}/{{ns}}.json'),
},
});
};
// Initialize i18next
await initializeI18next();
logger.info({}, 'i18next initialized');
// Configure the Meilisearch index on startup
logger.info({}, 'Configuring email index...');
await searchService.configureEmailIndex();
const app = express();
// --- Routes ---
const authRouter = createAuthRouter(authController);
const ingestionRouter = createIngestionRouter(ingestionController, authService);
const archivedEmailRouter = createArchivedEmailRouter(archivedEmailController, authService);
const storageRouter = createStorageRouter(storageController, authService);
const searchRouter = createSearchRouter(searchController, authService);
const dashboardRouter = createDashboardRouter(authService);
const iamRouter = createIamRouter(iamController, authService);
const uploadRouter = createUploadRouter(authService);
const userRouter = createUserRouter(authService);
const settingsRouter = createSettingsRouter(authService);
const apiKeyRouter = apiKeyRoutes(authService);
// upload route is added before middleware because it doesn't use the json middleware.
app.use(`/${config.api.version}/upload`, uploadRouter);
// Middleware for all other routes
app.use((req, res, next) => {
// exclude certain API endpoints from the rate limiter, for example status, system settings
const excludedPatterns = [/^\/v\d+\/auth\/status$/, /^\/v\d+\/settings\/system$/];
for (const pattern of excludedPatterns) {
if (pattern.test(req.path)) {
return next();
}
}
rateLimiter(req, res, next);
});
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// i18n middleware
app.use(i18nextMiddleware.handle(i18next));
app.use(`/${config.api.version}/auth`, authRouter);
app.use(`/${config.api.version}/iam`, iamRouter);
app.use(`/${config.api.version}/ingestion-sources`, ingestionRouter);
app.use(`/${config.api.version}/archived-emails`, archivedEmailRouter);
app.use(`/${config.api.version}/storage`, storageRouter);
app.use(`/${config.api.version}/search`, searchRouter);
app.use(`/${config.api.version}/dashboard`, dashboardRouter);
app.use(`/${config.api.version}/users`, userRouter);
// Load all provided extension modules
for (const module of modules) {
await module.initialize(app);
console.log(`🏢 Enterprise module loaded: ${module.name}`);
}
app.use(`/${config.api.version}/settings`, settingsRouter);
app.use(`/${config.api.version}/api-keys`, apiKeyRouter);
app.get('/', (req, res) => {
res.send('Backend is running!');
});
console.log('✅ Core OSS modules loaded.');
return app;
}

View File

@@ -9,4 +9,5 @@ export const apiConfig = {
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10)
: 100, // limit each IP to 100 requests per windowMs
},
version: 'v1'
};

View File

@@ -1,155 +1,3 @@
import express from 'express';
import dotenv from 'dotenv';
import { AuthController } from './api/controllers/auth.controller';
import { IngestionController } from './api/controllers/ingestion.controller';
import { ArchivedEmailController } from './api/controllers/archived-email.controller';
import { StorageController } from './api/controllers/storage.controller';
import { SearchController } from './api/controllers/search.controller';
import { IamController } from './api/controllers/iam.controller';
import { requireAuth } from './api/middleware/requireAuth';
import { createAuthRouter } from './api/routes/auth.routes';
import { createIamRouter } from './api/routes/iam.routes';
import { createIngestionRouter } from './api/routes/ingestion.routes';
import { createArchivedEmailRouter } from './api/routes/archived-email.routes';
import { createStorageRouter } from './api/routes/storage.routes';
import { createSearchRouter } from './api/routes/search.routes';
import { createDashboardRouter } from './api/routes/dashboard.routes';
import { createUploadRouter } from './api/routes/upload.routes';
import { createUserRouter } from './api/routes/user.routes';
import { createSettingsRouter } from './api/routes/settings.routes';
import { apiKeyRoutes } from './api/routes/api-key.routes';
import { AuthService } from './services/AuthService';
import { UserService } from './services/UserService';
import { IamService } from './services/IamService';
import { StorageService } from './services/StorageService';
import { SearchService } from './services/SearchService';
import { SettingsService } from './services/SettingsService';
import i18next from 'i18next';
import FsBackend from 'i18next-fs-backend';
import i18nextMiddleware from 'i18next-http-middleware';
import path from 'path';
import { logger } from './config/logger';
import { rateLimiter } from './api/middleware/rateLimiter';
// Load environment variables
dotenv.config();
// --- Environment Variable Validation ---
const { PORT_BACKEND, JWT_SECRET, JWT_EXPIRES_IN } = process.env;
if (!PORT_BACKEND || !JWT_SECRET || !JWT_EXPIRES_IN) {
throw new Error(
'Missing required environment variables for the backend: PORT_BACKEND, JWT_SECRET, JWT_EXPIRES_IN.'
);
}
// --- i18next Initialization ---
const initializeI18next = async () => {
const systemSettings = await settingsService.getSystemSettings();
const defaultLanguage = systemSettings?.language || 'en';
logger.info({ language: defaultLanguage }, 'Default language');
await i18next.use(FsBackend).init({
lng: defaultLanguage,
fallbackLng: defaultLanguage,
ns: ['translation'],
defaultNS: 'translation',
backend: {
loadPath: path.resolve(__dirname, './locales/{{lng}}/{{ns}}.json'),
},
});
};
// --- Dependency Injection Setup ---
const userService = new UserService();
const authService = new AuthService(userService, JWT_SECRET, JWT_EXPIRES_IN);
const authController = new AuthController(authService, userService);
const ingestionController = new IngestionController();
const archivedEmailController = new ArchivedEmailController();
const storageService = new StorageService();
const storageController = new StorageController(storageService);
const searchService = new SearchService();
const searchController = new SearchController();
const iamService = new IamService();
const iamController = new IamController(iamService);
const settingsService = new SettingsService();
// --- Express App Initialization ---
const app = express();
// --- Routes ---
const authRouter = createAuthRouter(authController);
const ingestionRouter = createIngestionRouter(ingestionController, authService);
const archivedEmailRouter = createArchivedEmailRouter(archivedEmailController, authService);
const storageRouter = createStorageRouter(storageController, authService);
const searchRouter = createSearchRouter(searchController, authService);
const dashboardRouter = createDashboardRouter(authService);
const iamRouter = createIamRouter(iamController, authService);
const uploadRouter = createUploadRouter(authService);
const userRouter = createUserRouter(authService);
const settingsRouter = createSettingsRouter(authService);
const apiKeyRouter = apiKeyRoutes(authService);
// upload route is added before middleware because it doesn't use the json middleware.
app.use('/v1/upload', uploadRouter);
// Middleware for all other routes
app.use((req, res, next) => {
// exclude certain API endpoints from the rate limiter, for example status, system settings
const excludedPatterns = [/^\/v\d+\/auth\/status$/, /^\/v\d+\/settings\/system$/];
for (const pattern of excludedPatterns) {
if (pattern.test(req.path)) {
return next();
}
}
rateLimiter(req, res, next);
});
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// i18n middleware
app.use(i18nextMiddleware.handle(i18next));
app.use('/v1/auth', authRouter);
app.use('/v1/iam', iamRouter);
app.use('/v1/ingestion-sources', ingestionRouter);
app.use('/v1/archived-emails', archivedEmailRouter);
app.use('/v1/storage', storageRouter);
app.use('/v1/search', searchRouter);
app.use('/v1/dashboard', dashboardRouter);
app.use('/v1/users', userRouter);
app.use('/v1/settings', settingsRouter);
app.use('/v1/api-keys', apiKeyRouter);
// Example of a protected route
app.get('/v1/protected', requireAuth(authService), (req, res) => {
res.json({
message: 'You have accessed a protected route!',
user: req.user, // The user payload is attached by the requireAuth middleware
});
});
app.get('/', (req, res) => {
res.send('Backend is running!');
});
// --- Server Start ---
const startServer = async () => {
try {
// Initialize i18next
await initializeI18next();
logger.info({}, 'i18next initialized');
// Configure the Meilisearch index on startup
logger.info({}, 'Configuring email index...');
await searchService.configureEmailIndex();
app.listen(PORT_BACKEND, () => {
logger.info({}, `Backend listening at http://localhost:${PORT_BACKEND}`);
});
} catch (error) {
logger.error({ error }, 'Failed to start the server:', error);
process.exit(1);
}
};
startServer();
export { createServer, ArchiverModule } from './api/server';
export { logger } from './config/logger';
export { config } from './config';

View File

@@ -4,7 +4,8 @@
"outDir": "./dist",
"rootDir": "./src",
"emitDecoratorMetadata": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
{
"name": "@open-archiver/enterprise",
"version": "1.0.0",
"private": true,
"license": "SEE LICENSE IN LICENSE.txt",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc && pnpm copy-assets",
"copy-assets": "mkdir -p dist/modules/license && cp src/modules/license/public-key.pem dist/modules/license/public-key.pem"
},
"dependencies": {
"@open-archiver/backend": "workspace:*",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10"
}
}

View File

@@ -0,0 +1,8 @@
import { ArchiverModule } from '@open-archiver/backend';
import { statusModule } from './modules/status/status.module';
import { retentionPolicyModule } from './modules/retention-policy/retention-policy.module';
export const enterpriseModules: ArchiverModule[] = [
statusModule,
retentionPolicyModule,
];

View File

@@ -0,0 +1,13 @@
import * as fs from 'fs';
import * as path from 'path';
// This is a placeholder for the LicenseService. In a real implementation, this service would handle license validation. Currently all validations returns true.
export class LicenseService {
private publicKey = fs.readFileSync(path.resolve(__dirname, './public-key.pem'), 'utf-8');
public isFeatureEnabled(feature: string): boolean {
// For now, all features are enabled.
console.log(`Checking feature: ${feature}`);
return true;
}
}

View File

@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEC6I4ubMlqW28CjS4IbFnCZ8fE+etwF50
h2KM3yh6acvko/k7234dCbJ/rwJZFq7DUlG4DABMnTg/1OwbjGZiMQ==
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,14 @@
import { Express, Request, Response } from 'express';
import { ArchiverModule, config } from '@open-archiver/backend';
class RetentionPolicyModule implements ArchiverModule {
name = 'retention-policy';
async initialize(app: Express): Promise<void> {
app.get(`/${config.api.version}/enterprise/retention-policy`, (req: Request, res: Response) => {
res.json({ status: 'Retention Policy module is active!' });
});
}
}
export const retentionPolicyModule = new RetentionPolicyModule();

View File

@@ -0,0 +1,21 @@
import { Express, Request, Response } from 'express';
import { ArchiverModule, config } from '@open-archiver/backend';
import { LicenseService } from '../license/LicenseService';
class StatusModule implements ArchiverModule {
name = 'Status';
async initialize(app: Express): Promise<void> {
const licenseService = new LicenseService();
app.get(`/${config.api.version}/enterprise/status`, (req: Request, res: Response) => {
if (licenseService.isFeatureEnabled('enterprise-status')) {
res.json({ status: 'Enterprise features are enabled!' });
} else {
res.status(403).json({ error: 'Enterprise license required.' });
}
});
}
}
export const statusModule = new StatusModule();

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"composite": true,
"declaration": true
},
"include": ["src/**/*.ts"],
"references": [{ "path": "../../packages/backend" }]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
{
"name": "@open-archiver/frontend",
"private": true,
"license": "SEE LICENSE IN LICENSE-AGPL.txt",
"version": "0.0.1",
"type": "module",
"scripts": {

View File

@@ -6,14 +6,17 @@
import { page } from '$app/state';
import ThemeSwitcher from '$lib/components/custom/ThemeSwitcher.svelte';
import { t } from '$lib/translations';
const navItems: {
interface NavItem {
href?: string;
label: string;
subMenu?: {
href: string;
label: string;
}[];
}[] = [
}
const baseNavItems: NavItem[] = [
{ href: '/dashboard', label: $t('app.layout.dashboard') },
{ href: '/dashboard/ingestions', label: $t('app.layout.ingestions') },
{ href: '/dashboard/archived-emails', label: $t('app.layout.archived_emails') },
@@ -40,6 +43,15 @@
],
},
];
const enterpriseNavItems: NavItem[] = [
{ href: '/dashboard/compliance-center', label: 'Compliance Center' },
];
let navItems: NavItem[] = $state(baseNavItems);
if (import.meta.env.VITE_ENTERPRISE_MODE) {
navItems = [...baseNavItems, ...enterpriseNavItems];
}
let { children } = $props();
function handleLogout() {
authStore.logout();

View File

@@ -0,0 +1,18 @@
import { api } from '$lib/server/api';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
if (import.meta.env.VITE_ENTERPRISE_MODE) {
try {
const response = await api('/enterprise/status', event);
const data = await response.json();
return {
status: data
};
} catch (err) {
console.log(err)
throw error(500, 'Failed to fetch enterprise status.');
}
}
};

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
{#if import.meta.env.VITE_ENTERPRISE_MODE}
<h1 class="text-2xl font-bold">Compliance Center (Enterprise)</h1>
<p class="mt-4">This is a placeholder page for the Compliance Center.</p>
{#if data && data.status}
<pre class="mt-4">{JSON.stringify(data.status, null, 2)}</pre>
{/if}
{:else}
<div class="text-center">
<h1 class="text-2xl font-bold">Access Denied</h1>
<p class="mt-4">This feature is only available in the Enterprise Edition.</p>
</div>
{/if}

View File

@@ -7,6 +7,10 @@ dotenv.config();
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
define: {
// This will be 'true' only during the enterprise build process
'import.meta.env.VITE_ENTERPRISE_MODE': process.env.VITE_ENTERPRISE_MODE === 'true'
},
server: {
port: Number(process.env.PORT_FRONTEND) || 3000,
proxy: {

View File

@@ -2,6 +2,7 @@
"name": "@open-archiver/types",
"version": "0.1.0",
"private": true,
"license": "SEE LICENSE IN LICENSE-AGPL.txt",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {

94
pnpm-lock.yaml generated
View File

@@ -15,6 +15,9 @@ importers:
specifier: 8.0.0
version: 8.0.0
devDependencies:
cross-env:
specifier: ^10.0.0
version: 10.0.0
prettier:
specifier: ^3.6.2
version: 3.6.2
@@ -31,6 +34,41 @@ importers:
specifier: ^1.6.4
version: 1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3)
apps/open-archiver:
dependencies:
'@open-archiver/backend':
specifier: workspace:*
version: link:../../packages/backend
dotenv:
specifier: ^17.2.0
version: 17.2.0
devDependencies:
'@types/dotenv':
specifier: ^8.2.3
version: 8.2.3
ts-node-dev:
specifier: ^2.0.0
version: 2.0.0(@types/node@24.0.13)(typescript@5.8.3)
apps/open-archiver-enterprise:
dependencies:
'@open-archiver/backend':
specifier: workspace:*
version: link:../../packages/backend
'@open-archiver/enterprise':
specifier: workspace:*
version: link:../../packages/enterprise
dotenv:
specifier: ^17.2.0
version: 17.2.0
devDependencies:
'@types/dotenv':
specifier: ^8.2.3
version: 8.2.3
ts-node-dev:
specifier: ^2.0.0
version: 2.0.0(@types/node@24.0.13)(typescript@5.8.3)
packages/backend:
dependencies:
'@aws-sdk/client-s3':
@@ -197,6 +235,25 @@ importers:
specifier: ^5.8.3
version: 5.8.3
packages/enterprise:
dependencies:
'@open-archiver/backend':
specifier: workspace:*
version: link:../backend
express:
specifier: ^5.1.0
version: 5.1.0
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
devDependencies:
'@types/express':
specifier: ^5.0.3
version: 5.0.3
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
packages/frontend:
dependencies:
'@iconify/svelte':
@@ -628,6 +685,9 @@ packages:
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@esbuild-kit/core-utils@3.3.2':
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
deprecated: 'Merged into tsx: https://tsx.is'
@@ -1755,6 +1815,10 @@ packages:
'@types/d3-shape@3.1.7':
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
'@types/dotenv@8.2.3':
resolution: {integrity: sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==}
deprecated: This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1770,6 +1834,9 @@ packages:
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
@@ -1791,6 +1858,9 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/multer@2.0.0':
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
@@ -2298,6 +2368,11 @@ packages:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
engines: {node: '>=12.0.0'}
cross-env@10.0.0:
resolution: {integrity: sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==}
engines: {node: '>=20'}
hasBin: true
cross-fetch@4.1.0:
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
@@ -3649,6 +3724,7 @@ packages:
resolution: {integrity: sha512-Nkwo9qeCvqVH0ZgYRUfPyj6o4o7StvNIxMFECeiz4y0uMOVyqc5Y9hjsdFVxdYCeiUjjXLQXA8KIz0iJL3HM0w==}
engines: {node: '>=20.18.0'}
hasBin: true
bundledDependencies: []
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
@@ -5325,6 +5401,8 @@ snapshots:
'@drizzle-team/brocli@0.10.2': {}
'@epic-web/invariant@1.0.0': {}
'@esbuild-kit/core-utils@3.3.2':
dependencies:
esbuild: 0.18.20
@@ -6330,6 +6408,10 @@ snapshots:
dependencies:
'@types/d3-path': 3.1.1
'@types/dotenv@8.2.3':
dependencies:
dotenv: 17.2.0
'@types/estree@1.0.8': {}
'@types/express-serve-static-core@5.0.7':
@@ -6351,6 +6433,11 @@ snapshots:
'@types/http-errors@2.0.5': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 24.0.13
'@types/linkify-it@5.0.0': {}
'@types/mailparser@3.4.6':
@@ -6373,6 +6460,8 @@ snapshots:
'@types/mime@1.3.5': {}
'@types/ms@2.1.0': {}
'@types/multer@2.0.0':
dependencies:
'@types/express': 5.0.3
@@ -6939,6 +7028,11 @@ snapshots:
dependencies:
luxon: 3.7.1
cross-env@10.0.0:
dependencies:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-fetch@4.1.0(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)

View File

@@ -1,3 +1,4 @@
# Defines the pnpm workspace for the monorepo
packages:
- 'packages/*'
- 'apps/*'

View File

@@ -7,7 +7,6 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
"resolveJsonModule": true
}
}