Add time-limited external access for optional services and database.

Users can open temporary NodePort access with auto-revoke via Bull jobs and a dashboard UI to manage active grants.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-15 13:58:08 +03:30
parent c7981074d4
commit 2303985d0c
13 changed files with 1015 additions and 19 deletions
@@ -27,6 +27,8 @@ import { Roles } from '../common/decorators/roles.decorator';
import { UserRole, DatabaseType } from '../common/enums';
import { KubernetesService } from '../kubernetes/kubernetes.service';
import { DeploymentsService } from '../deployments/deployments.service';
import { AccessService } from '../access/access.service';
import { CreateServiceAccessDto } from '../access/dto/service-access.dto';
@ApiTags('Applications')
@ApiBearerAuth()
@@ -41,8 +43,17 @@ export class ApplicationsController {
private readonly kubernetesService: KubernetesService,
@Inject(forwardRef(() => DeploymentsService))
private readonly deploymentsService: DeploymentsService,
private readonly accessService: AccessService,
) {}
private isStaff(role: string): boolean {
return role === UserRole.ADMIN || role === UserRole.TECHNICAL;
}
private staffUserIdFilter(req: any): string | undefined {
return this.isStaff(req.user.role) ? undefined : req.user.id;
}
@Post()
@ApiOperation({ summary: 'Create a new application' })
async create(@Request() req: any, @Body() dto: CreateApplicationDto) {
@@ -268,13 +279,41 @@ export class ApplicationsController {
@Get(':id/preview')
@ApiOperation({ summary: 'Get preview URL for the deployed application' })
async getPreview(@Param('id') id: string, @Request() req: any) {
const app = await this.applicationsService.findOne(
id,
(req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL) ? undefined : req.user.id,
);
const app = await this.applicationsService.findOne(id, this.staffUserIdFilter(req));
return this.kubernetesService.getPreviewInfo(app);
}
@Post(':id/access')
@ApiOperation({ summary: 'Open temporary external access to database, Redis, or RabbitMQ' })
async createAccess(
@Param('id') id: string,
@Request() req: any,
@Body() dto: CreateServiceAccessDto,
) {
await this.applicationsService.findOne(id, this.staffUserIdFilter(req));
return this.accessService.createGrant(id, this.staffUserIdFilter(req), dto);
}
@Get(':id/access')
@ApiOperation({ summary: 'List active temporary access grants for an application' })
async listAccess(@Param('id') id: string, @Request() req: any) {
await this.applicationsService.findOne(id, this.staffUserIdFilter(req));
return this.accessService.listGrants(id, this.staffUserIdFilter(req));
}
@Delete(':id/access/:grantId')
@ApiOperation({ summary: 'Revoke temporary external access' })
async revokeAccess(
@Param('id') id: string,
@Param('grantId') grantId: string,
@Request() req: any,
) {
const app = await this.applicationsService.findOne(id, this.staffUserIdFilter(req));
await this.accessService.findGrantForApp(grantId, id, this.staffUserIdFilter(req));
await this.accessService.revokeGrant(grantId, this.staffUserIdFilter(req));
return { message: 'Access revoked' };
}
// ── Custom Domain ─────────────────────────────────────────────
@Post('domain/check-dns')
@@ -351,7 +390,14 @@ export class ApplicationsController {
// 1. Get the app first
const app = await this.applicationsService.findOne(id, isStaff ? undefined : req.user.id);
// 2. Delete K8s resources (deployment, service, ingress, db, secrets, PVCs)
// 2. Revoke temporary access grants
try {
await this.accessService.revokeAllForApplication(app.id);
} catch (e: any) {
this.logger.warn(`Access grant cleanup failed for ${app.name}: ${e.message}`);
}
// 3. Delete K8s resources (deployment, service, ingress, db, secrets, PVCs)
try {
await this.kubernetesService.deleteApplication(app);
this.logger.log(`Deleted K8s resources for ${app.name}`);
@@ -359,14 +405,14 @@ export class ApplicationsController {
this.logger.warn(`K8s cleanup failed for ${app.name}: ${e.message}`);
}
// 3. Delete deployment records from DB
// 4. Delete deployment records from DB
try {
await this.deploymentsService.deleteAllForApplication(app.id);
} catch (e: any) {
this.logger.warn(`Deployment records cleanup failed for ${app.name}: ${e.message}`);
}
// 4. Delete app (also deletes uploaded files)
// 5. Delete app (also deletes uploaded files)
await this.applicationsService.delete(id, isStaff ? app.userId : req.user.id);
return { message: `Application "${app.name}" and all resources deleted` };