Add tests for authentication and security features

- Implement unit tests for password handling in `auth_test.ts`, covering bcrypt and legacy password hashes.
- Create a fake database for user management to facilitate testing of the `AdminHandler`.
- Validate JWT-based identity verification against database records.
- Introduce tests for credential encryption and registry management in `security_test.ts`.
- Ensure registry passwords are securely stored and can be decrypted correctly, including legacy support.
- Add utility functions for password hashing and verification in `auth.ts`.
This commit is contained in:
2026-04-19 01:30:54 +00:00
parent 0c9eb0653d
commit 618d4d674f
34 changed files with 585 additions and 255 deletions
+105
View File
@@ -0,0 +1,105 @@
import { assert, assertEquals, fail } from '@std/assert';
import * as plugins from '../ts/plugins.ts';
import type { IUser as IDatabaseUser } from '../ts/types.ts';
import { AdminHandler } from '../ts/opsserver/handlers/admin.handler.ts';
import {
hashPassword,
isBcryptHash,
needsPasswordUpgrade,
verifyPassword,
} from '../ts/utils/auth.ts';
class FakeDatabase {
constructor(private users: Map<string, IDatabaseUser>) {}
getUserByUsername(username: string): IDatabaseUser | null {
return this.users.get(username) ?? null;
}
updateUserPassword(username: string, passwordHash: string): void {
const user = this.users.get(username);
if (!user) {
return;
}
this.users.set(username, {
...user,
passwordHash,
updatedAt: Date.now(),
});
}
}
async function createAdminHandler(users: IDatabaseUser[]): Promise<AdminHandler> {
const userMap = new Map(users.map((user) => [user.username, user]));
const fakeOpsServer = {
typedrouter: new plugins.typedrequest.TypedRouter(),
oneboxRef: {
database: new FakeDatabase(userMap),
},
};
const adminHandler = new AdminHandler(fakeOpsServer as any);
await adminHandler.initialize();
return adminHandler;
}
Deno.test('password helpers support bcrypt and legacy password hashes', async () => {
const password = 'correct horse battery staple';
const bcryptHash = await hashPassword(password);
assert(isBcryptHash(bcryptHash));
assert(await verifyPassword(password, bcryptHash));
assert(!(await verifyPassword('wrong password', bcryptHash)));
assert(!needsPasswordUpgrade(bcryptHash));
const legacyHash = btoa(password);
assert(await verifyPassword(password, legacyHash));
assert(needsPasswordUpgrade(legacyHash));
});
Deno.test('verified identity is derived from the signed JWT and database, not client fields', async () => {
const adminHandler = await createAdminHandler([
{
id: 1,
username: 'alice',
passwordHash: await hashPassword('password123'),
role: 'user',
createdAt: Date.now(),
updatedAt: Date.now(),
},
]);
const expiresAt = Date.now() + 60_000;
const jwt = await adminHandler.smartjwtInstance.createJWT({
userId: '1',
username: 'alice',
role: 'user',
status: 'loggedIn',
expiresAt,
});
const verifiedIdentity = await adminHandler.getVerifiedIdentity({
jwt,
userId: '999',
username: 'mallory',
role: 'admin',
expiresAt: 0,
});
assertEquals(verifiedIdentity.userId, '1');
assertEquals(verifiedIdentity.username, 'alice');
assertEquals(verifiedIdentity.role, 'user');
assertEquals(verifiedIdentity.expiresAt, expiresAt);
let rejected = false;
try {
await adminHandler.getVerifiedAdminIdentity(verifiedIdentity);
fail('Expected admin-only identity verification to reject non-admin users');
} catch {
rejected = true;
}
assert(rejected);
});
+61
View File
@@ -0,0 +1,61 @@
import { assert, assertEquals } from '@std/assert';
import type { IRegistry } from '../ts/types.ts';
import { credentialEncryption } from '../ts/classes/encryption.ts';
import { OneboxRegistriesManager } from '../ts/classes/registries.ts';
class FakeRegistryDatabase {
private registries = new Map<string, IRegistry>();
getRegistryByURL(url: string): IRegistry | null {
return this.registries.get(url) ?? null;
}
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
const savedRegistry: IRegistry = {
id: this.registries.size + 1,
...registry,
};
this.registries.set(savedRegistry.url, savedRegistry);
return savedRegistry;
}
deleteRegistry(url: string): void {
this.registries.delete(url);
}
getAllRegistries(): IRegistry[] {
return Array.from(this.registries.values());
}
}
Deno.test('credential encryption lazily initializes and roundtrips payloads', async () => {
const encrypted = await credentialEncryption.encrypt({ password: 'super-secret' });
const decrypted = await credentialEncryption.decrypt<{ password: string }>(encrypted);
assert(encrypted.length > 0);
assertEquals(decrypted.password, 'super-secret');
});
Deno.test('registry passwords use encrypted storage with legacy decode fallback', async () => {
const fakeDatabase = new FakeRegistryDatabase();
const registriesManager = new OneboxRegistriesManager({ database: fakeDatabase } as any);
(registriesManager as any).loginToRegistry = async () => {};
const registry = await registriesManager.addRegistry(
'registry.example.com',
'ci-user',
'correct horse battery staple',
);
assert(registry.passwordEncrypted.startsWith('enc:v1:'));
assertEquals(
await (registriesManager as any).decryptPassword(registry.passwordEncrypted),
'correct horse battery staple',
);
assertEquals(
await (registriesManager as any).decryptPassword(btoa('legacy-password')),
'legacy-password',
);
});
+98 -56
View File
@@ -43,6 +43,14 @@ const IV_LENGTH = 12;
const SALT_LENGTH = 32; const SALT_LENGTH = 32;
const PBKDF2_ITERATIONS = 100000; const PBKDF2_ITERATIONS = 100000;
interface IS3ConnectionInfo {
endpoint: string;
accessKey: string;
secretKey: string;
bucket: string;
region: string;
}
export class BackupManager { export class BackupManager {
private oneboxRef: Onebox; private oneboxRef: Onebox;
public archive: plugins.ContainerArchive | null = null; public archive: plugins.ContainerArchive | null = null;
@@ -519,7 +527,8 @@ export class BackupManager {
* Get backup password from settings * Get backup password from settings
*/ */
private getBackupPassword(): string | null { private getBackupPassword(): string | null {
return this.oneboxRef.database.getSetting('backup_encryption_password'); return this.oneboxRef.database.getSetting('backup_encryption_password')
|| this.oneboxRef.database.getSetting('backupPassword');
} }
/** /**
@@ -860,47 +869,48 @@ export class BackupManager {
const bucketDir = `${dataDir}/${resource.resourceName}`; const bucketDir = `${dataDir}/${resource.resourceName}`;
await Deno.mkdir(bucketDir, { recursive: true }); await Deno.mkdir(bucketDir, { recursive: true });
const endpoint = credentials.endpoint || credentials.S3_ENDPOINT; const s3Info = this.getS3ConnectionInfo(credentials);
const accessKey = credentials.accessKey || credentials.S3_ACCESS_KEY; const s3Client = this.createS3Client(s3Info);
const secretKey = credentials.secretKey || credentials.S3_SECRET_KEY; let objectCount = 0;
const bucket = credentials.bucket || credentials.S3_BUCKET; let continuationToken: string | undefined;
if (!endpoint || !accessKey || !secretKey || !bucket) { do {
throw new Error('MinIO credentials incomplete'); const response = await s3Client.send(
} new plugins.awsS3.ListObjectsV2Command({
Bucket: s3Info.bucket,
ContinuationToken: continuationToken,
}),
);
const s3Client = new plugins.smartstorage.SmartStorage({ for (const object of response.Contents || []) {
endpoint, const objectKey = object.Key;
accessKey,
secretKey,
bucket,
});
await s3Client.start();
const objects = await s3Client.listObjects();
for (const obj of objects) {
const objectKey = obj.Key;
if (!objectKey) continue; if (!objectKey) continue;
const objectData = await s3Client.getObject(objectKey); const objectResponse = await s3Client.send(
if (objectData) { new plugins.awsS3.GetObjectCommand({
Bucket: s3Info.bucket,
Key: objectKey,
}),
);
if (!objectResponse.Body) continue;
const objectPath = `${bucketDir}/${objectKey}`; const objectPath = `${bucketDir}/${objectKey}`;
const parentDir = plugins.path.dirname(objectPath); const parentDir = plugins.path.dirname(objectPath);
await Deno.mkdir(parentDir, { recursive: true }); await Deno.mkdir(parentDir, { recursive: true });
await Deno.writeFile(objectPath, objectData); await Deno.writeFile(objectPath, await objectResponse.Body.transformToByteArray());
} objectCount++;
} }
await s3Client.stop(); continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
} while (continuationToken);
await Deno.writeTextFile( await Deno.writeTextFile(
`${bucketDir}/_metadata.json`, `${bucketDir}/_metadata.json`,
JSON.stringify({ bucket, objectCount: objects.length }, null, 2) JSON.stringify({ bucket: s3Info.bucket, objectCount }, null, 2)
); );
logger.success(`MinIO bucket exported: ${resource.resourceName} (${objects.length} objects)`); logger.success(`MinIO bucket exported: ${resource.resourceName} (${objectCount} objects)`);
} }
/** /**
@@ -1279,39 +1289,25 @@ export class BackupManager {
const bucketDir = `${dataDir}/${backupResourceName}`; const bucketDir = `${dataDir}/${backupResourceName}`;
const endpoint = credentials.endpoint || credentials.S3_ENDPOINT; const s3Info = this.getS3ConnectionInfo(credentials);
const accessKey = credentials.accessKey || credentials.S3_ACCESS_KEY; const s3Client = this.createS3Client(s3Info);
const secretKey = credentials.secretKey || credentials.S3_SECRET_KEY;
const bucket = credentials.bucket || credentials.S3_BUCKET;
if (!endpoint || !accessKey || !secretKey || !bucket) {
throw new Error('MinIO credentials incomplete');
}
const s3Client = new plugins.smartstorage.SmartStorage({
endpoint,
accessKey,
secretKey,
bucket,
});
await s3Client.start();
let uploadedCount = 0; let uploadedCount = 0;
for await (const entry of Deno.readDir(bucketDir)) { for await (const filePath of this.walkFiles(bucketDir)) {
if (entry.name === '_metadata.json') continue; if (plugins.path.basename(filePath) === '_metadata.json') continue;
const filePath = `${bucketDir}/${entry.name}`;
if (entry.isFile) {
const fileData = await Deno.readFile(filePath); const fileData = await Deno.readFile(filePath);
await s3Client.putObject(entry.name, fileData); const objectKey = plugins.path.relative(bucketDir, filePath).replaceAll('\\', '/');
await s3Client.send(
new plugins.awsS3.PutObjectCommand({
Bucket: s3Info.bucket,
Key: objectKey,
Body: fileData,
}),
);
uploadedCount++; uploadedCount++;
} }
}
await s3Client.stop();
logger.success(`MinIO bucket imported: ${resource.resourceName} (${uploadedCount} objects)`); logger.success(`MinIO bucket imported: ${resource.resourceName} (${uploadedCount} objects)`);
} }
@@ -1585,7 +1581,7 @@ export class BackupManager {
return await crypto.subtle.deriveKey( return await crypto.subtle.deriveKey(
{ {
name: 'PBKDF2', name: 'PBKDF2',
salt, salt: this.toArrayBuffer(salt),
iterations: PBKDF2_ITERATIONS, iterations: PBKDF2_ITERATIONS,
hash: 'SHA-256', hash: 'SHA-256',
}, },
@@ -1600,8 +1596,54 @@ export class BackupManager {
* Compute SHA-256 checksum * Compute SHA-256 checksum
*/ */
private async computeChecksum(data: Uint8Array): Promise<string> { private async computeChecksum(data: Uint8Array): Promise<string> {
const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashBuffer = await crypto.subtle.digest('SHA-256', this.toArrayBuffer(data));
const hashArray = new Uint8Array(hashBuffer); const hashArray = new Uint8Array(hashBuffer);
return 'sha256:' + Array.from(hashArray).map((b) => b.toString(16).padStart(2, '0')).join(''); return 'sha256:' + Array.from(hashArray).map((b) => b.toString(16).padStart(2, '0')).join('');
} }
private getS3ConnectionInfo(credentials: Record<string, string>): IS3ConnectionInfo {
const endpoint = credentials.endpoint || credentials.S3_ENDPOINT;
const accessKey = credentials.accessKey || credentials.S3_ACCESS_KEY;
const secretKey = credentials.secretKey || credentials.S3_SECRET_KEY;
const bucket = credentials.bucket || credentials.S3_BUCKET;
if (!endpoint || !accessKey || !secretKey || !bucket) {
throw new Error('MinIO credentials incomplete');
}
return {
endpoint,
accessKey,
secretKey,
bucket,
region: credentials.region || credentials.AWS_REGION || 'us-east-1',
};
}
private createS3Client(s3Info: IS3ConnectionInfo) {
return new plugins.awsS3.S3Client({
endpoint: s3Info.endpoint,
region: s3Info.region,
forcePathStyle: true,
credentials: {
accessKeyId: s3Info.accessKey,
secretAccessKey: s3Info.secretKey,
},
});
}
private async *walkFiles(directory: string): AsyncGenerator<string> {
for await (const entry of Deno.readDir(directory)) {
const entryPath = plugins.path.join(directory, entry.name);
if (entry.isDirectory) {
yield* this.walkFiles(entryPath);
} else if (entry.isFile) {
yield entryPath;
}
}
}
private toArrayBuffer(data: Uint8Array): ArrayBuffer {
return data.slice().buffer as ArrayBuffer;
}
} }
+2 -1
View File
@@ -24,7 +24,8 @@ export class CloudflareDomainSync {
*/ */
async init(): Promise<void> { async init(): Promise<void> {
try { try {
const apiKey = this.database.getSetting('cloudflareAPIKey'); const apiKey = this.database.getSetting('cloudflareAPIKey')
|| this.database.getSetting('cloudflareToken');
if (!apiKey) { if (!apiKey) {
logger.warn('Cloudflare API key not configured. Domain sync will be limited.'); logger.warn('Cloudflare API key not configured. Domain sync will be limited.');
+2 -1
View File
@@ -27,7 +27,8 @@ export class OneboxDnsManager {
async init(): Promise<void> { async init(): Promise<void> {
try { try {
// Get Cloudflare credentials from settings // Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey'); const apiKey = this.database.getSetting('cloudflareAPIKey')
|| this.database.getSetting('cloudflareToken');
const serverIP = this.database.getSetting('serverIP'); const serverIP = this.database.getSetting('serverIP');
if (!apiKey) { if (!apiKey) {
+16 -6
View File
@@ -97,7 +97,11 @@ export class CredentialEncryption {
*/ */
async encrypt(data: Record<string, string>): Promise<string> { async encrypt(data: Record<string, string>): Promise<string> {
if (!this.key) { if (!this.key) {
throw new Error('Encryption not initialized. Call init() first.'); await this.init();
}
const key = this.key;
if (!key) {
throw new Error('Encryption key initialization failed.');
} }
const iv = crypto.getRandomValues(new Uint8Array(this.ivLength)); const iv = crypto.getRandomValues(new Uint8Array(this.ivLength));
@@ -105,7 +109,7 @@ export class CredentialEncryption {
const ciphertext = await crypto.subtle.encrypt( const ciphertext = await crypto.subtle.encrypt(
{ name: this.algorithm, iv }, { name: this.algorithm, iv },
this.key, key,
encoded encoded
); );
@@ -120,9 +124,15 @@ export class CredentialEncryption {
/** /**
* Decrypt a base64 string back to credentials object * Decrypt a base64 string back to credentials object
*/ */
async decrypt(encrypted: string): Promise<Record<string, string>> { async decrypt<T extends Record<string, string> = Record<string, string>>(
encrypted: string,
): Promise<T> {
if (!this.key) { if (!this.key) {
throw new Error('Encryption not initialized. Call init() first.'); await this.init();
}
const key = this.key;
if (!key) {
throw new Error('Encryption key initialization failed.');
} }
const combined = this.base64ToBytes(encrypted); const combined = this.base64ToBytes(encrypted);
@@ -133,12 +143,12 @@ export class CredentialEncryption {
const decrypted = await crypto.subtle.decrypt( const decrypted = await crypto.subtle.decrypt(
{ name: this.algorithm, iv }, { name: this.algorithm, iv },
this.key, key,
ciphertext ciphertext
); );
const decoded = new TextDecoder().decode(decrypted); const decoded = new TextDecoder().decode(decrypted);
return JSON.parse(decoded); return JSON.parse(decoded) as T;
} }
/** /**
+14 -10
View File
@@ -6,6 +6,7 @@
import * as plugins from '../plugins.ts'; import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { hashPassword, needsPasswordUpgrade, verifyPassword } from '../utils/auth.ts';
import { getErrorMessage } from '../utils/error.ts'; import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts'; import type { Onebox } from './onebox.ts';
import type { import type {
@@ -404,15 +405,17 @@ export class OneboxHttpServer {
logger.info(`User found: ${username}, checking password...`); logger.info(`User found: ${username}, checking password...`);
// Verify password (simple base64 comparison for now) const passwordMatches = await verifyPassword(password, user.passwordHash);
const passwordHash = btoa(password); if (!passwordMatches) {
logger.info(`Password hash: ${passwordHash}, stored hash: ${user.passwordHash}`);
if (passwordHash !== user.passwordHash) {
logger.info(`Password mismatch for user: ${username}`); logger.info(`Password mismatch for user: ${username}`);
return this.jsonResponse({ success: false, error: 'Invalid credentials' }, 401); return this.jsonResponse({ success: false, error: 'Invalid credentials' }, 401);
} }
if (needsPasswordUpgrade(user.passwordHash)) {
const upgradedHash = await hashPassword(password);
this.oneboxRef.database.updateUserPassword(user.username, upgradedHash);
}
// Generate simple token (in production, use proper JWT) // Generate simple token (in production, use proper JWT)
const token = btoa(`${user.username}:${Date.now()}`); const token = btoa(`${user.username}:${Date.now()}`);
@@ -1324,7 +1327,7 @@ export class OneboxHttpServer {
type: 'service', type: 'service',
name: service.name, name: service.name,
domain: service.domain || null, domain: service.domain || null,
targetHost: service.containerIP || 'unknown', targetHost: service.containerID || 'unknown',
targetPort: service.port || 80, targetPort: service.port || 80,
status: service.status, status: service.status,
}); });
@@ -1380,6 +1383,7 @@ export class OneboxHttpServer {
rabbitmq: 5672, rabbitmq: 5672,
caddy: 80, caddy: 80,
clickhouse: 8123, clickhouse: 8123,
mariadb: 3306,
}; };
return ports[type] || 0; return ports[type] || 0;
} }
@@ -1396,11 +1400,11 @@ export class OneboxHttpServer {
success: true, success: true,
data: { data: {
proxy: { proxy: {
running: proxyStatus.running, running: proxyStatus.http.running || proxyStatus.https.running,
httpPort: proxyStatus.httpPort, httpPort: proxyStatus.http.port,
httpsPort: proxyStatus.httpsPort, httpsPort: proxyStatus.https.port,
routes: proxyStatus.routes, routes: proxyStatus.routes,
certificates: proxyStatus.certificates, certificates: proxyStatus.https.certificates,
}, },
logReceiver: { logReceiver: {
running: logReceiverStats.running, running: logReceiverStats.running,
+2 -2
View File
@@ -6,6 +6,7 @@
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts'; import { getErrorMessage } from '../utils/error.ts';
import { hashPassword } from '../utils/auth.ts';
import { OneboxDatabase } from './database.ts'; import { OneboxDatabase } from './database.ts';
import { OneboxDockerManager } from './docker.ts'; import { OneboxDockerManager } from './docker.ts';
import { OneboxServicesManager } from './services.ts'; import { OneboxServicesManager } from './services.ts';
@@ -226,8 +227,7 @@ export class Onebox {
if (!adminUser) { if (!adminUser) {
logger.info('Creating default admin user...'); logger.info('Creating default admin user...');
// Simple base64 encoding for now - should use bcrypt in production const passwordHash = await hashPassword('admin');
const passwordHash = btoa('admin');
await this.database.createUser({ await this.database.createUser({
username: 'admin', username: 'admin',
@@ -76,7 +76,9 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
if (dataExists && platformService?.adminCredentialsEncrypted) { if (dataExists && platformService?.adminCredentialsEncrypted) {
// Reuse existing credentials from database // Reuse existing credentials from database
logger.info('Reusing existing ClickHouse credentials (data directory already initialized)'); logger.info('Reusing existing ClickHouse credentials (data directory already initialized)');
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
} else { } else {
// Generate new credentials for fresh deployment // Generate new credentials for fresh deployment
logger.info('Generating new ClickHouse admin credentials'); logger.info('Generating new ClickHouse admin credentials');
@@ -191,7 +193,9 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
throw new Error('ClickHouse platform service not found or not configured'); throw new Error('ClickHouse platform service not found or not configured');
} }
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
const containerName = this.getContainerName(); const containerName = this.getContainerName();
// Generate resource names and credentials // Generate resource names and credentials
@@ -247,7 +251,9 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
throw new Error('ClickHouse platform service not found or not configured'); throw new Error('ClickHouse platform service not found or not configured');
} }
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
logger.info(`Deprovisioning ClickHouse database '${resource.resourceName}'...`); logger.info(`Deprovisioning ClickHouse database '${resource.resourceName}'...`);
@@ -74,7 +74,9 @@ export class MariaDBProvider extends BasePlatformServiceProvider {
if (dataExists && platformService?.adminCredentialsEncrypted) { if (dataExists && platformService?.adminCredentialsEncrypted) {
// Reuse existing credentials from database // Reuse existing credentials from database
logger.info('Reusing existing MariaDB credentials (data directory already initialized)'); logger.info('Reusing existing MariaDB credentials (data directory already initialized)');
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
} else { } else {
// Generate new credentials for fresh deployment // Generate new credentials for fresh deployment
logger.info('Generating new MariaDB admin credentials'); logger.info('Generating new MariaDB admin credentials');
@@ -80,7 +80,9 @@ export class MinioProvider extends BasePlatformServiceProvider {
if (dataExists && platformService?.adminCredentialsEncrypted) { if (dataExists && platformService?.adminCredentialsEncrypted) {
// Reuse existing credentials from database // Reuse existing credentials from database
logger.info('Reusing existing MinIO credentials (data directory already initialized)'); logger.info('Reusing existing MinIO credentials (data directory already initialized)');
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
} else { } else {
// Generate new credentials for fresh deployment // Generate new credentials for fresh deployment
logger.info('Generating new MinIO admin credentials'); logger.info('Generating new MinIO admin credentials');
@@ -74,7 +74,9 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
if (dataExists && platformService?.adminCredentialsEncrypted) { if (dataExists && platformService?.adminCredentialsEncrypted) {
// Reuse existing credentials from database // Reuse existing credentials from database
logger.info('Reusing existing MongoDB credentials (data directory already initialized)'); logger.info('Reusing existing MongoDB credentials (data directory already initialized)');
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
} else { } else {
// Generate new credentials for fresh deployment // Generate new credentials for fresh deployment
logger.info('Generating new MongoDB admin credentials'); logger.info('Generating new MongoDB admin credentials');
@@ -76,7 +76,9 @@ export class RedisProvider extends BasePlatformServiceProvider {
if (dataExists && platformService?.adminCredentialsEncrypted) { if (dataExists && platformService?.adminCredentialsEncrypted) {
// Reuse existing credentials from database // Reuse existing credentials from database
logger.info('Reusing existing Redis credentials (data directory already initialized)'); logger.info('Reusing existing Redis credentials (data directory already initialized)');
adminCredentials = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
platformService.adminCredentialsEncrypted,
);
} else { } else {
// Generate new credentials for fresh deployment // Generate new credentials for fresh deployment
logger.info('Generating new Redis admin credentials'); logger.info('Generating new Redis admin credentials');
+17 -8
View File
@@ -9,6 +9,9 @@ import type { IRegistry } from '../types.ts';
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts'; import { getErrorMessage } from '../utils/error.ts';
import { OneboxDatabase } from './database.ts'; import { OneboxDatabase } from './database.ts';
import { credentialEncryption } from './encryption.ts';
const encryptedPasswordPrefix = 'enc:v1:';
export class OneboxRegistriesManager { export class OneboxRegistriesManager {
private oneboxRef: any; // Will be Onebox instance private oneboxRef: any; // Will be Onebox instance
@@ -22,17 +25,23 @@ export class OneboxRegistriesManager {
/** /**
* Encrypt a password (simple base64 for now, should use proper encryption) * Encrypt a password (simple base64 for now, should use proper encryption)
*/ */
private encryptPassword(password: string): string { private async encryptPassword(password: string): Promise<string> {
// TODO: Use proper encryption with a secret key const encrypted = await credentialEncryption.encrypt({ password });
// For now, using base64 encoding (NOT SECURE, just for structure) return `${encryptedPasswordPrefix}${encrypted}`;
return plugins.encoding.encodeBase64(new TextEncoder().encode(password));
} }
/** /**
* Decrypt a password * Decrypt a password
*/ */
private decryptPassword(encrypted: string): string { private async decryptPassword(encrypted: string): Promise<string> {
// TODO: Use proper decryption if (encrypted.startsWith(encryptedPasswordPrefix)) {
const decrypted = await credentialEncryption.decrypt<{ password: string }>(
encrypted.slice(encryptedPasswordPrefix.length),
);
return decrypted.password;
}
// Legacy compatibility for older databases that stored base64-encoded passwords.
return new TextDecoder().decode(plugins.encoding.decodeBase64(encrypted)); return new TextDecoder().decode(plugins.encoding.decodeBase64(encrypted));
} }
@@ -48,7 +57,7 @@ export class OneboxRegistriesManager {
} }
// Encrypt password // Encrypt password
const passwordEncrypted = this.encryptPassword(password); const passwordEncrypted = await this.encryptPassword(password);
// Create registry in database // Create registry in database
const registry = await this.database.createRegistry({ const registry = await this.database.createRegistry({
@@ -111,7 +120,7 @@ export class OneboxRegistriesManager {
try { try {
logger.info(`Logging into registry: ${registry.url}`); logger.info(`Logging into registry: ${registry.url}`);
const password = this.decryptPassword(registry.passwordEncrypted); const password = await this.decryptPassword(registry.passwordEncrypted);
// Use docker login command // Use docker login command
const command = [ const command = [
+2 -1
View File
@@ -39,7 +39,8 @@ export class OneboxSslManager {
this.acmeEmail = acmeEmail; this.acmeEmail = acmeEmail;
// Get Cloudflare API key (reuse from DNS manager) // Get Cloudflare API key (reuse from DNS manager)
const cfApiKey = this.database.getSetting('cloudflareAPIKey'); const cfApiKey = this.database.getSetting('cloudflareAPIKey')
|| this.database.getSetting('cloudflareToken');
if (!cfApiKey) { if (!cfApiKey) {
logger.warn('Cloudflare API key not configured. SSL certificate management will be limited.'); logger.warn('Cloudflare API key not configured. SSL certificate management will be limited.');
+97 -52
View File
@@ -2,9 +2,12 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { hashPassword, needsPasswordUpgrade, verifyPassword } from '../../utils/auth.ts';
export interface IJwtData { export interface IJwtData {
userId: string; userId: string;
username: string;
role: 'admin' | 'user';
status: 'loggedIn' | 'loggedOut'; status: 'loggedIn' | 'loggedOut';
expiresAt: number; expiresAt: number;
} }
@@ -18,12 +21,80 @@ export class AdminHandler {
} }
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
this.smartjwtInstance = new plugins.smartjwt.SmartJwt(); this.smartjwtInstance = new plugins.smartjwt.SmartJwt<IJwtData>();
await this.smartjwtInstance.init(); await this.smartjwtInstance.init();
await this.smartjwtInstance.createNewKeyPair();
this.registerHandlers(); this.registerHandlers();
} }
private async createIdentityForUser(
user: interfaces.data.IUser & { id?: number },
expiresAt: number,
): Promise<interfaces.data.IIdentity> {
const userId = String(user.id || user.username);
const jwt = await this.smartjwtInstance.createJWT({
userId,
username: user.username,
role: user.role,
status: 'loggedIn',
expiresAt,
});
return {
jwt,
userId,
username: user.username,
expiresAt,
role: user.role,
};
}
public async getVerifiedIdentity(
identityArg: interfaces.data.IIdentity | null | undefined,
): Promise<interfaces.data.IIdentity> {
if (!identityArg?.jwt) {
throw new plugins.typedrequest.TypedResponseError('No identity provided');
}
let jwtData: IJwtData;
try {
jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt);
} catch {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
if (jwtData.expiresAt < Date.now() || jwtData.status !== 'loggedIn') {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
const user = this.opsServerRef.oneboxRef.database.getUserByUsername(jwtData.username);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
const userId = String(user.id || user.username);
if (jwtData.userId !== userId) {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
return {
jwt: identityArg.jwt,
userId,
username: user.username,
expiresAt: jwtData.expiresAt,
role: user.role,
};
}
public async getVerifiedAdminIdentity(
identityArg: interfaces.data.IIdentity | null | undefined,
): Promise<interfaces.data.IIdentity> {
const identity = await this.getVerifiedIdentity(identityArg);
if (identity.role !== 'admin') {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
return identity;
}
private registerHandlers(): void { private registerHandlers(): void {
// Login // Login
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
@@ -36,30 +107,24 @@ export class AdminHandler {
throw new plugins.typedrequest.TypedResponseError('Invalid credentials'); throw new plugins.typedrequest.TypedResponseError('Invalid credentials');
} }
// Verify password (base64 comparison to match existing DB scheme) const passwordMatches = await verifyPassword(dataArg.password, user.passwordHash);
const passwordHash = btoa(dataArg.password); if (!passwordMatches) {
if (passwordHash !== user.passwordHash) {
throw new plugins.typedrequest.TypedResponseError('Invalid credentials'); throw new plugins.typedrequest.TypedResponseError('Invalid credentials');
} }
if (needsPasswordUpgrade(user.passwordHash)) {
const upgradedHash = await hashPassword(dataArg.password);
this.opsServerRef.oneboxRef.database.updateUserPassword(user.username, upgradedHash);
}
const expiresAt = Date.now() + 24 * 3600 * 1000; const expiresAt = Date.now() + 24 * 3600 * 1000;
const userId = String(user.id || user.username); const freshUser = this.opsServerRef.oneboxRef.database.getUserByUsername(user.username) || user;
const jwt = await this.smartjwtInstance.createJWT({ const identity = await this.createIdentityForUser(freshUser, expiresAt);
userId,
status: 'loggedIn',
expiresAt,
});
logger.info(`User logged in: ${user.username}`); logger.info(`User logged in: ${user.username}`);
return { return {
identity: { identity,
jwt,
userId,
username: user.username,
expiresAt,
role: user.role,
},
}; };
} catch (error) { } catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error; if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
@@ -84,22 +149,11 @@ export class AdminHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
'verifyIdentity', 'verifyIdentity',
async (dataArg) => { async (dataArg) => {
if (!dataArg.identity?.jwt) {
return { valid: false };
}
try { try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt); const identity = await this.getVerifiedIdentity(dataArg.identity);
if (jwtData.expiresAt < Date.now()) return { valid: false };
if (jwtData.status !== 'loggedIn') return { valid: false };
return { return {
valid: true, valid: true,
identity: { identity,
jwt: dataArg.identity.jwt,
userId: jwtData.userId,
username: dataArg.identity.username,
expiresAt: jwtData.expiresAt,
role: dataArg.identity.role,
},
}; };
} catch { } catch {
return { valid: false }; return { valid: false };
@@ -113,18 +167,18 @@ export class AdminHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ChangePassword>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ChangePassword>(
'changePassword', 'changePassword',
async (dataArg) => { async (dataArg) => {
await this.requireValidIdentity(dataArg); const identity = await this.getVerifiedIdentity(dataArg.identity);
const user = this.opsServerRef.oneboxRef.database.getUserByUsername(dataArg.identity.username); const user = this.opsServerRef.oneboxRef.database.getUserByUsername(identity.username);
if (!user) { if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found'); throw new plugins.typedrequest.TypedResponseError('User not found');
} }
const currentHash = btoa(dataArg.currentPassword); const currentPasswordMatches = await verifyPassword(dataArg.currentPassword, user.passwordHash);
if (currentHash !== user.passwordHash) { if (!currentPasswordMatches) {
throw new plugins.typedrequest.TypedResponseError('Current password is incorrect'); throw new plugins.typedrequest.TypedResponseError('Current password is incorrect');
} }
const newHash = btoa(dataArg.newPassword); const newHash = await hashPassword(dataArg.newPassword);
this.opsServerRef.oneboxRef.database.updateUserPassword(user.username, newHash); this.opsServerRef.oneboxRef.database.updateUserPassword(user.username, newHash);
logger.info(`Password changed for user: ${user.username}`); logger.info(`Password changed for user: ${user.username}`);
@@ -134,25 +188,13 @@ export class AdminHandler {
); );
} }
private async requireValidIdentity(dataArg: { identity: interfaces.data.IIdentity }): Promise<void> {
const passed = await this.validIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
}
// Guard for valid identity // Guard for valid identity
public validIdentityGuard = new plugins.smartguard.Guard<{ public validIdentityGuard = new plugins.smartguard.Guard<{
identity: interfaces.data.IIdentity; identity: interfaces.data.IIdentity;
}>( }>(
async (dataArg) => { async (dataArg) => {
if (!dataArg.identity?.jwt) return false;
try { try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt); await this.getVerifiedIdentity(dataArg.identity);
if (jwtData.expiresAt < Date.now()) return false;
if (jwtData.status !== 'loggedIn') return false;
if (dataArg.identity.expiresAt !== jwtData.expiresAt) return false;
if (dataArg.identity.userId !== jwtData.userId) return false;
return true; return true;
} catch { } catch {
return false; return false;
@@ -166,9 +208,12 @@ export class AdminHandler {
identity: interfaces.data.IIdentity; identity: interfaces.data.IIdentity;
}>( }>(
async (dataArg) => { async (dataArg) => {
const isValid = await this.validIdentityGuard.exec(dataArg); try {
if (!isValid) return false; const identity = await this.getVerifiedIdentity(dataArg.identity);
return dataArg.identity.role === 'admin'; return identity.role === 'admin';
} catch {
return false;
}
}, },
{ failedHint: 'user is not admin', name: 'adminIdentityGuard' }, { failedHint: 'user is not admin', name: 'adminIdentityGuard' },
); );
+5 -5
View File
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class AppStoreHandler { export class AppStoreHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -18,7 +18,7 @@ export class AppStoreHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppTemplates>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppTemplates>(
'getAppTemplates', 'getAppTemplates',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const apps = await this.opsServerRef.oneboxRef.appStore.getApps(); const apps = await this.opsServerRef.oneboxRef.appStore.getApps();
return { apps }; return { apps };
}, },
@@ -30,7 +30,7 @@ export class AppStoreHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppConfig>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppConfig>(
'getAppConfig', 'getAppConfig',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const config = await this.opsServerRef.oneboxRef.appStore.getAppVersionConfig( const config = await this.opsServerRef.oneboxRef.appStore.getAppVersionConfig(
dataArg.appId, dataArg.appId,
dataArg.version, dataArg.version,
@@ -46,7 +46,7 @@ export class AppStoreHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUpgradeableServices>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUpgradeableServices>(
'getUpgradeableServices', 'getUpgradeableServices',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const services = await this.opsServerRef.oneboxRef.appStore.getUpgradeableServices(); const services = await this.opsServerRef.oneboxRef.appStore.getUpgradeableServices();
return { services }; return { services };
}, },
@@ -58,7 +58,7 @@ export class AppStoreHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpgradeService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpgradeService>(
'upgradeService', 'upgradeService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const existingService = this.opsServerRef.oneboxRef.database.getServiceByName(dataArg.serviceName); const existingService = this.opsServerRef.oneboxRef.database.getServiceByName(dataArg.serviceName);
if (!existingService) { if (!existingService) {
+6 -6
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class BackupsHandler { export class BackupsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class BackupsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackups>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackups>(
'getBackups', 'getBackups',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backups = this.opsServerRef.oneboxRef.backupManager.listBackups(); const backups = this.opsServerRef.oneboxRef.backupManager.listBackups();
return { backups }; return { backups };
}, },
@@ -27,7 +27,7 @@ export class BackupsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackup>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackup>(
'getBackup', 'getBackup',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backup = this.opsServerRef.oneboxRef.database.getBackupById(dataArg.backupId); const backup = this.opsServerRef.oneboxRef.database.getBackupById(dataArg.backupId);
if (!backup) { if (!backup) {
throw new plugins.typedrequest.TypedResponseError('Backup not found'); throw new plugins.typedrequest.TypedResponseError('Backup not found');
@@ -41,7 +41,7 @@ export class BackupsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteBackup>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteBackup>(
'deleteBackup', 'deleteBackup',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.backupManager.deleteBackup(dataArg.backupId); await this.opsServerRef.oneboxRef.backupManager.deleteBackup(dataArg.backupId);
return { ok: true }; return { ok: true };
}, },
@@ -52,7 +52,7 @@ export class BackupsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RestoreBackup>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RestoreBackup>(
'restoreBackup', 'restoreBackup',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const rawResult = await this.opsServerRef.oneboxRef.backupManager.restoreBackup( const rawResult = await this.opsServerRef.oneboxRef.backupManager.restoreBackup(
dataArg.backupId, dataArg.backupId,
dataArg.options, dataArg.options,
@@ -75,7 +75,7 @@ export class BackupsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DownloadBackup>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DownloadBackup>(
'downloadBackup', 'downloadBackup',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backup = this.opsServerRef.oneboxRef.database.getBackupById(dataArg.backupId); const backup = this.opsServerRef.oneboxRef.database.getBackupById(dataArg.backupId);
if (!backup) { if (!backup) {
throw new plugins.typedrequest.TypedResponseError('Backup not found'); throw new plugins.typedrequest.TypedResponseError('Backup not found');
+5 -5
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class DnsHandler { export class DnsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class DnsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsRecords>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsRecords>(
'getDnsRecords', 'getDnsRecords',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const records = this.opsServerRef.oneboxRef.dns.listDNSRecords(); const records = this.opsServerRef.oneboxRef.dns.listDNSRecords();
return { records }; return { records };
}, },
@@ -27,7 +27,7 @@ export class DnsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDnsRecord>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDnsRecord>(
'createDnsRecord', 'createDnsRecord',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.dns.addDNSRecord(dataArg.domain, dataArg.value); await this.opsServerRef.oneboxRef.dns.addDNSRecord(dataArg.domain, dataArg.value);
const records = this.opsServerRef.oneboxRef.dns.listDNSRecords(); const records = this.opsServerRef.oneboxRef.dns.listDNSRecords();
const record = records.find((r: any) => r.domain === dataArg.domain); const record = records.find((r: any) => r.domain === dataArg.domain);
@@ -40,7 +40,7 @@ export class DnsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteDnsRecord>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteDnsRecord>(
'deleteDnsRecord', 'deleteDnsRecord',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.dns.removeDNSRecord(dataArg.domain); await this.opsServerRef.oneboxRef.dns.removeDNSRecord(dataArg.domain);
return { ok: true }; return { ok: true };
}, },
@@ -51,7 +51,7 @@ export class DnsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncDns>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncDns>(
'syncDns', 'syncDns',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
if (!this.opsServerRef.oneboxRef.dns.isConfigured()) { if (!this.opsServerRef.oneboxRef.dns.isConfigured()) {
throw new plugins.typedrequest.TypedResponseError('DNS manager not configured'); throw new plugins.typedrequest.TypedResponseError('DNS manager not configured');
} }
+4 -4
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class DomainsHandler { export class DomainsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -57,7 +57,7 @@ export class DomainsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDomains>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDomains>(
'getDomains', 'getDomains',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const domains = this.buildDomainViews(); const domains = this.buildDomainViews();
return { domains }; return { domains };
}, },
@@ -68,7 +68,7 @@ export class DomainsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDomain>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDomain>(
'getDomain', 'getDomain',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const domain = this.opsServerRef.oneboxRef.database.getDomainByName(dataArg.domainName); const domain = this.opsServerRef.oneboxRef.database.getDomainByName(dataArg.domainName);
if (!domain) { if (!domain) {
throw new plugins.typedrequest.TypedResponseError('Domain not found'); throw new plugins.typedrequest.TypedResponseError('Domain not found');
@@ -87,7 +87,7 @@ export class DomainsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncDomains>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncDomains>(
'syncDomains', 'syncDomains',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
if (!this.opsServerRef.oneboxRef.cloudflareDomainSync) { if (!this.opsServerRef.oneboxRef.cloudflareDomainSync) {
throw new plugins.typedrequest.TypedResponseError('Cloudflare domain sync not configured'); throw new plugins.typedrequest.TypedResponseError('Cloudflare domain sync not configured');
} }
+5 -5
View File
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -18,7 +18,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceLogStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceLogStream>(
'getServiceLogStream', 'getServiceLogStream',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.database.getServiceByName(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.database.getServiceByName(dataArg.serviceName);
if (!service) { if (!service) {
@@ -99,7 +99,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogStream>(
'getPlatformServiceLogStream', 'getPlatformServiceLogStream',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const platformService = this.opsServerRef.oneboxRef.database.getPlatformServiceByType( const platformService = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(
dataArg.serviceType, dataArg.serviceType,
@@ -160,7 +160,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkLogStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkLogStream>(
'getNetworkLogStream', 'getNetworkLogStream',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>(); const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@@ -195,7 +195,7 @@ export class LogsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEventStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEventStream>(
'getEventStream', 'getEventStream',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>(); const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
+4 -4
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
import type { TPlatformServiceType } from '../../types.ts'; import type { TPlatformServiceType } from '../../types.ts';
export class NetworkHandler { export class NetworkHandler {
@@ -31,7 +31,7 @@ export class NetworkHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkTargets>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkTargets>(
'getNetworkTargets', 'getNetworkTargets',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const targets: interfaces.data.INetworkTarget[] = []; const targets: interfaces.data.INetworkTarget[] = [];
// Services // Services
@@ -83,7 +83,7 @@ export class NetworkHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>(
'getNetworkStats', 'getNetworkStats',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any; const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
const logReceiverStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getStats(); const logReceiverStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getStats();
@@ -114,7 +114,7 @@ export class NetworkHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetTrafficStats>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetTrafficStats>(
'getTrafficStats', 'getTrafficStats',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const trafficStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getTrafficStats(60); const trafficStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getTrafficStats(60);
return { stats: trafficStats }; return { stats: trafficStats };
}, },
+9 -9
View File
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class PlatformHandler { export class PlatformHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -99,7 +99,7 @@ export class PlatformHandler {
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard') typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => { .then((connections: any[]) => {
for (const conn of connections) { for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushPlatformServiceLog>( typedsocket.createTypedRequest(
'pushPlatformServiceLog', 'pushPlatformServiceLog',
conn, conn,
).fire({ serviceType, entry }).catch(() => {}); ).fire({ serviceType, entry }).catch(() => {});
@@ -121,7 +121,7 @@ export class PlatformHandler {
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard') typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => { .then((connections: any[]) => {
for (const conn of connections) { for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushServiceLog>( typedsocket.createTypedRequest(
'pushServiceLog', 'pushServiceLog',
conn, conn,
).fire({ serviceName, entry }).catch(() => {}); ).fire({ serviceName, entry }).catch(() => {});
@@ -136,7 +136,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServices>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServices>(
'getPlatformServices', 'getPlatformServices',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const platformServices = this.opsServerRef.oneboxRef.platformServices.getAllPlatformServices(); const platformServices = this.opsServerRef.oneboxRef.platformServices.getAllPlatformServices();
const providers = this.opsServerRef.oneboxRef.platformServices.getAllProviders(); const providers = this.opsServerRef.oneboxRef.platformServices.getAllProviders();
@@ -172,7 +172,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformService>(
'getPlatformService', 'getPlatformService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType); const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType);
if (!provider) { if (!provider) {
throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`); throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`);
@@ -208,7 +208,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartPlatformService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartPlatformService>(
'startPlatformService', 'startPlatformService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType); const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType);
if (!provider) { if (!provider) {
throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`); throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`);
@@ -235,7 +235,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StopPlatformService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StopPlatformService>(
'stopPlatformService', 'stopPlatformService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType); const provider = this.opsServerRef.oneboxRef.platformServices.getProvider(dataArg.serviceType);
if (!provider) { if (!provider) {
throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`); throw new plugins.typedrequest.TypedResponseError(`Unknown platform service type: ${dataArg.serviceType}`);
@@ -268,7 +268,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceStats>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceStats>(
'getPlatformServiceStats', 'getPlatformServiceStats',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType); const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType);
if (!service || !service.containerId) { if (!service || !service.containerId) {
throw new plugins.typedrequest.TypedResponseError('Platform service has no container'); throw new plugins.typedrequest.TypedResponseError('Platform service has no container');
@@ -289,7 +289,7 @@ export class PlatformHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogs>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogs>(
'getPlatformServiceLogs', 'getPlatformServiceLogs',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType); const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType);
if (!service || !service.containerId) { if (!service || !service.containerId) {
throw new plugins.typedrequest.TypedResponseError('Platform service has no container'); throw new plugins.typedrequest.TypedResponseError('Platform service has no container');
+6 -6
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class RegistryHandler { export class RegistryHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -17,7 +17,7 @@ export class RegistryHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRegistryTags>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRegistryTags>(
'getRegistryTags', 'getRegistryTags',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const tags = await this.opsServerRef.oneboxRef.registry.getImageTags(dataArg.serviceName); const tags = await this.opsServerRef.oneboxRef.registry.getImageTags(dataArg.serviceName);
return { tags }; return { tags };
}, },
@@ -29,7 +29,7 @@ export class RegistryHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRegistryTokens>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRegistryTokens>(
'getRegistryTokens', 'getRegistryTokens',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const rawTokens = this.opsServerRef.oneboxRef.database.getAllRegistryTokens(); const rawTokens = this.opsServerRef.oneboxRef.database.getAllRegistryTokens();
const now = Date.now(); const now = Date.now();
@@ -68,7 +68,7 @@ export class RegistryHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRegistryToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRegistryToken>(
'createRegistryToken', 'createRegistryToken',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const identity = await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const config = dataArg.tokenConfig; const config = dataArg.tokenConfig;
// Calculate expiration // Calculate expiration
@@ -95,7 +95,7 @@ export class RegistryHandler {
expiresAt, expiresAt,
createdAt: now, createdAt: now,
lastUsedAt: null, lastUsedAt: null,
createdBy: dataArg.identity.username, createdBy: identity.username,
}); });
let scopeDisplay: string; let scopeDisplay: string;
@@ -133,7 +133,7 @@ export class RegistryHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRegistryToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRegistryToken>(
'deleteRegistryToken', 'deleteRegistryToken',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const token = this.opsServerRef.oneboxRef.database.getRegistryTokenById(dataArg.tokenId); const token = this.opsServerRef.oneboxRef.database.getRegistryTokenById(dataArg.tokenId);
if (!token) { if (!token) {
throw new plugins.typedrequest.TypedResponseError('Token not found'); throw new plugins.typedrequest.TypedResponseError('Token not found');
+7 -7
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class SchedulesHandler { export class SchedulesHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupSchedules>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupSchedules>(
'getBackupSchedules', 'getBackupSchedules',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const schedules = this.opsServerRef.oneboxRef.backupScheduler.getAllSchedules(); const schedules = this.opsServerRef.oneboxRef.backupScheduler.getAllSchedules();
return { schedules }; return { schedules };
}, },
@@ -27,7 +27,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateBackupSchedule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateBackupSchedule>(
'createBackupSchedule', 'createBackupSchedule',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const schedule = await this.opsServerRef.oneboxRef.backupScheduler.createSchedule( const schedule = await this.opsServerRef.oneboxRef.backupScheduler.createSchedule(
dataArg.scheduleConfig, dataArg.scheduleConfig,
); );
@@ -40,7 +40,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupSchedule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupSchedule>(
'getBackupSchedule', 'getBackupSchedule',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const schedule = this.opsServerRef.oneboxRef.backupScheduler.getScheduleById(dataArg.scheduleId); const schedule = this.opsServerRef.oneboxRef.backupScheduler.getScheduleById(dataArg.scheduleId);
if (!schedule) { if (!schedule) {
throw new plugins.typedrequest.TypedResponseError('Schedule not found'); throw new plugins.typedrequest.TypedResponseError('Schedule not found');
@@ -54,7 +54,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateBackupSchedule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateBackupSchedule>(
'updateBackupSchedule', 'updateBackupSchedule',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const schedule = await this.opsServerRef.oneboxRef.backupScheduler.updateSchedule( const schedule = await this.opsServerRef.oneboxRef.backupScheduler.updateSchedule(
dataArg.scheduleId, dataArg.scheduleId,
dataArg.updates, dataArg.updates,
@@ -68,7 +68,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteBackupSchedule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteBackupSchedule>(
'deleteBackupSchedule', 'deleteBackupSchedule',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.backupScheduler.deleteSchedule(dataArg.scheduleId); await this.opsServerRef.oneboxRef.backupScheduler.deleteSchedule(dataArg.scheduleId);
return { ok: true }; return { ok: true };
}, },
@@ -79,7 +79,7 @@ export class SchedulesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TriggerBackupSchedule>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TriggerBackupSchedule>(
'triggerBackupSchedule', 'triggerBackupSchedule',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.backupScheduler.triggerBackup(dataArg.scheduleId); await this.opsServerRef.oneboxRef.backupScheduler.triggerBackup(dataArg.scheduleId);
// triggerBackup is void; the backup is created async by the scheduler // triggerBackup is void; the backup is created async by the scheduler
// Return the most recent backup for the schedule // Return the most recent backup for the schedule
+16 -16
View File
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class ServicesHandler { export class ServicesHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -18,7 +18,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServices>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServices>(
'getServices', 'getServices',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const services = this.opsServerRef.oneboxRef.services.listServices(); const services = this.opsServerRef.oneboxRef.services.listServices();
return { services }; return { services };
}, },
@@ -30,7 +30,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetService>(
'getService', 'getService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
if (!service) { if (!service) {
throw new plugins.typedrequest.TypedResponseError('Service not found'); throw new plugins.typedrequest.TypedResponseError('Service not found');
@@ -45,7 +45,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateService>(
'createService', 'createService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = await this.opsServerRef.oneboxRef.services.deployService(dataArg.serviceConfig); const service = await this.opsServerRef.oneboxRef.services.deployService(dataArg.serviceConfig);
return { service }; return { service };
}, },
@@ -57,7 +57,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateService>(
'updateService', 'updateService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = await this.opsServerRef.oneboxRef.services.updateService( const service = await this.opsServerRef.oneboxRef.services.updateService(
dataArg.serviceName, dataArg.serviceName,
dataArg.updates, dataArg.updates,
@@ -72,7 +72,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteService>(
'deleteService', 'deleteService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.services.removeService(dataArg.serviceName); await this.opsServerRef.oneboxRef.services.removeService(dataArg.serviceName);
return { ok: true }; return { ok: true };
}, },
@@ -84,7 +84,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartService>(
'startService', 'startService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.services.startService(dataArg.serviceName); await this.opsServerRef.oneboxRef.services.startService(dataArg.serviceName);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
return { service: service! }; return { service: service! };
@@ -97,7 +97,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StopService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StopService>(
'stopService', 'stopService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.services.stopService(dataArg.serviceName); await this.opsServerRef.oneboxRef.services.stopService(dataArg.serviceName);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
return { service: service! }; return { service: service! };
@@ -110,7 +110,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RestartService>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RestartService>(
'restartService', 'restartService',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.services.restartService(dataArg.serviceName); await this.opsServerRef.oneboxRef.services.restartService(dataArg.serviceName);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
return { service: service! }; return { service: service! };
@@ -123,7 +123,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceLogs>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceLogs>(
'getServiceLogs', 'getServiceLogs',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const logs = await this.opsServerRef.oneboxRef.services.getServiceLogs(dataArg.serviceName); const logs = await this.opsServerRef.oneboxRef.services.getServiceLogs(dataArg.serviceName);
return { logs: String(logs) }; return { logs: String(logs) };
}, },
@@ -135,7 +135,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceStats>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceStats>(
'getServiceStats', 'getServiceStats',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
if (!service || !service.containerID) { if (!service || !service.containerID) {
throw new plugins.typedrequest.TypedResponseError('Service has no container'); throw new plugins.typedrequest.TypedResponseError('Service has no container');
@@ -154,7 +154,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceMetrics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceMetrics>(
'getServiceMetrics', 'getServiceMetrics',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
if (!service || !service.id) { if (!service || !service.id) {
throw new plugins.typedrequest.TypedResponseError('Service not found'); throw new plugins.typedrequest.TypedResponseError('Service not found');
@@ -170,7 +170,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServicePlatformResources>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServicePlatformResources>(
'getServicePlatformResources', 'getServicePlatformResources',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const rawResources = await this.opsServerRef.oneboxRef.services.getServicePlatformResources( const rawResources = await this.opsServerRef.oneboxRef.services.getServicePlatformResources(
dataArg.serviceName, dataArg.serviceName,
); );
@@ -204,7 +204,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceBackups>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceBackups>(
'getServiceBackups', 'getServiceBackups',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backups = this.opsServerRef.oneboxRef.backupManager.listBackups(dataArg.serviceName); const backups = this.opsServerRef.oneboxRef.backupManager.listBackups(dataArg.serviceName);
return { backups }; return { backups };
}, },
@@ -216,7 +216,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateServiceBackup>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateServiceBackup>(
'createServiceBackup', 'createServiceBackup',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const result = await this.opsServerRef.oneboxRef.backupManager.createBackup(dataArg.serviceName); const result = await this.opsServerRef.oneboxRef.backupManager.createBackup(dataArg.serviceName);
return { backup: result.backup }; return { backup: result.backup };
}, },
@@ -228,7 +228,7 @@ export class ServicesHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceBackupSchedules>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServiceBackupSchedules>(
'getServiceBackupSchedules', 'getServiceBackupSchedules',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName); const service = this.opsServerRef.oneboxRef.services.getService(dataArg.serviceName);
if (!service) { if (!service) {
throw new plugins.typedrequest.TypedResponseError('Service not found'); throw new plugins.typedrequest.TypedResponseError('Service not found');
+14 -7
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class SettingsHandler { export class SettingsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class SettingsHandler {
const settingsMap = db.getAllSettings(); // Returns Record<string, string> const settingsMap = db.getAllSettings(); // Returns Record<string, string>
return { return {
cloudflareToken: settingsMap['cloudflareToken'] || '', cloudflareToken: settingsMap['cloudflareToken'] || settingsMap['cloudflareAPIKey'] || '',
cloudflareZoneId: settingsMap['cloudflareZoneId'] || '', cloudflareZoneId: settingsMap['cloudflareZoneId'] || '',
autoRenewCerts: settingsMap['autoRenewCerts'] === 'true', autoRenewCerts: settingsMap['autoRenewCerts'] === 'true',
renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10), renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10),
@@ -32,7 +32,7 @@ export class SettingsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSettings>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSettings>(
'getSettings', 'getSettings',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const settings = this.getSettingsObject(); const settings = this.getSettingsObject();
return { settings }; return { settings };
}, },
@@ -43,16 +43,21 @@ export class SettingsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSettings>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSettings>(
'updateSettings', 'updateSettings',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const db = this.opsServerRef.oneboxRef.database; const db = this.opsServerRef.oneboxRef.database;
const updates = dataArg.settings; const updates = dataArg.settings;
// Store each setting as key-value pair // Store each setting as key-value pair
for (const [key, value] of Object.entries(updates)) { for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) { if (value !== undefined) {
if (key === 'cloudflareToken') {
db.setSetting('cloudflareToken', String(value));
db.setSetting('cloudflareAPIKey', String(value));
} else {
db.setSetting(key, String(value)); db.setSetting(key, String(value));
} }
} }
}
const settings = this.getSettingsObject(); const settings = this.getSettingsObject();
return { settings }; return { settings };
@@ -64,7 +69,8 @@ export class SettingsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetBackupPassword>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetBackupPassword>(
'setBackupPassword', 'setBackupPassword',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
this.opsServerRef.oneboxRef.database.setSetting('backup_encryption_password', dataArg.password);
this.opsServerRef.oneboxRef.database.setSetting('backupPassword', dataArg.password); this.opsServerRef.oneboxRef.database.setSetting('backupPassword', dataArg.password);
return { ok: true }; return { ok: true };
}, },
@@ -75,8 +81,9 @@ export class SettingsHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupPasswordStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBackupPasswordStatus>(
'getBackupPasswordStatus', 'getBackupPasswordStatus',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backupPassword = this.opsServerRef.oneboxRef.database.getSetting('backupPassword'); const backupPassword = this.opsServerRef.oneboxRef.database.getSetting('backupPassword')
|| this.opsServerRef.oneboxRef.database.getSetting('backup_encryption_password');
const isConfigured = !!backupPassword; const isConfigured = !!backupPassword;
return { status: { isConfigured } }; return { status: { isConfigured } };
}, },
+5 -5
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class SslHandler { export class SslHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class SslHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ObtainCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ObtainCertificate>(
'obtainCertificate', 'obtainCertificate',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.ssl.obtainCertificate(dataArg.domain, false); await this.opsServerRef.oneboxRef.ssl.obtainCertificate(dataArg.domain, false);
const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain); const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain);
return { certificate: certificate as unknown as interfaces.data.ICertificate }; return { certificate: certificate as unknown as interfaces.data.ICertificate };
@@ -28,7 +28,7 @@ export class SslHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListCertificates>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListCertificates>(
'listCertificates', 'listCertificates',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const certificates = this.opsServerRef.oneboxRef.ssl.listCertificates(); const certificates = this.opsServerRef.oneboxRef.ssl.listCertificates();
return { certificates: certificates as unknown as interfaces.data.ICertificate[] }; return { certificates: certificates as unknown as interfaces.data.ICertificate[] };
}, },
@@ -39,7 +39,7 @@ export class SslHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificate>(
'getCertificate', 'getCertificate',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain); const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain);
if (!certificate) { if (!certificate) {
throw new plugins.typedrequest.TypedResponseError('Certificate not found'); throw new plugins.typedrequest.TypedResponseError('Certificate not found');
@@ -53,7 +53,7 @@ export class SslHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RenewCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RenewCertificate>(
'renewCertificate', 'renewCertificate',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.oneboxRef.ssl.renewCertificate(dataArg.domain); await this.opsServerRef.oneboxRef.ssl.renewCertificate(dataArg.domain);
const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain); const certificate = this.opsServerRef.oneboxRef.ssl.getCertificate(dataArg.domain);
return { certificate: certificate as unknown as interfaces.data.ICertificate }; return { certificate: certificate as unknown as interfaces.data.ICertificate };
+2 -2
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.ts'; import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
export class StatusHandler { export class StatusHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -16,7 +16,7 @@ export class StatusHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSystemStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSystemStatus>(
'getSystemStatus', 'getSystemStatus',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const status = await this.opsServerRef.oneboxRef.getSystemStatus(); const status = await this.opsServerRef.oneboxRef.getSystemStatus();
return { status: status as unknown as interfaces.data.ISystemStatus }; return { status: status as unknown as interfaces.data.ISystemStatus };
}, },
+8 -8
View File
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts'; import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts'; import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts'; import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts'; import { requireAdminIdentity } from '../helpers/guards.ts';
import { getErrorMessage } from '../../utils/error.ts'; import { getErrorMessage } from '../../utils/error.ts';
export class WorkspaceHandler { export class WorkspaceHandler {
@@ -30,7 +30,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadFile>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadFile>(
'workspaceReadFile', 'workspaceReadFile',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer( const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId, containerId,
@@ -49,7 +49,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceWriteFile>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceWriteFile>(
'workspaceWriteFile', 'workspaceWriteFile',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
// Use sh -c with printf to write content (handles special characters) // Use sh -c with printf to write content (handles special characters)
const escaped = dataArg.content.replace(/'/g, "'\\''"); const escaped = dataArg.content.replace(/'/g, "'\\''");
@@ -70,7 +70,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadDir>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadDir>(
'workspaceReadDir', 'workspaceReadDir',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
// Use ls with -1 -F to get entries with type indicators (/ for dirs) // Use ls with -1 -F to get entries with type indicators (/ for dirs)
const result = await this.opsServerRef.oneboxRef.docker.execInContainer( const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
@@ -103,7 +103,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceMkdir>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceMkdir>(
'workspaceMkdir', 'workspaceMkdir',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer( const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId, containerId,
@@ -122,7 +122,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceRm>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceRm>(
'workspaceRm', 'workspaceRm',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
const args = dataArg.recursive ? ['rm', '-rf', dataArg.path] : ['rm', '-f', dataArg.path]; const args = dataArg.recursive ? ['rm', '-rf', dataArg.path] : ['rm', '-f', dataArg.path];
const result = await this.opsServerRef.oneboxRef.docker.execInContainer( const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
@@ -142,7 +142,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExists>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExists>(
'workspaceExists', 'workspaceExists',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer( const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId, containerId,
@@ -158,7 +158,7 @@ export class WorkspaceHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExec>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExec>(
'workspaceExec', 'workspaceExec',
async (dataArg) => { async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName); const containerId = await this.resolveContainerId(dataArg.serviceName);
const cmd = dataArg.args const cmd = dataArg.args
? [dataArg.command, ...dataArg.args] ? [dataArg.command, ...dataArg.args]
+4 -16
View File
@@ -5,25 +5,13 @@ import * as interfaces from '../../../ts_interfaces/index.ts';
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>( export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>(
adminHandler: AdminHandler, adminHandler: AdminHandler,
dataArg: T, dataArg: T,
): Promise<void> { ): Promise<interfaces.data.IIdentity> {
if (!dataArg.identity) { return await adminHandler.getVerifiedIdentity(dataArg.identity);
throw new plugins.typedrequest.TypedResponseError('No identity provided');
}
const passed = await adminHandler.validIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
}
} }
export async function requireAdminIdentity<T extends { identity?: interfaces.data.IIdentity }>( export async function requireAdminIdentity<T extends { identity?: interfaces.data.IIdentity }>(
adminHandler: AdminHandler, adminHandler: AdminHandler,
dataArg: T, dataArg: T,
): Promise<void> { ): Promise<interfaces.data.IIdentity> {
if (!dataArg.identity) { return await adminHandler.getVerifiedAdminIdentity(dataArg.identity);
throw new plugins.typedrequest.TypedResponseError('No identity provided');
}
const passed = await adminHandler.adminIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
} }
+14
View File
@@ -37,6 +37,20 @@ export { smartregistry };
import * as smartstorage from '@push.rocks/smartstorage'; import * as smartstorage from '@push.rocks/smartstorage';
export { smartstorage }; export { smartstorage };
// AWS S3 client for S3-compatible object operations
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
PutObjectCommand,
} from 'npm:@aws-sdk/client-s3@3.1009.0';
export const awsS3 = {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
PutObjectCommand,
};
// Task scheduling and cron jobs // Task scheduling and cron jobs
import * as taskbuffer from '@push.rocks/taskbuffer'; import * as taskbuffer from '@push.rocks/taskbuffer';
export { taskbuffer }; export { taskbuffer };
+28
View File
@@ -0,0 +1,28 @@
import * as plugins from '../plugins.ts';
const bcryptHashPattern = /^\$2[abxy]\$\d\d\$/;
export function isBcryptHash(passwordHash: string): boolean {
return bcryptHashPattern.test(passwordHash);
}
export function needsPasswordUpgrade(passwordHash: string): boolean {
return !isBcryptHash(passwordHash);
}
export async function hashPassword(password: string): Promise<string> {
return await plugins.bcrypt.hash(password);
}
export async function verifyPassword(password: string, passwordHash: string): Promise<boolean> {
if (!passwordHash) {
return false;
}
if (isBcryptHash(passwordHash)) {
return await plugins.bcrypt.compare(password, passwordHash);
}
// Legacy compatibility for older databases that stored base64-encoded passwords.
return passwordHash === btoa(password);
}
+1 -1
View File
File diff suppressed because one or more lines are too long