From e1a3ea3b785b9d78df0eb5795db3dc05990511ab Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 21 Feb 2026 13:28:48 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20Wave=202=20=E2=80=94=20entities,=20secu?= =?UTF-8?q?rity=20guards,=20API=20key=20encryption=20(15=20files)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entities: - Create 5 new TypeORM entities: webstore_config, webstore_categories, webstore_items, webstore_transactions, module_store (all verified against live DB) - Fix wipe-profile entity: remove incorrect default {} for pre/post wipe configs Security: - Add @RequirePermission guards to 7 controllers (36 endpoints total): team, webstore, notifications, alerts, analytics, settings, schedules - Encrypt panel API key with AES-256-GCM in setup service (was plaintext) Co-Authored-By: Claude Opus 4.6 --- .../src/entities/module-store.entity.ts | 52 +++++++++++++++++++ .../src/entities/webstore-category.entity.ts | 24 +++++++++ .../src/entities/webstore-config.entity.ts | 39 ++++++++++++++ .../src/entities/webstore-item.entity.ts | 47 +++++++++++++++++ .../entities/webstore-transaction.entity.ts | 47 +++++++++++++++++ .../src/entities/wipe-profile.entity.ts | 4 +- .../src/modules/alerts/alerts.controller.ts | 4 ++ .../modules/analytics/analytics.controller.ts | 8 +++ .../notifications/notifications.controller.ts | 3 ++ .../modules/schedules/schedules.controller.ts | 5 ++ .../modules/settings/settings.controller.ts | 4 ++ .../src/modules/setup/setup.service.ts | 15 +++++- .../src/modules/team/team.controller.ts | 7 +++ .../modules/webstore/webstore.controller.ts | 12 +++++ corrosion-final-push.md | 2 +- 15 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 backend-nest/src/entities/module-store.entity.ts create mode 100644 backend-nest/src/entities/webstore-category.entity.ts create mode 100644 backend-nest/src/entities/webstore-config.entity.ts create mode 100644 backend-nest/src/entities/webstore-item.entity.ts create mode 100644 backend-nest/src/entities/webstore-transaction.entity.ts diff --git a/backend-nest/src/entities/module-store.entity.ts b/backend-nest/src/entities/module-store.entity.ts new file mode 100644 index 0000000..e6b4ea1 --- /dev/null +++ b/backend-nest/src/entities/module-store.entity.ts @@ -0,0 +1,52 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('module_store') +export class ModuleStore { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', unique: true }) + module_slug: string; + + @Column({ type: 'varchar' }) + module_name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'text', nullable: true }) + long_description: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) + price: number; + + @Column({ type: 'varchar', default: 'one_time' }) + price_type: string; + + @Column({ type: 'decimal', precision: 10, scale: 2, nullable: true }) + monthly_price: number | null; + + @Column({ type: 'varchar', default: '1.0.0' }) + version: string; + + @Column({ type: 'text', nullable: true }) + download_path: string | null; + + @Column({ type: 'text', nullable: true }) + thumbnail_url: string | null; + + @Column({ type: 'text', array: true, nullable: true, default: () => "'{}'" }) + screenshots: string[] | null; + + @Column({ type: 'varchar', nullable: true }) + category: string | null; + + @Column({ type: 'boolean', default: true }) + is_active: boolean; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + created_at: Date; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + updated_at: Date; +} diff --git a/backend-nest/src/entities/webstore-category.entity.ts b/backend-nest/src/entities/webstore-category.entity.ts new file mode 100644 index 0000000..0f2db95 --- /dev/null +++ b/backend-nest/src/entities/webstore-category.entity.ts @@ -0,0 +1,24 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { License } from './license.entity'; + +@Entity('webstore_categories') +export class WebstoreCategory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + license_id: string; + + @Column({ type: 'varchar' }) + category_name: string; + + @Column({ type: 'integer', default: 0 }) + display_order: number; + + @Column({ type: 'boolean', default: true }) + is_active: boolean; + + @ManyToOne(() => License, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'license_id' }) + license: License; +} diff --git a/backend-nest/src/entities/webstore-config.entity.ts b/backend-nest/src/entities/webstore-config.entity.ts new file mode 100644 index 0000000..a9b921d --- /dev/null +++ b/backend-nest/src/entities/webstore-config.entity.ts @@ -0,0 +1,39 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { License } from './license.entity'; + +@Entity('webstore_config') +export class WebstoreConfig { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + license_id: string; + + @Column({ type: 'boolean', default: false }) + is_active: boolean; + + @Column({ type: 'text', nullable: true }) + paypal_client_id: string | null; + + @Column({ type: 'text', nullable: true }) + paypal_secret: string | null; + + @Column({ type: 'varchar', default: 'sandbox' }) + paypal_mode: string; + + @Column({ type: 'varchar', nullable: true }) + store_name: string | null; + + @Column({ type: 'text', nullable: true }) + store_description: string | null; + + @Column({ type: 'varchar', default: 'USD' }) + currency: string; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + created_at: Date; + + @ManyToOne(() => License, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'license_id' }) + license: License; +} diff --git a/backend-nest/src/entities/webstore-item.entity.ts b/backend-nest/src/entities/webstore-item.entity.ts new file mode 100644 index 0000000..d85ffec --- /dev/null +++ b/backend-nest/src/entities/webstore-item.entity.ts @@ -0,0 +1,47 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { License } from './license.entity'; +import { WebstoreCategory } from './webstore-category.entity'; + +@Entity('webstore_items') +export class WebstoreItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + license_id: string; + + @Column({ type: 'uuid' }) + category_id: string; + + @Column({ type: 'varchar' }) + item_name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + price: number; + + @Column({ type: 'text', nullable: true }) + image_url: string | null; + + @Column({ type: 'varchar', default: 'kit' }) + item_type: string; + + @Column({ type: 'jsonb' }) + delivery_config: Record; + + @Column({ type: 'boolean', default: true }) + is_active: boolean; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + created_at: Date; + + @ManyToOne(() => License, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'license_id' }) + license: License; + + @ManyToOne(() => WebstoreCategory, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'category_id' }) + category: WebstoreCategory; +} diff --git a/backend-nest/src/entities/webstore-transaction.entity.ts b/backend-nest/src/entities/webstore-transaction.entity.ts new file mode 100644 index 0000000..486fa64 --- /dev/null +++ b/backend-nest/src/entities/webstore-transaction.entity.ts @@ -0,0 +1,47 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { License } from './license.entity'; +import { WebstoreItem } from './webstore-item.entity'; + +@Entity('webstore_transactions') +export class WebstoreTransaction { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + license_id: string; + + @Column({ type: 'uuid' }) + item_id: string; + + @Column({ type: 'varchar' }) + buyer_steam_id: string; + + @Column({ type: 'varchar', nullable: true }) + buyer_name: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + amount: number; + + @Column({ type: 'varchar', default: 'USD' }) + currency: string; + + @Column({ type: 'varchar', nullable: true }) + paypal_transaction_id: string | null; + + @Column({ type: 'varchar', default: 'pending' }) + status: string; + + @Column({ type: 'timestamptz', nullable: true }) + delivered_at: Date | null; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + created_at: Date; + + @ManyToOne(() => License, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'license_id' }) + license: License; + + @ManyToOne(() => WebstoreItem, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'item_id' }) + item: WebstoreItem; +} diff --git a/backend-nest/src/entities/wipe-profile.entity.ts b/backend-nest/src/entities/wipe-profile.entity.ts index 22de562..3460cc1 100644 --- a/backend-nest/src/entities/wipe-profile.entity.ts +++ b/backend-nest/src/entities/wipe-profile.entity.ts @@ -15,10 +15,10 @@ export class WipeProfile { @Column({ type: 'text', nullable: true }) description: string | null; - @Column({ type: 'jsonb', default: {} }) + @Column({ type: 'jsonb' }) pre_wipe_config: Record; - @Column({ type: 'jsonb', default: {} }) + @Column({ type: 'jsonb' }) post_wipe_config: Record; @Column({ type: 'timestamptz', default: () => 'NOW()' }) diff --git a/backend-nest/src/modules/alerts/alerts.controller.ts b/backend-nest/src/modules/alerts/alerts.controller.ts index 68b47e3..0fa42d7 100644 --- a/backend-nest/src/modules/alerts/alerts.controller.ts +++ b/backend-nest/src/modules/alerts/alerts.controller.ts @@ -3,6 +3,7 @@ import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger' import { AlertsService } from './alerts.service'; import { UpdateAlertConfigDto } from './dto/update-alert-config.dto'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; @ApiTags('alerts') @ApiBearerAuth() @@ -11,12 +12,14 @@ export class AlertsController { constructor(private readonly alertsService: AlertsService) {} @Get('config') + @RequirePermission('alerts.view') @ApiOperation({ summary: 'Get alert configuration' }) async getConfig(@CurrentTenant() licenseId: string) { return this.alertsService.getConfig(licenseId); } @Put('config') + @RequirePermission('alerts.manage') @ApiOperation({ summary: 'Update alert configuration' }) async updateConfig( @CurrentTenant() licenseId: string, @@ -26,6 +29,7 @@ export class AlertsController { } @Get('history') + @RequirePermission('alerts.view') @ApiOperation({ summary: 'Get alert history' }) @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Max records to return (default: 50)' }) async getHistory( diff --git a/backend-nest/src/modules/analytics/analytics.controller.ts b/backend-nest/src/modules/analytics/analytics.controller.ts index d852140..4b9d8dd 100644 --- a/backend-nest/src/modules/analytics/analytics.controller.ts +++ b/backend-nest/src/modules/analytics/analytics.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Query, Header } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { AnalyticsService } from './analytics.service'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; @ApiTags('analytics') @ApiBearerAuth() @@ -10,6 +11,7 @@ export class AnalyticsController { constructor(private readonly analyticsService: AnalyticsService) {} @Get('summary') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get analytics summary for time range' }) @ApiQuery({ name: 'range', required: false, type: Number, description: 'Hours to analyze (default: 24)' }) async getSummary( @@ -21,6 +23,7 @@ export class AnalyticsController { } @Get('timeseries') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get timeseries data for charts' }) @ApiQuery({ name: 'range', required: false, type: Number }) @ApiQuery({ name: 'granularity', required: false, enum: ['raw', 'hourly'] }) @@ -35,6 +38,7 @@ export class AnalyticsController { } @Get('wipes/performance') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get wipe performance metrics' }) @ApiQuery({ name: 'range', required: false, type: Number, description: 'Hours (default: 720 = 30 days)' }) async getWipePerformance( @@ -46,6 +50,7 @@ export class AnalyticsController { } @Get('maps') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get map usage analytics' }) @ApiQuery({ name: 'range', required: false, type: Number }) async getMapAnalytics( @@ -57,6 +62,7 @@ export class AnalyticsController { } @Get('players') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get player analytics' }) @ApiQuery({ name: 'range', required: false, type: Number }) @ApiQuery({ name: 'metric', required: false, enum: ['sessions', 'retention'] }) @@ -71,6 +77,7 @@ export class AnalyticsController { } @Get('retention') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Get player retention across wipes' }) @ApiQuery({ name: 'wipe_count', required: false, type: Number, description: 'Number of recent wipes (default: 5)' }) async getRetention( @@ -82,6 +89,7 @@ export class AnalyticsController { } @Get('export') + @RequirePermission('analytics.view') @ApiOperation({ summary: 'Export analytics data as CSV' }) @ApiQuery({ name: 'range', required: false, type: Number }) @Header('Content-Type', 'text/csv') diff --git a/backend-nest/src/modules/notifications/notifications.controller.ts b/backend-nest/src/modules/notifications/notifications.controller.ts index 704fc2b..37fe0fa 100644 --- a/backend-nest/src/modules/notifications/notifications.controller.ts +++ b/backend-nest/src/modules/notifications/notifications.controller.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { NotificationsService } from './notifications.service'; import { UpdateConfigDto } from './dto/update-config.dto'; @@ -18,6 +19,7 @@ export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @Get('config') + @RequirePermission('notifications.view') @ApiOperation({ summary: 'Get notification configuration', description: 'Returns notification settings for this license', @@ -32,6 +34,7 @@ export class NotificationsController { } @Put('config') + @RequirePermission('notifications.manage') @ApiOperation({ summary: 'Update notification configuration', description: 'Update notification settings for this license', diff --git a/backend-nest/src/modules/schedules/schedules.controller.ts b/backend-nest/src/modules/schedules/schedules.controller.ts index 24dd447..a8db5d5 100644 --- a/backend-nest/src/modules/schedules/schedules.controller.ts +++ b/backend-nest/src/modules/schedules/schedules.controller.ts @@ -16,6 +16,7 @@ import { } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { SchedulesService } from './schedules.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { UpdateTaskDto } from './dto/update-task.dto'; @@ -28,6 +29,7 @@ export class SchedulesController { constructor(private readonly schedulesService: SchedulesService) {} @Get('tasks') + @RequirePermission('schedules.view') @ApiOperation({ summary: 'Get all scheduled tasks', description: 'Returns all scheduled tasks for this license', @@ -41,6 +43,7 @@ export class SchedulesController { } @Post('tasks') + @RequirePermission('schedules.manage') @ApiOperation({ summary: 'Create a scheduled task', description: 'Create a new scheduled task (restart, announcement, command, or plugin reload)', @@ -61,6 +64,7 @@ export class SchedulesController { } @Put('tasks/:id') + @RequirePermission('schedules.manage') @ApiOperation({ summary: 'Update a scheduled task', description: 'Update task configuration, schedule, or settings', @@ -82,6 +86,7 @@ export class SchedulesController { } @Delete('tasks/:id') + @RequirePermission('schedules.manage') @ApiOperation({ summary: 'Delete a scheduled task', description: 'Remove a scheduled task and unregister from scheduler', diff --git a/backend-nest/src/modules/settings/settings.controller.ts b/backend-nest/src/modules/settings/settings.controller.ts index 24fd208..95b583b 100644 --- a/backend-nest/src/modules/settings/settings.controller.ts +++ b/backend-nest/src/modules/settings/settings.controller.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { SettingsService } from './settings.service'; import { UpdatePublicSiteDto } from './dto/update-public-site.dto'; import { UpdateDomainDto } from './dto/update-domain.dto'; @@ -19,6 +20,7 @@ export class SettingsController { constructor(private readonly settingsService: SettingsService) {} @Get('public-site') + @RequirePermission('settings.view') @ApiOperation({ summary: 'Get public site configuration', description: 'Returns public site settings for this license', @@ -32,6 +34,7 @@ export class SettingsController { } @Put('public-site') + @RequirePermission('settings.manage') @ApiOperation({ summary: 'Update public site configuration', description: 'Update public site settings for this license', @@ -48,6 +51,7 @@ export class SettingsController { } @Put('domain') + @RequirePermission('settings.manage') @ApiOperation({ summary: 'Update domain settings', description: 'Update subdomain or custom domain for this license', diff --git a/backend-nest/src/modules/setup/setup.service.ts b/backend-nest/src/modules/setup/setup.service.ts index e5be423..3c0ab37 100644 --- a/backend-nest/src/modules/setup/setup.service.ts +++ b/backend-nest/src/modules/setup/setup.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ServerConnection } from '../../entities/server-connection.entity'; @@ -12,6 +13,7 @@ import * as crypto from 'crypto'; @Injectable() export class SetupService { constructor( + private readonly configService: ConfigService, @InjectRepository(ServerConnection) private readonly connectionRepo: Repository, @InjectRepository(ServerConfig) @@ -51,8 +53,17 @@ export class SetupService { // Store encrypted API key if provided if (dto.panel_api_key) { - // Stub - would encrypt in production - connection.panel_api_key_encrypted = dto.panel_api_key; + const encryptionKey = this.configService.get('encryption.key', ''); + const keyBuffer = Buffer.from(encryptionKey, 'hex'); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv); + const encrypted = Buffer.concat([ + cipher.update(dto.panel_api_key, 'utf8'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + connection.panel_api_key_encrypted = + `${iv.toString('hex')}:${encrypted.toString('hex')}:${authTag.toString('hex')}`; } connection.updated_at = new Date(); diff --git a/backend-nest/src/modules/team/team.controller.ts b/backend-nest/src/modules/team/team.controller.ts index 1375678..9e725b7 100644 --- a/backend-nest/src/modules/team/team.controller.ts +++ b/backend-nest/src/modules/team/team.controller.ts @@ -17,6 +17,7 @@ import { import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { TeamService } from './team.service'; import { InviteMemberDto } from './dto/invite-member.dto'; import { CreateRoleDto } from './dto/create-role.dto'; @@ -30,6 +31,7 @@ export class TeamController { constructor(private readonly teamService: TeamService) {} @Get() + @RequirePermission('team.view') @ApiOperation({ summary: 'Get team members and roles', description: 'Returns all team members with their roles and all available roles', @@ -43,6 +45,7 @@ export class TeamController { } @Post('invite') + @RequirePermission('team.manage') @ApiOperation({ summary: 'Invite a team member', description: 'Invite a user by email and assign them a role', @@ -68,6 +71,7 @@ export class TeamController { } @Delete(':id') + @RequirePermission('team.manage') @ApiOperation({ summary: 'Remove a team member', description: 'Remove a team member by ID', @@ -88,6 +92,7 @@ export class TeamController { } @Post('roles') + @RequirePermission('team.manage') @ApiOperation({ summary: 'Create a custom role', description: 'Create a new custom role for this license', @@ -108,6 +113,7 @@ export class TeamController { } @Put('roles/:id') + @RequirePermission('team.manage') @ApiOperation({ summary: 'Update a role', description: 'Update role permissions and details', @@ -133,6 +139,7 @@ export class TeamController { } @Delete('roles/:id') + @RequirePermission('team.manage') @ApiOperation({ summary: 'Delete a role', description: 'Delete a custom role (cannot delete system roles or roles in use)', diff --git a/backend-nest/src/modules/webstore/webstore.controller.ts b/backend-nest/src/modules/webstore/webstore.controller.ts index a589b6d..f1427d9 100644 --- a/backend-nest/src/modules/webstore/webstore.controller.ts +++ b/backend-nest/src/modules/webstore/webstore.controller.ts @@ -7,6 +7,7 @@ import { CreateItemDto } from './dto/create-item.dto'; import { PurchaseDto } from './dto/purchase.dto'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; import { Public } from '../../common/decorators/public.decorator'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator'; @ApiTags('webstore') @Controller() @@ -15,6 +16,7 @@ export class WebstoreController { // Admin Routes @Get('webstore/config') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Get webstore configuration' }) async getConfig(@CurrentTenant() licenseId: string) { @@ -23,6 +25,7 @@ export class WebstoreController { } @Put('webstore/config') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Update webstore configuration' }) async updateConfig( @@ -34,6 +37,7 @@ export class WebstoreController { } @Get('webstore/categories') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Get all store categories' }) async getCategories(@CurrentTenant() licenseId: string) { @@ -41,6 +45,7 @@ export class WebstoreController { } @Post('webstore/categories') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Create a new category' }) async createCategory( @@ -51,6 +56,7 @@ export class WebstoreController { } @Put('webstore/categories/:id') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Update a category' }) @ApiParam({ name: 'id', description: 'Category ID' }) @@ -63,6 +69,7 @@ export class WebstoreController { } @Delete('webstore/categories/:id') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Delete a category' }) @ApiParam({ name: 'id', description: 'Category ID' }) @@ -75,6 +82,7 @@ export class WebstoreController { } @Get('webstore/items') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Get all store items' }) async getItems(@CurrentTenant() licenseId: string) { @@ -82,6 +90,7 @@ export class WebstoreController { } @Post('webstore/items') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Create a new store item' }) async createItem( @@ -92,6 +101,7 @@ export class WebstoreController { } @Put('webstore/items/:id') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Update a store item' }) @ApiParam({ name: 'id', description: 'Item ID' }) @@ -104,6 +114,7 @@ export class WebstoreController { } @Delete('webstore/items/:id') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Delete a store item' }) @ApiParam({ name: 'id', description: 'Item ID' }) @@ -116,6 +127,7 @@ export class WebstoreController { } @Get('webstore/transactions') + @RequirePermission('store.manage') @ApiBearerAuth() @ApiOperation({ summary: 'Get all store transactions' }) async getTransactions(@CurrentTenant() licenseId: string) { diff --git a/corrosion-final-push.md b/corrosion-final-push.md index 01b4ec7..8ed9b47 100644 --- a/corrosion-final-push.md +++ b/corrosion-final-push.md @@ -21,7 +21,7 @@ | Wave | Focus | Agents | Status | |------|-------|--------|--------| | 1 | Critical Bug Fixes | 3 Sonnet parallel | COMPLETE | -| 2 | Missing Entities + Security | 2 Sonnet parallel | PENDING | +| 2 | Missing Entities + Security | 2 Sonnet parallel | COMPLETE | | 3 | Frontend Wiring | 3 Sonnet parallel | PENDING | | 4 | Backend Completion | 2 Sonnet parallel | PENDING | | 5 | Docker + Polish | 1 Sonnet | PENDING |