This commit is contained in:
2026-02-24 12:29:58 +00:00
commit 3fad287a29
58 changed files with 3999 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import type { GitopsApp } from '../classes/gitopsapp.ts';
import * as handlers from './handlers/index.ts';
export class OpsServer {
public gitopsAppRef: GitopsApp;
public typedrouter = new plugins.typedrequest.TypedRouter();
public server!: plugins.typedserver.utilityservers.UtilityWebsiteServer;
// Handler instances
public adminHandler!: handlers.AdminHandler;
public connectionsHandler!: handlers.ConnectionsHandler;
public projectsHandler!: handlers.ProjectsHandler;
public groupsHandler!: handlers.GroupsHandler;
public secretsHandler!: handlers.SecretsHandler;
public pipelinesHandler!: handlers.PipelinesHandler;
public logsHandler!: handlers.LogsHandler;
constructor(gitopsAppRef: GitopsApp) {
this.gitopsAppRef = gitopsAppRef;
}
public async start(port = 3000) {
const absoluteServeDir = plugins.path.resolve('./dist_serve');
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
domain: 'localhost',
feedMetadata: undefined,
serveDir: absoluteServeDir,
});
// Chain typedrouters
this.server.typedrouter.addTypedRouter(this.typedrouter);
// Set up all handlers
await this.setupHandlers();
await this.server.start(port);
logger.success(`OpsServer started on http://localhost:${port}`);
}
private async setupHandlers(): Promise<void> {
// AdminHandler requires async initialization for JWT key generation
this.adminHandler = new handlers.AdminHandler(this);
await this.adminHandler.initialize();
// All other handlers self-register in their constructors
this.connectionsHandler = new handlers.ConnectionsHandler(this);
this.projectsHandler = new handlers.ProjectsHandler(this);
this.groupsHandler = new handlers.GroupsHandler(this);
this.secretsHandler = new handlers.SecretsHandler(this);
this.pipelinesHandler = new handlers.PipelinesHandler(this);
this.logsHandler = new handlers.LogsHandler(this);
logger.success('OpsServer TypedRequest handlers initialized');
}
public async stop() {
if (this.server) {
await this.server.stop();
logger.success('OpsServer stopped');
}
}
}

View File

@@ -0,0 +1,122 @@
import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
export interface IJwtData {
userId: string;
status: 'loggedIn' | 'loggedOut';
expiresAt: number;
}
export class AdminHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
}
public async initialize(): Promise<void> {
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
await this.smartjwtInstance.init();
await this.smartjwtInstance.createNewKeyPair();
this.registerHandlers();
}
private registerHandlers(): void {
// Login
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogin>(
'adminLogin',
async (dataArg) => {
const expectedUsername = Deno.env.get('GITOPS_ADMIN_USERNAME') || 'admin';
const expectedPassword = Deno.env.get('GITOPS_ADMIN_PASSWORD') || 'admin';
if (dataArg.username !== expectedUsername || dataArg.password !== expectedPassword) {
throw new plugins.typedrequest.TypedResponseError('Invalid credentials');
}
const expiresAt = Date.now() + 24 * 3600 * 1000;
const userId = 'admin';
const jwt = await this.smartjwtInstance.createJWT({
userId,
status: 'loggedIn',
expiresAt,
});
logger.info(`User logged in: ${dataArg.username}`);
return {
identity: {
jwt,
userId,
username: dataArg.username,
expiresAt,
role: 'admin' as const,
},
};
},
),
);
// Logout
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
'adminLogout',
async (_dataArg) => {
return { ok: true };
},
),
);
// Verify Identity
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
'verifyIdentity',
async (dataArg) => {
if (!dataArg.identity?.jwt) {
return { valid: false };
}
try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
if (jwtData.expiresAt < Date.now()) return { valid: false };
if (jwtData.status !== 'loggedIn') return { valid: false };
return {
valid: true,
identity: {
jwt: dataArg.identity.jwt,
userId: jwtData.userId,
username: dataArg.identity.username,
expiresAt: jwtData.expiresAt,
role: dataArg.identity.role,
},
};
} catch {
return { valid: false };
}
},
),
);
}
// Guard for valid identity
public validIdentityGuard = new plugins.smartguard.Guard<{
identity: interfaces.data.IIdentity;
}>(
async (dataArg) => {
if (!dataArg.identity?.jwt) return false;
try {
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
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;
} catch {
return false;
}
},
{ failedHint: 'identity is not valid', name: 'validIdentityGuard' },
);
}

View File

@@ -0,0 +1,91 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class ConnectionsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get all connections
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConnections>(
'getConnections',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const connections = this.opsServerRef.gitopsAppRef.connectionManager.getConnections();
return { connections };
},
),
);
// Create connection
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateConnection>(
'createConnection',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const connection = await this.opsServerRef.gitopsAppRef.connectionManager.createConnection(
dataArg.name,
dataArg.providerType,
dataArg.baseUrl,
dataArg.token,
);
return { connection };
},
),
);
// Update connection
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateConnection>(
'updateConnection',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const connection = await this.opsServerRef.gitopsAppRef.connectionManager.updateConnection(
dataArg.connectionId,
{
name: dataArg.name,
baseUrl: dataArg.baseUrl,
token: dataArg.token,
},
);
return { connection };
},
),
);
// Test connection
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestConnection>(
'testConnection',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const result = await this.opsServerRef.gitopsAppRef.connectionManager.testConnection(
dataArg.connectionId,
);
return result;
},
),
);
// Delete connection
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteConnection>(
'deleteConnection',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
await this.opsServerRef.gitopsAppRef.connectionManager.deleteConnection(
dataArg.connectionId,
);
return { ok: true };
},
),
);
}
}

View File

@@ -0,0 +1,32 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class GroupsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGroups>(
'getGroups',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const groups = await provider.getGroups({
search: dataArg.search,
page: dataArg.page,
});
return { groups };
},
),
);
}
}

View File

@@ -0,0 +1,7 @@
export { AdminHandler } from './admin.handler.ts';
export { ConnectionsHandler } from './connections.handler.ts';
export { ProjectsHandler } from './projects.handler.ts';
export { GroupsHandler } from './groups.handler.ts';
export { SecretsHandler } from './secrets.handler.ts';
export { PipelinesHandler } from './pipelines.handler.ts';
export { LogsHandler } from './logs.handler.ts';

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetJobLog>(
'getJobLog',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const log = await provider.getJobLog(dataArg.projectId, dataArg.jobId);
return { log };
},
),
);
}
}

View File

@@ -0,0 +1,77 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class PipelinesHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get pipelines
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPipelines>(
'getPipelines',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const pipelines = await provider.getPipelines(dataArg.projectId, {
page: dataArg.page,
});
return { pipelines };
},
),
);
// Get pipeline jobs
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPipelineJobs>(
'getPipelineJobs',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const jobs = await provider.getPipelineJobs(dataArg.projectId, dataArg.pipelineId);
return { jobs };
},
),
);
// Retry pipeline
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RetryPipeline>(
'retryPipeline',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
await provider.retryPipeline(dataArg.projectId, dataArg.pipelineId);
return { ok: true };
},
),
);
// Cancel pipeline
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CancelPipeline>(
'cancelPipeline',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
await provider.cancelPipeline(dataArg.projectId, dataArg.pipelineId);
return { ok: true };
},
),
);
}
}

View File

@@ -0,0 +1,32 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class ProjectsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetProjects>(
'getProjects',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const projects = await provider.getProjects({
search: dataArg.search,
page: dataArg.page,
});
return { projects };
},
),
);
}
}

View File

@@ -0,0 +1,85 @@
import * as plugins from '../../plugins.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
export class SecretsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get secrets
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecrets>(
'getSecrets',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const secrets = dataArg.scope === 'project'
? await provider.getProjectSecrets(dataArg.scopeId)
: await provider.getGroupSecrets(dataArg.scopeId);
return { secrets };
},
),
);
// Create secret
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecret>(
'createSecret',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const secret = dataArg.scope === 'project'
? await provider.createProjectSecret(dataArg.scopeId, dataArg.key, dataArg.value)
: await provider.createGroupSecret(dataArg.scopeId, dataArg.key, dataArg.value);
return { secret };
},
),
);
// Update secret
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecret>(
'updateSecret',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
const secret = dataArg.scope === 'project'
? await provider.updateProjectSecret(dataArg.scopeId, dataArg.key, dataArg.value)
: await provider.updateGroupSecret(dataArg.scopeId, dataArg.key, dataArg.value);
return { secret };
},
),
);
// Delete secret
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecret>(
'deleteSecret',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
dataArg.connectionId,
);
if (dataArg.scope === 'project') {
await provider.deleteProjectSecret(dataArg.scopeId, dataArg.key);
} else {
await provider.deleteGroupSecret(dataArg.scopeId, dataArg.key);
}
return { ok: true };
},
),
);
}
}

View File

@@ -0,0 +1,16 @@
import * as plugins from '../../plugins.ts';
import type { AdminHandler } from '../handlers/admin.handler.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>(
adminHandler: AdminHandler,
dataArg: T,
): Promise<void> {
if (!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');
}
}

1
ts/opsserver/index.ts Normal file
View File

@@ -0,0 +1 @@
export { OpsServer } from './classes.opsserver.ts';