199 lines
8.0 KiB
TypeScript
199 lines
8.0 KiB
TypeScript
|
|
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');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|