2025-11-27 22:15:38 +00:00
|
|
|
/**
|
|
|
|
|
* ApiToken model for Stack.Gallery Registry
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
|
|
|
import type { IApiToken, ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
2025-11-27 23:47:33 +00:00
|
|
|
import { db } from './db.ts';
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2025-11-27 23:47:33 +00:00
|
|
|
@plugins.smartdata.Collection(() => db)
|
|
|
|
|
export class ApiToken extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken> implements IApiToken {
|
2025-11-27 22:15:38 +00:00
|
|
|
@plugins.smartdata.unI()
|
|
|
|
|
public id: string = '';
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
@plugins.smartdata.index()
|
|
|
|
|
public userId: string = '';
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public name: string = '';
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
@plugins.smartdata.index({ unique: true })
|
|
|
|
|
public tokenHash: string = '';
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public tokenPrefix: string = '';
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public protocols: TRegistryProtocol[] = [];
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public scopes: ITokenScope[] = [];
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
@plugins.smartdata.index()
|
|
|
|
|
public expiresAt?: Date;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public lastUsedAt?: Date;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public lastUsedIp?: string;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public usageCount: number = 0;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
@plugins.smartdata.index()
|
|
|
|
|
public isRevoked: boolean = false;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public revokedAt?: Date;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public revokedReason?: string;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
@plugins.smartdata.index()
|
|
|
|
|
public createdAt: Date = new Date();
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
|
|
|
|
public createdIp?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find token by hash
|
|
|
|
|
*/
|
|
|
|
|
public static async findByHash(tokenHash: string): Promise<ApiToken | null> {
|
|
|
|
|
return await ApiToken.getInstance({
|
|
|
|
|
tokenHash,
|
|
|
|
|
isRevoked: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find token by prefix (for listing)
|
|
|
|
|
*/
|
|
|
|
|
public static async findByPrefix(tokenPrefix: string): Promise<ApiToken | null> {
|
|
|
|
|
return await ApiToken.getInstance({
|
|
|
|
|
tokenPrefix,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all tokens for a user
|
|
|
|
|
*/
|
|
|
|
|
public static async getUserTokens(userId: string): Promise<ApiToken[]> {
|
|
|
|
|
return await ApiToken.getInstances({
|
|
|
|
|
userId,
|
|
|
|
|
isRevoked: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if token is valid (not expired, not revoked)
|
|
|
|
|
*/
|
|
|
|
|
public isValid(): boolean {
|
|
|
|
|
if (this.isRevoked) return false;
|
|
|
|
|
if (this.expiresAt && this.expiresAt < new Date()) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Record token usage
|
|
|
|
|
*/
|
|
|
|
|
public async recordUsage(ip?: string): Promise<void> {
|
|
|
|
|
this.lastUsedAt = new Date();
|
|
|
|
|
this.lastUsedIp = ip;
|
|
|
|
|
this.usageCount += 1;
|
|
|
|
|
await this.save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Revoke token
|
|
|
|
|
*/
|
|
|
|
|
public async revoke(reason?: string): Promise<void> {
|
|
|
|
|
this.isRevoked = true;
|
|
|
|
|
this.revokedAt = new Date();
|
|
|
|
|
this.revokedReason = reason;
|
|
|
|
|
await this.save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if token has permission for protocol
|
|
|
|
|
*/
|
|
|
|
|
public hasProtocol(protocol: TRegistryProtocol): boolean {
|
|
|
|
|
return this.protocols.includes(protocol) || this.protocols.includes('*' as TRegistryProtocol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if token has permission for action on resource
|
|
|
|
|
*/
|
|
|
|
|
public hasScope(
|
|
|
|
|
protocol: TRegistryProtocol,
|
|
|
|
|
organizationId?: string,
|
|
|
|
|
repositoryId?: string,
|
|
|
|
|
action?: string
|
|
|
|
|
): boolean {
|
|
|
|
|
for (const scope of this.scopes) {
|
|
|
|
|
// Check protocol
|
|
|
|
|
if (scope.protocol !== '*' && scope.protocol !== protocol) continue;
|
|
|
|
|
|
|
|
|
|
// Check organization
|
|
|
|
|
if (scope.organizationId && scope.organizationId !== organizationId) continue;
|
|
|
|
|
|
|
|
|
|
// Check repository
|
|
|
|
|
if (scope.repositoryId && scope.repositoryId !== repositoryId) continue;
|
|
|
|
|
|
|
|
|
|
// Check action
|
|
|
|
|
if (action && !scope.actions.includes('*') && !scope.actions.includes(action as never)) continue;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lifecycle hook
|
|
|
|
|
*/
|
|
|
|
|
public async beforeSave(): Promise<void> {
|
|
|
|
|
if (!this.id) {
|
|
|
|
|
this.id = await ApiToken.getNewId();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|