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

@@ -0,0 +1,51 @@
import * as plugins from '../plugins.ts';
import type { StackGalleryRegistry } from '../registry.ts';
import * as handlers from './handlers/index.ts';
export class OpsServer {
public registryRef: StackGalleryRegistry;
public typedrouter = new plugins.typedrequest.TypedRouter();
// Handler instances
public authHandler!: handlers.AuthHandler;
public organizationHandler!: handlers.OrganizationHandler;
public repositoryHandler!: handlers.RepositoryHandler;
public packageHandler!: handlers.PackageHandler;
public tokenHandler!: handlers.TokenHandler;
public auditHandler!: handlers.AuditHandler;
public adminHandler!: handlers.AdminHandler;
public oauthHandler!: handlers.OAuthHandler;
public userHandler!: handlers.UserHandler;
constructor(registryRef: StackGalleryRegistry) {
this.registryRef = registryRef;
}
/**
* Initialize all handlers. Must be called before routing requests.
*/
public async start(): Promise<void> {
// AuthHandler must be initialized first (other handlers depend on its guards)
this.authHandler = new handlers.AuthHandler(this);
await this.authHandler.initialize();
// All other handlers self-register in their constructors
this.organizationHandler = new handlers.OrganizationHandler(this);
this.repositoryHandler = new handlers.RepositoryHandler(this);
this.packageHandler = new handlers.PackageHandler(this);
this.tokenHandler = new handlers.TokenHandler(this);
this.auditHandler = new handlers.AuditHandler(this);
this.adminHandler = new handlers.AdminHandler(this);
this.oauthHandler = new handlers.OAuthHandler(this);
this.userHandler = new handlers.UserHandler(this);
console.log('[OpsServer] TypedRequest handlers initialized');
}
/**
* Cleanup resources
*/
public async stop(): Promise<void> {
console.log('[OpsServer] Stopped');
}
}

View File

@@ -0,0 +1,380 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireAdminIdentity } from '../helpers/guards.ts';
import { AuthProvider, PlatformSettings } from '../../models/index.ts';
import { cryptoService } from '../../services/crypto.service.ts';
import { externalAuthService } from '../../services/external.auth.service.ts';
import { AuditService } from '../../services/audit.service.ts';
export class AdminHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Admin Providers
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAdminProviders>(
'getAdminProviders',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const providers = await AuthProvider.getAllProviders();
return {
providers: providers.map((p) => p.toAdminInfo()),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list providers');
}
},
),
);
// Create Admin Provider
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateAdminProvider>(
'createAdminProvider',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { name, displayName, type, oauthConfig, ldapConfig, attributeMapping, provisioning } = dataArg;
// Validate required fields
if (!name || !displayName || !type) {
throw new plugins.typedrequest.TypedResponseError(
'name, displayName, and type are required',
);
}
// Check name uniqueness
const existing = await AuthProvider.findByName(name);
if (existing) {
throw new plugins.typedrequest.TypedResponseError('Provider name already exists');
}
// Validate type-specific config
if (type === 'oidc' && !oauthConfig) {
throw new plugins.typedrequest.TypedResponseError(
'oauthConfig is required for OIDC provider',
);
}
if (type === 'ldap' && !ldapConfig) {
throw new plugins.typedrequest.TypedResponseError(
'ldapConfig is required for LDAP provider',
);
}
let provider: AuthProvider;
if (type === 'oidc' && oauthConfig) {
// Encrypt client secret
const encryptedSecret = await cryptoService.encrypt(
oauthConfig.clientSecretEncrypted,
);
provider = await AuthProvider.createOAuthProvider({
name,
displayName,
oauthConfig: {
...oauthConfig,
clientSecretEncrypted: encryptedSecret,
},
attributeMapping,
provisioning,
createdById: dataArg.identity.userId,
});
} else if (type === 'ldap' && ldapConfig) {
// Encrypt bind password
const encryptedPassword = await cryptoService.encrypt(
ldapConfig.bindPasswordEncrypted,
);
provider = await AuthProvider.createLdapProvider({
name,
displayName,
ldapConfig: {
...ldapConfig,
bindPasswordEncrypted: encryptedPassword,
},
attributeMapping,
provisioning,
createdById: dataArg.identity.userId,
});
} else {
throw new plugins.typedrequest.TypedResponseError('Invalid provider type');
}
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).log('AUTH_PROVIDER_CREATED', 'auth_provider', {
resourceId: provider.id,
success: true,
metadata: {
providerName: provider.name,
providerType: provider.type,
},
});
return { provider: provider.toAdminInfo() };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to create provider');
}
},
),
);
// Get Admin Provider
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAdminProvider>(
'getAdminProvider',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const provider = await AuthProvider.findById(dataArg.providerId);
if (!provider) {
throw new plugins.typedrequest.TypedResponseError('Provider not found');
}
return { provider: provider.toAdminInfo() };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get provider');
}
},
),
);
// Update Admin Provider
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateAdminProvider>(
'updateAdminProvider',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const provider = await AuthProvider.findById(dataArg.providerId);
if (!provider) {
throw new plugins.typedrequest.TypedResponseError('Provider not found');
}
// Update basic fields
if (dataArg.displayName !== undefined) provider.displayName = dataArg.displayName;
if (dataArg.status !== undefined) provider.status = dataArg.status as any;
if (dataArg.priority !== undefined) provider.priority = dataArg.priority;
// Update OAuth config
if (dataArg.oauthConfig && provider.oauthConfig) {
const newOAuthConfig = { ...provider.oauthConfig, ...dataArg.oauthConfig };
// Encrypt new client secret if provided and not already encrypted
if (
dataArg.oauthConfig.clientSecretEncrypted &&
!cryptoService.isEncrypted(dataArg.oauthConfig.clientSecretEncrypted)
) {
newOAuthConfig.clientSecretEncrypted = await cryptoService.encrypt(
dataArg.oauthConfig.clientSecretEncrypted,
);
}
provider.oauthConfig = newOAuthConfig;
}
// Update LDAP config
if (dataArg.ldapConfig && provider.ldapConfig) {
const newLdapConfig = { ...provider.ldapConfig, ...dataArg.ldapConfig };
// Encrypt new bind password if provided and not already encrypted
if (
dataArg.ldapConfig.bindPasswordEncrypted &&
!cryptoService.isEncrypted(dataArg.ldapConfig.bindPasswordEncrypted)
) {
newLdapConfig.bindPasswordEncrypted = await cryptoService.encrypt(
dataArg.ldapConfig.bindPasswordEncrypted,
);
}
provider.ldapConfig = newLdapConfig;
}
// Update attribute mapping
if (dataArg.attributeMapping) {
provider.attributeMapping = {
...provider.attributeMapping,
...dataArg.attributeMapping,
} as any;
}
// Update provisioning settings
if (dataArg.provisioning) {
provider.provisioning = {
...provider.provisioning,
...dataArg.provisioning,
} as any;
}
await provider.save();
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).log('AUTH_PROVIDER_UPDATED', 'auth_provider', {
resourceId: provider.id,
success: true,
metadata: { providerName: provider.name },
});
return { provider: provider.toAdminInfo() };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update provider');
}
},
),
);
// Delete Admin Provider
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteAdminProvider>(
'deleteAdminProvider',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const provider = await AuthProvider.findById(dataArg.providerId);
if (!provider) {
throw new plugins.typedrequest.TypedResponseError('Provider not found');
}
// Soft delete - disable instead of removing
provider.status = 'disabled';
await provider.save();
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).log('AUTH_PROVIDER_DELETED', 'auth_provider', {
resourceId: provider.id,
success: true,
metadata: { providerName: provider.name },
});
return { message: 'Provider disabled' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete provider');
}
},
),
);
// Test Admin Provider
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestAdminProvider>(
'testAdminProvider',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const result = await externalAuthService.testConnection(dataArg.providerId);
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).log('AUTH_PROVIDER_TESTED', 'auth_provider', {
resourceId: dataArg.providerId,
success: result.success,
metadata: {
result: result.success ? 'success' : 'failure',
latencyMs: result.latencyMs,
error: result.error,
},
});
return { result };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to test provider');
}
},
),
);
// Get Platform Settings
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformSettings>(
'getPlatformSettings',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const settings = await PlatformSettings.get();
return {
settings: {
id: settings.id,
auth: settings.auth,
updatedAt: settings.updatedAt instanceof Date ? settings.updatedAt.toISOString() : String(settings.updatedAt),
updatedById: settings.updatedById,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get settings');
}
},
),
);
// Update Platform Settings
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdatePlatformSettings>(
'updatePlatformSettings',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const settings = await PlatformSettings.get();
if (dataArg.auth) {
await settings.updateAuthSettings(dataArg.auth as any, dataArg.identity.userId);
}
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).log('PLATFORM_SETTINGS_UPDATED', 'platform_settings', {
resourceId: 'platform-settings',
success: true,
});
return {
settings: {
id: settings.id,
auth: settings.auth,
updatedAt: settings.updatedAt instanceof Date ? settings.updatedAt.toISOString() : String(settings.updatedAt),
updatedById: settings.updatedById,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update settings');
}
},
),
);
}
}

View File

@@ -0,0 +1,105 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { AuditLog } from '../../models/auditlog.ts';
import { PermissionService } from '../../services/permission.service.ts';
export class AuditHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Query Audit Logs
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_QueryAudit>(
'queryAudit',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const {
organizationId,
repositoryId,
resourceType,
actions,
success,
startDate: startDateStr,
endDate: endDateStr,
limit: limitParam,
offset: offsetParam,
} = dataArg;
const limit = limitParam || 100;
const offset = offsetParam || 0;
const startDate = startDateStr ? new Date(startDateStr) : undefined;
const endDate = endDateStr ? new Date(endDateStr) : undefined;
// Determine actor filter based on permissions
let actorId: string | undefined;
if (dataArg.identity.isSystemAdmin) {
// System admins can see all
actorId = dataArg.actorId;
} else if (organizationId) {
// Check if user can manage this org
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
organizationId,
);
if (!canManage) {
// User can only see their own actions in this org
actorId = dataArg.identity.userId;
}
} else {
// Non-admins without org filter can only see their own actions
actorId = dataArg.identity.userId;
}
const result = await AuditLog.query({
actorId,
organizationId,
repositoryId,
resourceType: resourceType as any,
action: actions as any[],
success,
startDate,
endDate,
limit,
offset,
});
return {
logs: result.logs.map((log) => ({
id: log.id,
actorId: log.actorId,
actorType: log.actorType as interfaces.data.IAuditEntry['actorType'],
action: log.action as interfaces.data.TAuditAction,
resourceType: log.resourceType as interfaces.data.TAuditResourceType,
resourceId: log.resourceId,
resourceName: log.resourceName,
organizationId: log.organizationId,
repositoryId: log.repositoryId,
success: log.success,
errorCode: log.errorCode,
timestamp: log.timestamp instanceof Date ? log.timestamp.toISOString() : String(log.timestamp),
metadata: log.metadata || {},
})),
total: result.total,
limit,
offset,
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to query audit logs');
}
},
),
);
}
}

View File

@@ -0,0 +1,263 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { AuthService } from '../../services/auth.service.ts';
import { User } from '../../models/user.ts';
import { AuthProvider, PlatformSettings } from '../../models/index.ts';
export class AuthHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private authService: AuthService;
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.authService = new AuthService();
}
/**
* Initialize auth handler - must be called after construction
*/
public async initialize(): Promise<void> {
this.registerHandlers();
}
private registerHandlers(): void {
// Login
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_Login>(
'login',
async (dataArg) => {
try {
const { email, password } = dataArg;
if (!email || !password) {
throw new plugins.typedrequest.TypedResponseError('Email and password are required');
}
const result = await this.authService.login(email, password);
if (!result.success || !result.user || !result.accessToken || !result.refreshToken) {
return {
errorCode: result.errorCode,
errorMessage: result.errorMessage,
};
}
const user = result.user;
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
const identity: interfaces.data.IIdentity = {
jwt: result.accessToken,
refreshJwt: result.refreshToken,
userId: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
isSystemAdmin: user.isSystemAdmin,
expiresAt,
sessionId: result.sessionId!,
};
return {
identity,
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
isSystemAdmin: user.isSystemAdmin,
isActive: user.isActive,
createdAt: user.createdAt instanceof Date
? user.createdAt.toISOString()
: String(user.createdAt),
lastLoginAt: user.lastLoginAt instanceof Date
? user.lastLoginAt.toISOString()
: user.lastLoginAt
? String(user.lastLoginAt)
: undefined,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Login failed');
}
},
),
);
// Refresh Token
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshToken>(
'refreshToken',
async (dataArg) => {
try {
if (!dataArg.identity?.refreshJwt) {
throw new plugins.typedrequest.TypedResponseError('Refresh token is required');
}
const result = await this.authService.refresh(dataArg.identity.refreshJwt);
if (!result.success || !result.user || !result.accessToken) {
throw new plugins.typedrequest.TypedResponseError(
result.errorMessage || 'Token refresh failed',
);
}
const user = result.user;
const expiresAt = Date.now() + 15 * 60 * 1000;
return {
identity: {
jwt: result.accessToken,
refreshJwt: dataArg.identity.refreshJwt,
userId: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
isSystemAdmin: user.isSystemAdmin,
expiresAt,
sessionId: result.sessionId || dataArg.identity.sessionId,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Token refresh failed');
}
},
),
);
// Logout
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_Logout>(
'logout',
async (dataArg) => {
try {
if (!dataArg.identity?.jwt) {
throw new plugins.typedrequest.TypedResponseError('Identity required');
}
if (dataArg.all) {
const count = await this.authService.logoutAll(dataArg.identity.userId);
return { message: `Logged out from ${count} sessions` };
}
const sessionId = dataArg.sessionId || dataArg.identity.sessionId;
if (sessionId) {
await this.authService.logout(sessionId, {
userId: dataArg.identity.userId,
});
}
return { message: 'Logged out successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Logout failed');
}
},
),
);
// Get Me
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetMe>(
'getMe',
async (dataArg) => {
try {
if (!dataArg.identity?.jwt) {
throw new plugins.typedrequest.TypedResponseError('Identity required');
}
const validated = await this.authService.validateAccessToken(dataArg.identity.jwt);
if (!validated) {
throw new plugins.typedrequest.TypedResponseError('Invalid or expired token');
}
const user = validated.user;
return {
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
isSystemAdmin: user.isSystemAdmin,
isActive: user.isActive,
createdAt: user.createdAt instanceof Date
? user.createdAt.toISOString()
: String(user.createdAt),
lastLoginAt: user.lastLoginAt instanceof Date
? user.lastLoginAt.toISOString()
: user.lastLoginAt
? String(user.lastLoginAt)
: undefined,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get user info');
}
},
),
);
// Get Auth Providers (public)
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAuthProviders>(
'getAuthProviders',
async (_dataArg) => {
try {
const settings = await PlatformSettings.get();
const providers = await AuthProvider.getActiveProviders();
return {
providers: providers.map((p) => p.toPublicInfo()),
localAuthEnabled: settings.auth.localAuthEnabled,
defaultProviderId: settings.auth.defaultProviderId,
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get auth providers');
}
},
),
);
}
// Guard for valid identity - validates JWT via AuthService
public validIdentityGuard = new plugins.smartguard.Guard<{
identity: interfaces.data.IIdentity;
}>(
async (dataArg) => {
if (!dataArg.identity?.jwt) return false;
try {
const validated = await this.authService.validateAccessToken(dataArg.identity.jwt);
if (!validated) return false;
// Verify the userId matches the identity claim
if (dataArg.identity.userId !== validated.user.id) return false;
return true;
} catch {
return false;
}
},
{ failedHint: 'identity is not valid', name: 'validIdentityGuard' },
);
// Guard for admin identity - validates JWT + checks isSystemAdmin
public adminIdentityGuard = new plugins.smartguard.Guard<{
identity: interfaces.data.IIdentity;
}>(
async (dataArg) => {
const isValid = await this.validIdentityGuard.exec(dataArg);
if (!isValid) return false;
// Check isSystemAdmin claim from the identity
if (!dataArg.identity.isSystemAdmin) return false;
// Double-check from database
const user = await User.findById(dataArg.identity.userId);
if (!user || !user.isSystemAdmin) return false;
return true;
},
{ failedHint: 'user is not admin', name: 'adminIdentityGuard' },
);
}

View File

@@ -0,0 +1,9 @@
export * from './auth.handler.ts';
export * from './organization.handler.ts';
export * from './repository.handler.ts';
export * from './package.handler.ts';
export * from './token.handler.ts';
export * from './audit.handler.ts';
export * from './admin.handler.ts';
export * from './oauth.handler.ts';
export * from './user.handler.ts';

View File

@@ -0,0 +1,160 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { externalAuthService } from '../../services/external.auth.service.ts';
export class OAuthHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// OAuth Authorize - initiate OAuth flow
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_OAuthAuthorize>(
'oauthAuthorize',
async (dataArg) => {
try {
const { providerId, returnUrl } = dataArg;
const { authUrl } = await externalAuthService.initiateOAuth(providerId, returnUrl);
return { redirectUrl: authUrl };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
const errorMessage = error instanceof Error ? error.message : 'Authorization failed';
throw new plugins.typedrequest.TypedResponseError(errorMessage);
}
},
),
);
// OAuth Callback - handle provider callback
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_OAuthCallback>(
'oauthCallback',
async (dataArg) => {
try {
const { code, state } = dataArg;
if (!code || !state) {
return {
errorCode: 'MISSING_PARAMETERS',
errorMessage: 'Code and state are required',
};
}
const result = await externalAuthService.handleOAuthCallback(
{ code, state },
{},
);
if (!result.success) {
return {
errorCode: result.errorCode,
errorMessage: result.errorMessage,
};
}
const user = result.user!;
const expiresAt = Date.now() + 15 * 60 * 1000;
return {
identity: {
jwt: result.accessToken!,
refreshJwt: result.refreshToken!,
userId: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
isSystemAdmin: user.isSystemAdmin,
expiresAt,
sessionId: result.sessionId!,
},
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
isSystemAdmin: user.isSystemAdmin,
isActive: user.isActive,
createdAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : String(user.createdAt),
lastLoginAt: user.lastLoginAt instanceof Date ? user.lastLoginAt.toISOString() : user.lastLoginAt ? String(user.lastLoginAt) : undefined,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('OAuth callback failed');
}
},
),
);
// LDAP Login
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_LdapLogin>(
'ldapLogin',
async (dataArg) => {
try {
const { providerId, username, password } = dataArg;
if (!username || !password) {
throw new plugins.typedrequest.TypedResponseError(
'Username and password are required',
);
}
const result = await externalAuthService.authenticateLdap(
providerId,
username,
password,
{},
);
if (!result.success) {
return {
errorCode: result.errorCode,
errorMessage: result.errorMessage,
};
}
const user = result.user!;
const expiresAt = Date.now() + 15 * 60 * 1000;
return {
identity: {
jwt: result.accessToken!,
refreshJwt: result.refreshToken!,
userId: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
isSystemAdmin: user.isSystemAdmin,
expiresAt,
sessionId: result.sessionId!,
},
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
isSystemAdmin: user.isSystemAdmin,
isActive: user.isActive,
createdAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : String(user.createdAt),
lastLoginAt: user.lastLoginAt instanceof Date ? user.lastLoginAt.toISOString() : user.lastLoginAt ? String(user.lastLoginAt) : undefined,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('LDAP login failed');
}
},
),
);
}
}

View File

@@ -0,0 +1,548 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { Organization, OrganizationMember, User } from '../../models/index.ts';
import { PermissionService } from '../../services/permission.service.ts';
import { AuditService } from '../../services/audit.service.ts';
export class OrganizationHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
/**
* Helper to resolve organization by ID or name
*/
private async resolveOrganization(idOrName: string): Promise<Organization | null> {
return idOrName.startsWith('Organization:')
? await Organization.findById(idOrName)
: await Organization.findByName(idOrName);
}
private registerHandlers(): void {
// Get Organizations
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganizations>(
'getOrganizations',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const userId = dataArg.identity.userId;
let organizations: Organization[];
if (dataArg.identity.isSystemAdmin) {
organizations = await Organization.getInstances({});
} else {
const memberships = await OrganizationMember.getUserOrganizations(userId);
const orgs: Organization[] = [];
for (const m of memberships) {
const org = await Organization.findById(m.organizationId);
if (org) orgs.push(org);
}
organizations = orgs;
}
return {
organizations: organizations.map((org) => ({
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
})),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list organizations');
}
},
),
);
// Get Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganization>(
'getOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check access - public orgs visible to all, private requires membership
if (!org.isPublic) {
const isMember = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (!isMember && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
}
const orgData: interfaces.data.IOrganizationDetail = {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
};
// Include settings for admins
if (dataArg.identity.isSystemAdmin && org.settings) {
orgData.settings = org.settings as any;
}
return { organization: orgData };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get organization');
}
},
),
);
// Create Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateOrganization>(
'createOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { name, displayName, description, isPublic } = dataArg;
if (!name) {
throw new plugins.typedrequest.TypedResponseError('Organization name is required');
}
// Validate name format
if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(name)) {
throw new plugins.typedrequest.TypedResponseError(
'Name must be lowercase alphanumeric with optional hyphens and dots',
);
}
// Check uniqueness
const existing = await Organization.findByName(name);
if (existing) {
throw new plugins.typedrequest.TypedResponseError('Organization name already taken');
}
// Create organization
const org = new Organization();
org.id = await Organization.getNewId();
org.name = name;
org.displayName = displayName || name;
org.description = description;
org.isPublic = isPublic ?? false;
org.memberCount = 1;
org.createdAt = new Date();
org.createdById = dataArg.identity.userId;
await org.save();
// Add creator as owner
const membership = new OrganizationMember();
membership.id = await OrganizationMember.getNewId();
membership.organizationId = org.id;
membership.userId = dataArg.identity.userId;
membership.role = 'owner';
membership.invitedBy = dataArg.identity.userId;
membership.joinedAt = new Date();
await membership.save();
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
}).logOrganizationCreated(org.id, org.name);
return {
organization: {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to create organization');
}
},
),
);
// Update Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateOrganization>(
'updateOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
if (dataArg.displayName !== undefined) org.displayName = dataArg.displayName;
if (dataArg.description !== undefined) org.description = dataArg.description;
if (dataArg.avatarUrl !== undefined) org.avatarUrl = dataArg.avatarUrl;
if (dataArg.website !== undefined) org.website = dataArg.website;
if (dataArg.isPublic !== undefined) org.isPublic = dataArg.isPublic;
// Only system admins can change settings
if (dataArg.settings && dataArg.identity.isSystemAdmin) {
org.settings = { ...org.settings, ...dataArg.settings } as any;
}
await org.save();
return {
organization: {
id: org.id,
name: org.name,
displayName: org.displayName,
description: org.description,
avatarUrl: org.avatarUrl,
website: org.website,
isPublic: org.isPublic,
memberCount: org.memberCount,
plan: (org as any).plan || 'free',
usedStorageBytes: org.usedStorageBytes || 0,
storageQuotaBytes: (org as any).storageQuotaBytes || 0,
createdAt: org.createdAt instanceof Date
? org.createdAt.toISOString()
: String(org.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update organization');
}
},
),
);
// Delete Organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteOrganization>(
'deleteOrganization',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Only owners and system admins can delete
const membership = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (membership?.role !== 'owner' && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Owner access required');
}
await org.delete();
return { message: 'Organization deleted successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete organization');
}
},
),
);
// Get Organization Members
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrganizationMembers>(
'getOrganizationMembers',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check membership
const isMember = await OrganizationMember.findMembership(
org.id,
dataArg.identity.userId,
);
if (!isMember && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
const members = await OrganizationMember.getOrgMembers(org.id);
const membersWithUsers = await Promise.all(
members.map(async (m) => {
const user = await User.findById(m.userId);
return {
userId: m.userId,
role: m.role as interfaces.data.TOrganizationRole,
addedAt: m.joinedAt instanceof Date
? m.joinedAt.toISOString()
: String(m.joinedAt),
user: user
? {
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
}
: null,
};
}),
);
return { members: membersWithUsers };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list members');
}
},
),
);
// Add Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AddOrganizationMember>(
'addOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
const { userId, role } = dataArg;
if (!userId || !role) {
throw new plugins.typedrequest.TypedResponseError('userId and role are required');
}
if (!['owner', 'admin', 'member'].includes(role)) {
throw new plugins.typedrequest.TypedResponseError('Invalid role');
}
// Check user exists
const user = await User.findById(userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
// Check if already a member
const existing = await OrganizationMember.findMembership(org.id, userId);
if (existing) {
throw new plugins.typedrequest.TypedResponseError('User is already a member');
}
// Add member
const membership = new OrganizationMember();
membership.id = await OrganizationMember.getNewId();
membership.organizationId = org.id;
membership.userId = userId;
membership.role = role;
membership.invitedBy = dataArg.identity.userId;
membership.joinedAt = new Date();
await membership.save();
// Update member count
org.memberCount += 1;
await org.save();
return {
member: {
userId: membership.userId,
role: membership.role as interfaces.data.TOrganizationRole,
addedAt: membership.joinedAt instanceof Date
? membership.joinedAt.toISOString()
: String(membership.joinedAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to add member');
}
},
),
);
// Update Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateOrganizationMember>(
'updateOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
const { userId, role } = dataArg;
if (!role || !['owner', 'admin', 'member'].includes(role)) {
throw new plugins.typedrequest.TypedResponseError('Valid role is required');
}
const membership = await OrganizationMember.findMembership(org.id, userId);
if (!membership) {
throw new plugins.typedrequest.TypedResponseError('Member not found');
}
// Cannot change last owner
if (membership.role === 'owner' && role !== 'owner') {
const members = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = members.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner');
}
}
membership.role = role;
await membership.save();
return {
member: {
userId: membership.userId,
role: membership.role as interfaces.data.TOrganizationRole,
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update member');
}
},
),
);
// Remove Organization Member
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveOrganizationMember>(
'removeOrganizationMember',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const org = await this.resolveOrganization(dataArg.organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Users can remove themselves, admins can remove others
if (dataArg.userId !== dataArg.identity.userId) {
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
org.id,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
}
const membership = await OrganizationMember.findMembership(org.id, dataArg.userId);
if (!membership) {
throw new plugins.typedrequest.TypedResponseError('Member not found');
}
// Cannot remove last owner
if (membership.role === 'owner') {
const members = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = members.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner');
}
}
await membership.delete();
// Update member count
org.memberCount = Math.max(0, org.memberCount - 1);
await org.save();
return { message: 'Member removed successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to remove member');
}
},
),
);
}
}

View File

@@ -0,0 +1,315 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { Package, Repository } from '../../models/index.ts';
import { PermissionService } from '../../services/permission.service.ts';
export class PackageHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Search Packages
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SearchPackages>(
'searchPackages',
async (dataArg) => {
try {
const query = dataArg.query || '';
const protocol = dataArg.protocol;
const organizationId = dataArg.organizationId;
const limit = dataArg.limit || 50;
const offset = dataArg.offset || 0;
// Determine visibility: anonymous users see only public packages
const hasIdentity = !!dataArg.identity?.jwt;
const isPrivate = hasIdentity ? undefined : false;
const packages = await Package.searchPackages(query, {
protocol,
organizationId,
isPrivate,
limit,
offset,
});
// Filter out packages user doesn't have access to
const accessiblePackages: typeof packages = [];
for (const pkg of packages) {
if (!pkg.isPrivate) {
accessiblePackages.push(pkg);
continue;
}
if (hasIdentity && dataArg.identity) {
const canAccess = await this.permissionService.canAccessPackage(
dataArg.identity.userId,
pkg.organizationId,
pkg.repositoryId,
'read',
);
if (canAccess) {
accessiblePackages.push(pkg);
}
}
}
return {
packages: accessiblePackages.map((pkg) => ({
id: pkg.id,
name: pkg.name,
description: pkg.description,
protocol: pkg.protocol as interfaces.data.TRegistryProtocol,
organizationId: pkg.organizationId,
repositoryId: pkg.repositoryId,
latestVersion: pkg.distTags?.['latest'],
isPrivate: pkg.isPrivate,
downloadCount: pkg.downloadCount || 0,
starCount: pkg.starCount || 0,
storageBytes: pkg.storageBytes || 0,
updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt),
createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt),
})),
total: accessiblePackages.length,
limit,
offset,
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to search packages');
}
},
),
);
// Get Package
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPackage>(
'getPackage',
async (dataArg) => {
try {
const pkg = await Package.findById(dataArg.packageId);
if (!pkg) {
throw new plugins.typedrequest.TypedResponseError('Package not found');
}
// Check access for private packages
if (pkg.isPrivate) {
if (!dataArg.identity?.jwt) {
throw new plugins.typedrequest.TypedResponseError('Authentication required');
}
const canAccess = await this.permissionService.canAccessPackage(
dataArg.identity.userId,
pkg.organizationId,
pkg.repositoryId,
'read',
);
if (!canAccess) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
}
return {
package: {
id: pkg.id,
name: pkg.name,
description: pkg.description,
protocol: pkg.protocol as interfaces.data.TRegistryProtocol,
organizationId: pkg.organizationId,
repositoryId: pkg.repositoryId,
latestVersion: pkg.distTags?.['latest'],
isPrivate: pkg.isPrivate,
downloadCount: pkg.downloadCount || 0,
starCount: pkg.starCount || 0,
storageBytes: pkg.storageBytes || 0,
distTags: pkg.distTags || {},
versions: Object.keys(pkg.versions || {}),
updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt),
createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get package');
}
},
),
);
// Get Package Versions
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPackageVersions>(
'getPackageVersions',
async (dataArg) => {
try {
const pkg = await Package.findById(dataArg.packageId);
if (!pkg) {
throw new plugins.typedrequest.TypedResponseError('Package not found');
}
// Check access for private packages
if (pkg.isPrivate) {
if (!dataArg.identity?.jwt) {
throw new plugins.typedrequest.TypedResponseError('Authentication required');
}
const canAccess = await this.permissionService.canAccessPackage(
dataArg.identity.userId,
pkg.organizationId,
pkg.repositoryId,
'read',
);
if (!canAccess) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
}
const versions = Object.entries(pkg.versions || {}).map(([version, data]) => ({
version,
publishedAt: data.publishedAt instanceof Date ? data.publishedAt.toISOString() : String(data.publishedAt || ''),
size: data.size || 0,
downloads: data.downloads || 0,
checksum: data.metadata?.checksum as interfaces.data.IPackageVersion['checksum'],
}));
return {
packageId: pkg.id,
packageName: pkg.name,
distTags: pkg.distTags || {},
versions,
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list versions');
}
},
),
);
// Delete Package
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeletePackage>(
'deletePackage',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const pkg = await Package.findById(dataArg.packageId);
if (!pkg) {
throw new plugins.typedrequest.TypedResponseError('Package not found');
}
// Check delete permission
const canDelete = await this.permissionService.canAccessPackage(
dataArg.identity.userId,
pkg.organizationId,
pkg.repositoryId,
'delete',
);
if (!canDelete) {
throw new plugins.typedrequest.TypedResponseError('Delete permission required');
}
// Update repository counts before deleting
const repo = await Repository.findById(pkg.repositoryId);
if (repo) {
repo.packageCount = Math.max(0, repo.packageCount - 1);
repo.storageBytes -= pkg.storageBytes;
await repo.save();
}
await pkg.delete();
return { message: 'Package deleted successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete package');
}
},
),
);
// Delete Package Version
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeletePackageVersion>(
'deletePackageVersion',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const pkg = await Package.findById(dataArg.packageId);
if (!pkg) {
throw new plugins.typedrequest.TypedResponseError('Package not found');
}
const versionData = pkg.versions?.[dataArg.version];
if (!versionData) {
throw new plugins.typedrequest.TypedResponseError('Version not found');
}
// Check delete permission
const canDelete = await this.permissionService.canAccessPackage(
dataArg.identity.userId,
pkg.organizationId,
pkg.repositoryId,
'delete',
);
if (!canDelete) {
throw new plugins.typedrequest.TypedResponseError('Delete permission required');
}
// Check if this is the only version
if (Object.keys(pkg.versions).length === 1) {
throw new plugins.typedrequest.TypedResponseError(
'Cannot delete the only version. Delete the entire package instead.',
);
}
// Remove version
const sizeReduction = versionData.size || 0;
delete pkg.versions[dataArg.version];
pkg.storageBytes -= sizeReduction;
// Update dist tags
for (const [tag, tagVersion] of Object.entries(pkg.distTags || {})) {
if (tagVersion === dataArg.version) {
delete pkg.distTags[tag];
}
}
// Set new latest if needed
if (!pkg.distTags['latest'] && Object.keys(pkg.versions).length > 0) {
const versions = Object.keys(pkg.versions).sort();
pkg.distTags['latest'] = versions[versions.length - 1];
}
await pkg.save();
// Update repository storage
const repo = await Repository.findById(pkg.repositoryId);
if (repo) {
repo.storageBytes -= sizeReduction;
await repo.save();
}
return { message: 'Version deleted successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete version');
}
},
),
);
}
}

View File

@@ -0,0 +1,272 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { Organization, Repository } from '../../models/index.ts';
import { PermissionService } from '../../services/permission.service.ts';
import { AuditService } from '../../services/audit.service.ts';
export class RepositoryHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Repositories
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRepositories>(
'getRepositories',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const repositories = await this.permissionService.getAccessibleRepositories(
dataArg.identity.userId,
dataArg.organizationId,
);
return {
repositories: repositories.map((repo) => ({
id: repo.id,
organizationId: repo.organizationId,
name: repo.name,
description: repo.description,
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
isPublic: repo.isPublic,
packageCount: repo.packageCount,
storageBytes: repo.storageBytes || 0,
downloadCount: (repo as any).downloadCount || 0,
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
})),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list repositories');
}
},
),
);
// Get Repository
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRepository>(
'getRepository',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const repo = await Repository.findById(dataArg.repositoryId);
if (!repo) {
throw new plugins.typedrequest.TypedResponseError('Repository not found');
}
// Check access
if (!repo.isPublic) {
const permissions = await this.permissionService.resolvePermissions({
userId: dataArg.identity.userId,
organizationId: repo.organizationId,
repositoryId: repo.id,
});
if (!permissions.canRead) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
}
return {
repository: {
id: repo.id,
organizationId: repo.organizationId,
name: repo.name,
description: repo.description,
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
isPublic: repo.isPublic,
packageCount: repo.packageCount,
storageBytes: repo.storageBytes || 0,
downloadCount: (repo as any).downloadCount || 0,
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get repository');
}
},
),
);
// Create Repository
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRepository>(
'createRepository',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { organizationId, name, description, protocol, visibility } = dataArg;
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
organizationId,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
if (!name) {
throw new plugins.typedrequest.TypedResponseError('Repository name is required');
}
// Validate name format
if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(name)) {
throw new plugins.typedrequest.TypedResponseError(
'Name must be lowercase alphanumeric with optional dots, hyphens, or underscores',
);
}
// Check org exists
const org = await Organization.findById(organizationId);
if (!org) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
// Create repository
const repo = await Repository.createRepository({
organizationId,
name,
description,
protocol: protocol || 'npm',
visibility: visibility || 'private',
createdById: dataArg.identity.userId,
});
// Audit log
await AuditService.withContext({
actorId: dataArg.identity.userId,
actorType: 'user',
organizationId,
}).logRepositoryCreated(repo.id, repo.name, organizationId);
return {
repository: {
id: repo.id,
organizationId: repo.organizationId,
name: repo.name,
description: repo.description,
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
isPublic: repo.isPublic,
packageCount: repo.packageCount,
storageBytes: repo.storageBytes || 0,
downloadCount: 0,
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to create repository');
}
},
),
);
// Update Repository
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRepository>(
'updateRepository',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const repo = await Repository.findById(dataArg.repositoryId);
if (!repo) {
throw new plugins.typedrequest.TypedResponseError('Repository not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageRepository(
dataArg.identity.userId,
repo.organizationId,
dataArg.repositoryId,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
if (dataArg.description !== undefined) repo.description = dataArg.description;
if (dataArg.visibility !== undefined) repo.visibility = dataArg.visibility as any;
await repo.save();
return {
repository: {
id: repo.id,
organizationId: repo.organizationId,
name: repo.name,
description: repo.description,
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
isPublic: repo.isPublic,
packageCount: repo.packageCount,
storageBytes: repo.storageBytes || 0,
downloadCount: (repo as any).downloadCount || 0,
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update repository');
}
},
),
);
// Delete Repository
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRepository>(
'deleteRepository',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const repo = await Repository.findById(dataArg.repositoryId);
if (!repo) {
throw new plugins.typedrequest.TypedResponseError('Repository not found');
}
// Check admin permission
const canManage = await this.permissionService.canManageRepository(
dataArg.identity.userId,
repo.organizationId,
dataArg.repositoryId,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
// Check for packages
if (repo.packageCount > 0) {
throw new plugins.typedrequest.TypedResponseError(
'Cannot delete repository with packages. Remove all packages first.',
);
}
await repo.delete();
return { message: 'Repository deleted successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete repository');
}
},
),
);
}
}

View File

@@ -0,0 +1,198 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { ApiToken } from '../../models/index.ts';
import { TokenService } from '../../services/token.service.ts';
import { PermissionService } from '../../services/permission.service.ts';
export class TokenHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private tokenService = new TokenService();
private permissionService = new PermissionService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Tokens
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetTokens>(
'getTokens',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
let tokens;
if (dataArg.organizationId) {
// Check if user can manage org
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
dataArg.organizationId,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError(
'Not authorized to view organization tokens',
);
}
tokens = await this.tokenService.getOrgTokens(dataArg.organizationId);
} else {
tokens = await this.tokenService.getUserTokens(dataArg.identity.userId);
}
return {
tokens: tokens.map((t) => ({
id: t.id,
name: t.name,
tokenPrefix: t.tokenPrefix,
protocols: t.protocols as interfaces.data.TRegistryProtocol[],
scopes: t.scopes as interfaces.data.ITokenScope[],
organizationId: t.organizationId,
createdById: t.createdById,
expiresAt: t.expiresAt instanceof Date ? t.expiresAt.toISOString() : t.expiresAt ? String(t.expiresAt) : undefined,
lastUsedAt: t.lastUsedAt instanceof Date ? t.lastUsedAt.toISOString() : t.lastUsedAt ? String(t.lastUsedAt) : undefined,
usageCount: t.usageCount,
createdAt: t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt),
})),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list tokens');
}
},
),
);
// Create Token
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateToken>(
'createToken',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { name, organizationId, protocols, scopes, expiresInDays } = dataArg;
if (!name) {
throw new plugins.typedrequest.TypedResponseError('Token name is required');
}
if (!protocols || protocols.length === 0) {
throw new plugins.typedrequest.TypedResponseError('At least one protocol is required');
}
if (!scopes || scopes.length === 0) {
throw new plugins.typedrequest.TypedResponseError('At least one scope is required');
}
// Validate protocols
const validProtocols = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems', '*'];
for (const protocol of protocols) {
if (!validProtocols.includes(protocol)) {
throw new plugins.typedrequest.TypedResponseError(`Invalid protocol: ${protocol}`);
}
}
// Validate scopes
for (const scope of scopes) {
if (!scope.protocol || !scope.actions || scope.actions.length === 0) {
throw new plugins.typedrequest.TypedResponseError('Invalid scope configuration');
}
}
// If creating org token, verify permission
if (organizationId) {
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
organizationId,
);
if (!canManage) {
throw new plugins.typedrequest.TypedResponseError(
'Not authorized to create organization tokens',
);
}
}
const result = await this.tokenService.createToken({
userId: dataArg.identity.userId,
organizationId,
createdById: dataArg.identity.userId,
name,
protocols: protocols as any[],
scopes: scopes as any[],
expiresInDays,
});
return {
token: {
id: result.token.id,
name: result.token.name,
token: result.rawToken,
tokenPrefix: result.token.tokenPrefix,
protocols: result.token.protocols as interfaces.data.TRegistryProtocol[],
scopes: result.token.scopes as interfaces.data.ITokenScope[],
organizationId: result.token.organizationId,
createdById: result.token.createdById,
expiresAt: result.token.expiresAt instanceof Date ? result.token.expiresAt.toISOString() : result.token.expiresAt ? String(result.token.expiresAt) : undefined,
usageCount: result.token.usageCount,
createdAt: result.token.createdAt instanceof Date ? result.token.createdAt.toISOString() : String(result.token.createdAt),
warning: 'Store this token securely. It will not be shown again.',
},
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to create token');
}
},
),
);
// Revoke Token
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeToken>(
'revokeToken',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { tokenId } = dataArg;
// Check if it's a personal token
const userTokens = await this.tokenService.getUserTokens(dataArg.identity.userId);
let token = userTokens.find((t) => t.id === tokenId);
if (!token) {
// Check if it's an org token the user can manage
const anyToken = await ApiToken.getInstance({ id: tokenId, isRevoked: false });
if (anyToken?.organizationId) {
const canManage = await this.permissionService.canManageOrganization(
dataArg.identity.userId,
anyToken.organizationId,
);
if (canManage) {
token = anyToken;
}
}
}
if (!token) {
throw new plugins.typedrequest.TypedResponseError('Token not found');
}
const success = await this.tokenService.revokeToken(tokenId, 'user_revoked');
if (!success) {
throw new plugins.typedrequest.TypedResponseError('Failed to revoke token');
}
return { message: 'Token revoked successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to revoke token');
}
},
),
);
}
}

View File

@@ -0,0 +1,263 @@
import * as plugins from '../../plugins.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import { requireValidIdentity, requireAdminIdentity } from '../helpers/guards.ts';
import { User } from '../../models/user.ts';
import { Session } from '../../models/session.ts';
import { AuthService } from '../../services/auth.service.ts';
export class UserHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private authService = new AuthService();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
/**
* Helper to format user to IUser interface
*/
private formatUser(user: User): interfaces.data.IUser {
return {
id: user.id,
email: user.email,
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
isSystemAdmin: user.isSystemAdmin,
isActive: user.isActive,
createdAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : String(user.createdAt),
lastLoginAt: user.lastLoginAt instanceof Date ? user.lastLoginAt.toISOString() : user.lastLoginAt ? String(user.lastLoginAt) : undefined,
};
}
private registerHandlers(): void {
// Get Users (admin only)
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUsers>(
'getUsers',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.authHandler, dataArg);
try {
const users = await User.getInstances({});
return {
users: users.map((u) => this.formatUser(u)),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to list users');
}
},
),
);
// Get User
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUser>(
'getUser',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { userId } = dataArg;
// Users can view their own profile, admins can view any
if (userId !== dataArg.identity.userId && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
const user = await User.findById(userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
return { user: this.formatUser(user) };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get user');
}
},
),
);
// Update User
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateUser>(
'updateUser',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { userId, displayName, avatarUrl, password, isActive, isSystemAdmin } = dataArg;
// Users can update their own profile, admins can update any
if (userId !== dataArg.identity.userId && !dataArg.identity.isSystemAdmin) {
throw new plugins.typedrequest.TypedResponseError('Access denied');
}
const user = await User.findById(userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
if (displayName !== undefined) user.displayName = displayName;
if (avatarUrl !== undefined) user.avatarUrl = avatarUrl;
// Only admins can change these
if (dataArg.identity.isSystemAdmin) {
if (isActive !== undefined) user.status = isActive ? 'active' : 'suspended';
if (isSystemAdmin !== undefined) user.isPlatformAdmin = isSystemAdmin;
}
// Password change
if (password) {
user.passwordHash = await AuthService.hashPassword(password);
}
await user.save();
return { user: this.formatUser(user) };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to update user');
}
},
),
);
// Get User Sessions
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUserSessions>(
'getUserSessions',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const sessions = await Session.getUserSessions(dataArg.identity.userId);
return {
sessions: sessions.map((s) => ({
id: s.id,
userId: s.userId,
userAgent: s.userAgent,
ipAddress: s.ipAddress,
isValid: s.isValid,
lastActivityAt: s.lastActivityAt instanceof Date ? s.lastActivityAt.toISOString() : String(s.lastActivityAt),
createdAt: s.createdAt instanceof Date ? s.createdAt.toISOString() : String(s.createdAt),
})),
};
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to get sessions');
}
},
),
);
// Revoke Session
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeSession>(
'revokeSession',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
await this.authService.logout(dataArg.sessionId, {
userId: dataArg.identity.userId,
});
return { message: 'Session revoked successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to revoke session');
}
},
),
);
// Change Password
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ChangePassword>(
'changePassword',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { currentPassword, newPassword } = dataArg;
if (!currentPassword || !newPassword) {
throw new plugins.typedrequest.TypedResponseError(
'Current password and new password are required',
);
}
const user = await User.findById(dataArg.identity.userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
// Verify current password
const isValid = await user.verifyPassword(currentPassword);
if (!isValid) {
throw new plugins.typedrequest.TypedResponseError('Current password is incorrect');
}
// Hash and set new password
user.passwordHash = await AuthService.hashPassword(newPassword);
await user.save();
return { message: 'Password changed successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to change password');
}
},
),
);
// Delete Account
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteAccount>(
'deleteAccount',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
try {
const { password } = dataArg;
if (!password) {
throw new plugins.typedrequest.TypedResponseError(
'Password is required to delete account',
);
}
const user = await User.findById(dataArg.identity.userId);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
// Verify password
const isValid = await user.verifyPassword(password);
if (!isValid) {
throw new plugins.typedrequest.TypedResponseError('Password is incorrect');
}
// Soft delete - deactivate instead of removing
user.status = 'suspended';
await user.save();
// Invalidate all sessions
await Session.invalidateAllUserSessions(user.id, 'account_deleted');
return { message: 'Account deactivated successfully' };
} catch (error) {
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
throw new plugins.typedrequest.TypedResponseError('Failed to delete account');
}
},
),
);
}
}

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../plugins.ts';
import type { AuthHandler } from '../handlers/auth.handler.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>(
authHandler: AuthHandler,
dataArg: T,
): Promise<void> {
if (!dataArg.identity) {
throw new plugins.typedrequest.TypedResponseError('No identity provided');
}
const passed = await authHandler.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 }>(
authHandler: AuthHandler,
dataArg: T,
): Promise<void> {
if (!dataArg.identity) {
throw new plugins.typedrequest.TypedResponseError('No identity provided');
}
const passed = await authHandler.adminIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Admin access required');
}
}