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:
198
ts/opsserver/handlers/token.handler.ts
Normal file
198
ts/opsserver/handlers/token.handler.ts
Normal 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');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user