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

@@ -113,7 +113,9 @@ export class AdminAuthApi {
});
} else if (body.type === 'ldap' && body.ldapConfig) {
// Encrypt bind password
const encryptedPassword = await cryptoService.encrypt(body.ldapConfig.bindPasswordEncrypted);
const encryptedPassword = await cryptoService.encrypt(
body.ldapConfig.bindPasswordEncrypted,
);
provider = await AuthProvider.createLdapProvider({
name: body.name,
@@ -228,7 +230,7 @@ export class AdminAuthApi {
!cryptoService.isEncrypted(body.oauthConfig.clientSecretEncrypted)
) {
newOAuthConfig.clientSecretEncrypted = await cryptoService.encrypt(
body.oauthConfig.clientSecretEncrypted
body.oauthConfig.clientSecretEncrypted,
);
}
@@ -245,7 +247,7 @@ export class AdminAuthApi {
!cryptoService.isEncrypted(body.ldapConfig.bindPasswordEncrypted)
) {
newLdapConfig.bindPasswordEncrypted = await cryptoService.encrypt(
body.ldapConfig.bindPasswordEncrypted
body.ldapConfig.bindPasswordEncrypted,
);
}

View File

@@ -26,7 +26,9 @@ export class AuditApi {
// Parse query parameters
const organizationId = ctx.url.searchParams.get('organizationId') || undefined;
const repositoryId = ctx.url.searchParams.get('repositoryId') || undefined;
const resourceType = ctx.url.searchParams.get('resourceType') as TAuditResourceType | undefined;
const resourceType = ctx.url.searchParams.get('resourceType') as
| TAuditResourceType
| undefined;
const actionsParam = ctx.url.searchParams.get('actions');
const actions = actionsParam ? (actionsParam.split(',') as TAuditAction[]) : undefined;
const success = ctx.url.searchParams.has('success')
@@ -54,7 +56,7 @@ export class AuditApi {
// Check if user can manage this org
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
organizationId
organizationId,
);
if (!canManage) {
// User can only see their own actions in this org

View File

@@ -93,7 +93,7 @@ export class OAuthApi {
const result = await externalAuthService.handleOAuthCallback(
{ code, state },
{ ipAddress: ctx.ip, userAgent: ctx.userAgent }
{ ipAddress: ctx.ip, userAgent: ctx.userAgent },
);
if (!result.success) {

View File

@@ -208,7 +208,10 @@ export class OrganizationApi {
}
// Check admin permission using org.id
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
org.id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
@@ -319,13 +322,13 @@ export class OrganizationApi {
addedAt: m.joinedAt,
user: user
? {
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
}
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
}
: null,
};
})
}),
);
return {
@@ -356,7 +359,10 @@ export class OrganizationApi {
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
org.id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
@@ -431,7 +437,10 @@ export class OrganizationApi {
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
org.id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
@@ -492,7 +501,10 @@ export class OrganizationApi {
// Users can remove themselves, admins can remove others
if (userId !== ctx.actor.userId) {
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
org.id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}

View File

@@ -50,7 +50,7 @@ export class PackageApi {
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
'read',
);
if (canAccess) {
accessiblePackages.push(pkg);
@@ -106,7 +106,7 @@ export class PackageApi {
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
'read',
);
if (!canAccess) {
@@ -161,7 +161,7 @@ export class PackageApi {
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
'read',
);
if (!canAccess) {
@@ -213,7 +213,7 @@ export class PackageApi {
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'delete'
'delete',
);
if (!canDelete) {
@@ -267,7 +267,7 @@ export class PackageApi {
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'delete'
'delete',
);
if (!canDelete) {

View File

@@ -5,7 +5,7 @@
import type { IApiContext, IApiResponse } from '../router.ts';
import { PermissionService } from '../../services/permission.service.ts';
import { AuditService } from '../../services/audit.service.ts';
import { Repository, Organization } from '../../models/index.ts';
import { Organization, Repository } from '../../models/index.ts';
import type { TRegistryProtocol, TRepositoryVisibility } from '../../interfaces/auth.interfaces.ts';
export class RepositoryApi {
@@ -28,7 +28,7 @@ export class RepositoryApi {
try {
const repositories = await this.permissionService.getAccessibleRepositories(
ctx.actor.userId,
orgId
orgId,
);
return {
@@ -131,7 +131,10 @@ export class RepositoryApi {
if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(name)) {
return {
status: 400,
body: { error: 'Name must be lowercase alphanumeric with optional dots, hyphens, or underscores' },
body: {
error:
'Name must be lowercase alphanumeric with optional dots, hyphens, or underscores',
},
};
}
@@ -198,7 +201,7 @@ export class RepositoryApi {
const canManage = await this.permissionService.canManageRepository(
ctx.actor.userId,
repo.organizationId,
id
id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
@@ -252,7 +255,7 @@ export class RepositoryApi {
const canManage = await this.permissionService.canManageRepository(
ctx.actor.userId,
repo.organizationId,
id
id,
);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };

View File

@@ -33,7 +33,10 @@ export class TokenApi {
let tokens;
if (organizationId) {
// Check if user can manage org
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, organizationId);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
organizationId,
);
if (!canManage) {
return { status: 403, body: { error: 'Not authorized to view organization tokens' } };
}
@@ -119,7 +122,10 @@ export class TokenApi {
// If creating org token, verify permission
if (organizationId) {
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, organizationId);
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
organizationId,
);
if (!canManage) {
return { status: 403, body: { error: 'Not authorized to create organization tokens' } };
}
@@ -181,7 +187,7 @@ export class TokenApi {
if (anyToken?.organizationId) {
const canManage = await this.permissionService.canManageOrganization(
ctx.actor.userId,
anyToken.organizationId
anyToken.organizationId,
);
if (canManage) {
token = anyToken;

View File

@@ -104,24 +104,56 @@ export class ApiRouter {
this.addRoute('POST', '/api/v1/organizations', (ctx) => this.organizationApi.create(ctx));
this.addRoute('PUT', '/api/v1/organizations/:id', (ctx) => this.organizationApi.update(ctx));
this.addRoute('DELETE', '/api/v1/organizations/:id', (ctx) => this.organizationApi.delete(ctx));
this.addRoute('GET', '/api/v1/organizations/:id/members', (ctx) => this.organizationApi.listMembers(ctx));
this.addRoute('POST', '/api/v1/organizations/:id/members', (ctx) => this.organizationApi.addMember(ctx));
this.addRoute('PUT', '/api/v1/organizations/:id/members/:userId', (ctx) => this.organizationApi.updateMember(ctx));
this.addRoute('DELETE', '/api/v1/organizations/:id/members/:userId', (ctx) => this.organizationApi.removeMember(ctx));
this.addRoute(
'GET',
'/api/v1/organizations/:id/members',
(ctx) => this.organizationApi.listMembers(ctx),
);
this.addRoute(
'POST',
'/api/v1/organizations/:id/members',
(ctx) => this.organizationApi.addMember(ctx),
);
this.addRoute(
'PUT',
'/api/v1/organizations/:id/members/:userId',
(ctx) => this.organizationApi.updateMember(ctx),
);
this.addRoute(
'DELETE',
'/api/v1/organizations/:id/members/:userId',
(ctx) => this.organizationApi.removeMember(ctx),
);
// Repository routes
this.addRoute('GET', '/api/v1/organizations/:orgId/repositories', (ctx) => this.repositoryApi.list(ctx));
this.addRoute(
'GET',
'/api/v1/organizations/:orgId/repositories',
(ctx) => this.repositoryApi.list(ctx),
);
this.addRoute('GET', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.get(ctx));
this.addRoute('POST', '/api/v1/organizations/:orgId/repositories', (ctx) => this.repositoryApi.create(ctx));
this.addRoute(
'POST',
'/api/v1/organizations/:orgId/repositories',
(ctx) => this.repositoryApi.create(ctx),
);
this.addRoute('PUT', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.update(ctx));
this.addRoute('DELETE', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.delete(ctx));
// Package routes
this.addRoute('GET', '/api/v1/packages', (ctx) => this.packageApi.search(ctx));
this.addRoute('GET', '/api/v1/packages/:id', (ctx) => this.packageApi.get(ctx));
this.addRoute('GET', '/api/v1/packages/:id/versions', (ctx) => this.packageApi.listVersions(ctx));
this.addRoute(
'GET',
'/api/v1/packages/:id/versions',
(ctx) => this.packageApi.listVersions(ctx),
);
this.addRoute('DELETE', '/api/v1/packages/:id', (ctx) => this.packageApi.delete(ctx));
this.addRoute('DELETE', '/api/v1/packages/:id/versions/:version', (ctx) => this.packageApi.deleteVersion(ctx));
this.addRoute(
'DELETE',
'/api/v1/packages/:id/versions/:version',
(ctx) => this.packageApi.deleteVersion(ctx),
);
// Token routes
this.addRoute('GET', '/api/v1/tokens', (ctx) => this.tokenApi.list(ctx));
@@ -138,14 +170,46 @@ export class ApiRouter {
this.addRoute('POST', '/api/v1/auth/ldap/:id/login', (ctx) => this.oauthApi.ldapLogin(ctx));
// Admin auth routes (platform admin only)
this.addRoute('GET', '/api/v1/admin/auth/providers', (ctx) => this.adminAuthApi.listProviders(ctx));
this.addRoute('POST', '/api/v1/admin/auth/providers', (ctx) => this.adminAuthApi.createProvider(ctx));
this.addRoute('GET', '/api/v1/admin/auth/providers/:id', (ctx) => this.adminAuthApi.getProvider(ctx));
this.addRoute('PUT', '/api/v1/admin/auth/providers/:id', (ctx) => this.adminAuthApi.updateProvider(ctx));
this.addRoute('DELETE', '/api/v1/admin/auth/providers/:id', (ctx) => this.adminAuthApi.deleteProvider(ctx));
this.addRoute('POST', '/api/v1/admin/auth/providers/:id/test', (ctx) => this.adminAuthApi.testProvider(ctx));
this.addRoute('GET', '/api/v1/admin/auth/settings', (ctx) => this.adminAuthApi.getSettings(ctx));
this.addRoute('PUT', '/api/v1/admin/auth/settings', (ctx) => this.adminAuthApi.updateSettings(ctx));
this.addRoute(
'GET',
'/api/v1/admin/auth/providers',
(ctx) => this.adminAuthApi.listProviders(ctx),
);
this.addRoute(
'POST',
'/api/v1/admin/auth/providers',
(ctx) => this.adminAuthApi.createProvider(ctx),
);
this.addRoute(
'GET',
'/api/v1/admin/auth/providers/:id',
(ctx) => this.adminAuthApi.getProvider(ctx),
);
this.addRoute(
'PUT',
'/api/v1/admin/auth/providers/:id',
(ctx) => this.adminAuthApi.updateProvider(ctx),
);
this.addRoute(
'DELETE',
'/api/v1/admin/auth/providers/:id',
(ctx) => this.adminAuthApi.deleteProvider(ctx),
);
this.addRoute(
'POST',
'/api/v1/admin/auth/providers/:id/test',
(ctx) => this.adminAuthApi.testProvider(ctx),
);
this.addRoute(
'GET',
'/api/v1/admin/auth/settings',
(ctx) => this.adminAuthApi.getSettings(ctx),
);
this.addRoute(
'PUT',
'/api/v1/admin/auth/settings',
(ctx) => this.adminAuthApi.updateSettings(ctx),
);
}
/**