feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend

This commit is contained in:
2026-03-20 16:43:44 +00:00
parent 0fc74ff995
commit d4f758ce0f
159 changed files with 12465 additions and 14861 deletions

View File

@@ -45,7 +45,7 @@ export class AuditService {
errorCode?: string;
errorMessage?: string;
durationMs?: number;
} = {}
} = {},
): Promise<AuditLog> {
return await AuditLog.log({
actorId: this.context.actorId,
@@ -75,7 +75,7 @@ export class AuditService {
resourceType: TAuditResourceType,
resourceId?: string,
resourceName?: string,
metadata?: Record<string, unknown>
metadata?: Record<string, unknown>,
): Promise<AuditLog> {
return await this.log(action, resourceType, {
resourceId,
@@ -94,7 +94,7 @@ export class AuditService {
errorCode: string,
errorMessage: string,
resourceId?: string,
metadata?: Record<string, unknown>
metadata?: Record<string, unknown>,
): Promise<AuditLog> {
return await this.log(action, resourceType, {
resourceId,
@@ -107,11 +107,21 @@ export class AuditService {
// Convenience methods for common actions
public async logUserLogin(userId: string, success: boolean, errorMessage?: string): Promise<AuditLog> {
public async logUserLogin(
userId: string,
success: boolean,
errorMessage?: string,
): Promise<AuditLog> {
if (success) {
return await this.logSuccess('AUTH_LOGIN', 'user', userId);
}
return await this.logFailure('AUTH_LOGIN', 'user', 'LOGIN_FAILED', errorMessage || 'Login failed', userId);
return await this.logFailure(
'AUTH_LOGIN',
'user',
'LOGIN_FAILED',
errorMessage || 'Login failed',
userId,
);
}
public async logUserLogout(userId: string): Promise<AuditLog> {
@@ -131,7 +141,7 @@ export class AuditService {
packageName: string,
version: string,
organizationId: string,
repositoryId: string
repositoryId: string,
): Promise<AuditLog> {
return await this.log('PACKAGE_PUSHED', 'package', {
resourceId: packageId,
@@ -148,7 +158,7 @@ export class AuditService {
packageName: string,
version: string,
organizationId: string,
repositoryId: string
repositoryId: string,
): Promise<AuditLog> {
return await this.log('PACKAGE_PULLED', 'package', {
resourceId: packageId,
@@ -167,7 +177,7 @@ export class AuditService {
public async logRepositoryCreated(
repoId: string,
repoName: string,
organizationId: string
organizationId: string,
): Promise<AuditLog> {
return await this.log('REPO_CREATED', 'repository', {
resourceId: repoId,
@@ -182,7 +192,7 @@ export class AuditService {
resourceId: string,
targetUserId: string,
oldRole: string | null,
newRole: string | null
newRole: string | null,
): Promise<AuditLog> {
return await this.log('ORG_MEMBER_ROLE_CHANGED', resourceType, {
resourceId,

View File

@@ -3,7 +3,7 @@
*/
import * as plugins from '../plugins.ts';
import { User, Session } from '../models/index.ts';
import { Session, User } from '../models/index.ts';
import { AuditService } from './audit.service.ts';
export interface IJwtPayload {
@@ -52,7 +52,7 @@ export class AuthService {
public async login(
email: string,
password: string,
options: { userAgent?: string; ipAddress?: string } = {}
options: { userAgent?: string; ipAddress?: string } = {},
): Promise<IAuthResult> {
const auditContext = AuditService.withContext({
actorIp: options.ipAddress,
@@ -195,7 +195,7 @@ export class AuthService {
*/
public async logout(
sessionId: string,
options: { userId?: string; ipAddress?: string } = {}
options: { userId?: string; ipAddress?: string } = {},
): Promise<boolean> {
const session = await Session.findValidSession(sessionId);
if (!session) return false;
@@ -218,7 +218,7 @@ export class AuthService {
*/
public async logoutAll(
userId: string,
options: { ipAddress?: string } = {}
options: { ipAddress?: string } = {},
): Promise<number> {
const count = await Session.invalidateAllUserSessions(userId, 'logout_all');
@@ -238,7 +238,9 @@ export class AuthService {
/**
* Validate access token and return user
*/
public async validateAccessToken(accessToken: string): Promise<{ user: User; sessionId: string } | null> {
public async validateAccessToken(
accessToken: string,
): Promise<{ user: User; sessionId: string } | null> {
const payload = await this.verifyToken(accessToken);
if (!payload || payload.type !== 'access') return null;
@@ -339,7 +341,7 @@ export class AuthService {
encoder.encode(this.config.jwtSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));

View File

@@ -4,8 +4,8 @@
*/
import type {
IExternalUserInfo,
IConnectionTestResult,
IExternalUserInfo,
} from '../../../interfaces/auth.interfaces.ts';
export interface IOAuthCallbackData {

View File

@@ -9,8 +9,8 @@
import type { AuthProvider } from '../../../models/auth.provider.ts';
import type { CryptoService } from '../../crypto.service.ts';
import type {
IExternalUserInfo,
IConnectionTestResult,
IExternalUserInfo,
} from '../../../interfaces/auth.interfaces.ts';
import type { IAuthStrategy } from './auth.strategy.interface.ts';
@@ -23,7 +23,7 @@ interface ILdapEntry {
export class LdapStrategy implements IAuthStrategy {
constructor(
private provider: AuthProvider,
private cryptoService: CryptoService
private cryptoService: CryptoService,
) {}
/**
@@ -31,7 +31,7 @@ export class LdapStrategy implements IAuthStrategy {
*/
public async authenticateCredentials(
username: string,
password: string
password: string,
): Promise<IExternalUserInfo> {
const config = this.provider.ldapConfig;
if (!config) {
@@ -55,7 +55,7 @@ export class LdapStrategy implements IAuthStrategy {
bindPassword,
config.baseDn,
userFilter,
password
password,
);
// Map LDAP attributes to user info
@@ -86,7 +86,7 @@ export class LdapStrategy implements IAuthStrategy {
config.serverUrl,
config.bindDn,
bindPassword,
config.baseDn
config.baseDn,
);
return {
@@ -129,7 +129,7 @@ export class LdapStrategy implements IAuthStrategy {
bindPassword: string,
baseDn: string,
userFilter: string,
userPassword: string
userPassword: string,
): Promise<ILdapEntry> {
// In a real implementation, this would:
// 1. Connect to LDAP server
@@ -150,7 +150,7 @@ export class LdapStrategy implements IAuthStrategy {
throw new Error(
'LDAP authentication is not yet fully implemented. ' +
'Please integrate with a Deno-compatible LDAP library (e.g., ldapts via npm compatibility).'
'Please integrate with a Deno-compatible LDAP library (e.g., ldapts via npm compatibility).',
);
}
@@ -161,7 +161,7 @@ export class LdapStrategy implements IAuthStrategy {
serverUrl: string,
bindDn: string,
bindPassword: string,
baseDn: string
baseDn: string,
): Promise<void> {
// Similar to ldapBind, this is a placeholder
// Would connect and bind with service account to verify connectivity
@@ -185,7 +185,9 @@ export class LdapStrategy implements IAuthStrategy {
// Return success for configuration validation
// Actual connectivity test would happen with LDAP library
console.log('[LdapStrategy] LDAP configuration is valid (actual connection test requires LDAP library)');
console.log(
'[LdapStrategy] LDAP configuration is valid (actual connection test requires LDAP library)',
);
}
/**
@@ -206,15 +208,9 @@ export class LdapStrategy implements IAuthStrategy {
return {
externalId,
email,
username: entry[mapping.username]
? String(entry[mapping.username])
: undefined,
displayName: entry[mapping.displayName]
? String(entry[mapping.displayName])
: undefined,
groups: mapping.groups
? this.parseGroups(entry[mapping.groups])
: undefined,
username: entry[mapping.username] ? String(entry[mapping.username]) : undefined,
displayName: entry[mapping.displayName] ? String(entry[mapping.displayName]) : undefined,
groups: mapping.groups ? this.parseGroups(entry[mapping.groups]) : undefined,
rawAttributes: entry as Record<string, unknown>,
};
}

View File

@@ -6,8 +6,8 @@
import type { AuthProvider } from '../../../models/auth.provider.ts';
import type { CryptoService } from '../../crypto.service.ts';
import type {
IExternalUserInfo,
IConnectionTestResult,
IExternalUserInfo,
} from '../../../interfaces/auth.interfaces.ts';
import type { IAuthStrategy, IOAuthCallbackData } from './auth.strategy.interface.ts';
@@ -34,7 +34,7 @@ export class OAuthStrategy implements IAuthStrategy {
constructor(
private provider: AuthProvider,
private cryptoService: CryptoService
private cryptoService: CryptoService,
) {}
/**
@@ -243,19 +243,15 @@ export class OAuthStrategy implements IAuthStrategy {
return {
externalId,
email,
username: rawInfo[mapping.username]
? String(rawInfo[mapping.username])
: undefined,
displayName: rawInfo[mapping.displayName]
? String(rawInfo[mapping.displayName])
: undefined,
username: rawInfo[mapping.username] ? String(rawInfo[mapping.username]) : undefined,
displayName: rawInfo[mapping.displayName] ? String(rawInfo[mapping.displayName]) : undefined,
avatarUrl: mapping.avatarUrl && rawInfo[mapping.avatarUrl]
? String(rawInfo[mapping.avatarUrl])
: (rawInfo.picture ? String(rawInfo.picture) : undefined),
groups: mapping.groups && rawInfo[mapping.groups]
? (Array.isArray(rawInfo[mapping.groups])
? (rawInfo[mapping.groups] as string[])
: [String(rawInfo[mapping.groups])])
? (rawInfo[mapping.groups] as string[])
: [String(rawInfo[mapping.groups])])
: undefined,
rawAttributes: rawInfo,
};

View File

@@ -17,7 +17,7 @@ export class CryptoService {
const keyHex = Deno.env.get('AUTH_ENCRYPTION_KEY');
if (!keyHex) {
console.warn(
'[CryptoService] AUTH_ENCRYPTION_KEY not set. Generating ephemeral key (NOT for production!)'
'[CryptoService] AUTH_ENCRYPTION_KEY not set. Generating ephemeral key (NOT for production!)',
);
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
this.masterKey = await this.importKey(this.bytesToHex(randomBytes));
@@ -52,7 +52,7 @@ export class CryptoService {
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv.buffer as ArrayBuffer },
this.masterKey,
encoded.buffer as ArrayBuffer
encoded.buffer as ArrayBuffer,
);
// Format: iv:ciphertext (both base64)
@@ -88,7 +88,7 @@ export class CryptoService {
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv.buffer as ArrayBuffer },
this.masterKey,
encrypted.buffer as ArrayBuffer
encrypted.buffer as ArrayBuffer,
);
// Decode to string
@@ -123,7 +123,7 @@ export class CryptoService {
keyBytes.buffer as ArrayBuffer,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
['encrypt', 'decrypt'],
);
}

View File

@@ -3,12 +3,18 @@
* Orchestrates OAuth/OIDC and LDAP authentication flows
*/
import { User, Session, AuthProvider, ExternalIdentity, PlatformSettings } from '../models/index.ts';
import {
AuthProvider,
ExternalIdentity,
PlatformSettings,
Session,
User,
} from '../models/index.ts';
import { AuthService, type IAuthResult } from './auth.service.ts';
import { AuditService } from './audit.service.ts';
import { cryptoService } from './crypto.service.ts';
import { AuthStrategyFactory, type IOAuthCallbackData } from './auth/strategies/index.ts';
import type { IExternalUserInfo, IConnectionTestResult } from '../interfaces/auth.interfaces.ts';
import type { IConnectionTestResult, IExternalUserInfo } from '../interfaces/auth.interfaces.ts';
export interface IOAuthState {
providerId: string;
@@ -33,7 +39,7 @@ export class ExternalAuthService {
*/
public async initiateOAuth(
providerId: string,
returnUrl?: string
returnUrl?: string,
): Promise<{ authUrl: string; state: string }> {
const provider = await AuthProvider.findById(providerId);
if (!provider) {
@@ -67,7 +73,7 @@ export class ExternalAuthService {
*/
public async handleOAuthCallback(
data: IOAuthCallbackData,
options: { ipAddress?: string; userAgent?: string } = {}
options: { ipAddress?: string; userAgent?: string } = {},
): Promise<IAuthResult> {
// Validate state
const stateData = await this.validateState(data.state);
@@ -170,7 +176,7 @@ export class ExternalAuthService {
providerId: string,
username: string,
password: string,
options: { ipAddress?: string; userAgent?: string } = {}
options: { ipAddress?: string; userAgent?: string } = {},
): Promise<IAuthResult> {
const provider = await AuthProvider.findById(providerId);
if (!provider || provider.status !== 'active' || provider.type !== 'ldap') {
@@ -261,7 +267,7 @@ export class ExternalAuthService {
public async linkProvider(
userId: string,
providerId: string,
externalUser: IExternalUserInfo
externalUser: IExternalUserInfo,
): Promise<ExternalIdentity> {
// Check if this external ID is already linked to another user
const existing = await ExternalIdentity.findByExternalId(providerId, externalUser.externalId);
@@ -377,12 +383,12 @@ export class ExternalAuthService {
private async findOrCreateUser(
provider: AuthProvider,
externalUser: IExternalUserInfo,
options: { ipAddress?: string } = {}
options: { ipAddress?: string } = {},
): Promise<{ user: User; isNew: boolean }> {
// 1. Check if external identity already exists
const existingIdentity = await ExternalIdentity.findByExternalId(
provider.id,
externalUser.externalId
externalUser.externalId,
);
if (existingIdentity) {
@@ -544,12 +550,12 @@ export class ExternalAuthService {
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
const encodedSignature = this.base64UrlEncode(
String.fromCharCode(...new Uint8Array(signature))
String.fromCharCode(...new Uint8Array(signature)),
);
return `${data}.${encodedSignature}`;

View File

@@ -4,19 +4,19 @@
export { AuditService, type IAuditContext } from './audit.service.ts';
export {
TokenService,
type ICreateTokenOptions,
type ITokenValidationResult,
TokenService,
} from './token.service.ts';
export {
PermissionService,
type TAction,
type IPermissionContext,
type IResolvedPermissions,
PermissionService,
type TAction,
} from './permission.service.ts';
export {
AuthService,
type IJwtPayload,
type IAuthResult,
type IAuthConfig,
type IAuthResult,
type IJwtPayload,
} from './auth.service.ts';

View File

@@ -4,18 +4,18 @@
import type {
TOrganizationRole,
TTeamRole,
TRepositoryRole,
TRegistryProtocol,
TRepositoryRole,
TTeamRole,
} from '../interfaces/auth.interfaces.ts';
import {
User,
Organization,
OrganizationMember,
Team,
TeamMember,
Repository,
RepositoryPermission,
Team,
TeamMember,
User,
} from '../models/index.ts';
export type TAction = 'read' | 'write' | 'delete' | 'admin';
@@ -71,7 +71,10 @@ export class PermissionService {
if (!context.organizationId) return result;
// Get organization membership
const orgMember = await OrganizationMember.findMembership(context.organizationId, context.userId);
const orgMember = await OrganizationMember.findMembership(
context.organizationId,
context.userId,
);
if (orgMember) {
result.organizationRole = orgMember.role;
@@ -137,7 +140,10 @@ export class PermissionService {
}
// Get direct repository permission (highest priority)
const repoPerm = await RepositoryPermission.findPermission(context.repositoryId, context.userId);
const repoPerm = await RepositoryPermission.findPermission(
context.repositoryId,
context.userId,
);
if (repoPerm) {
result.repositoryRole = repoPerm.role;
this.applyRole(result, repoPerm.role);
@@ -151,7 +157,7 @@ export class PermissionService {
*/
public async checkPermission(
context: IPermissionContext,
action: TAction
action: TAction,
): Promise<boolean> {
const permissions = await this.resolvePermissions(context);
@@ -176,11 +182,11 @@ export class PermissionService {
userId: string,
organizationId: string,
repositoryId: string,
action: 'read' | 'write' | 'delete'
action: 'read' | 'write' | 'delete',
): Promise<boolean> {
return await this.checkPermission(
{ userId, organizationId, repositoryId },
action
action,
);
}
@@ -202,7 +208,7 @@ export class PermissionService {
public async canManageRepository(
userId: string,
organizationId: string,
repositoryId: string
repositoryId: string,
): Promise<boolean> {
const permissions = await this.resolvePermissions({
userId,
@@ -217,7 +223,7 @@ export class PermissionService {
*/
public async getAccessibleRepositories(
userId: string,
organizationId: string
organizationId: string,
): Promise<Repository[]> {
const user = await User.findById(userId);
if (!user || !user.isActive) return [];

View File

@@ -37,7 +37,9 @@ export class TokenService {
* Generate a new API token
* Returns the raw token (only shown once) and the saved token record
*/
public async createToken(options: ICreateTokenOptions): Promise<{ rawToken: string; token: ApiToken }> {
public async createToken(
options: ICreateTokenOptions,
): Promise<{ rawToken: string; token: ApiToken }> {
// Generate secure random token: srg_{64 hex chars}
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
@@ -206,7 +208,7 @@ export class TokenService {
protocol: TRegistryProtocol,
organizationId?: string,
repositoryId?: string,
action?: string
action?: string,
): boolean {
if (!token.hasProtocol(protocol)) return false;
return token.hasScope(protocol, organizationId, repositoryId, action);