fix: Resolve 500/404 cascade — JWT tenant context, wipe routes, changelog stub
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Root cause: super_admin JWT returned early with no license_id, causing
@CurrentTenant() to pass undefined to every tenant-scoped service query.
- jwt.strategy: Move license lookup before super_admin early return so
admins who own licenses get their license_id in the JWT payload
- CurrentTenant decorator: Throw 401 with clear message when license_id
is undefined instead of letting undefined cascade into TypeORM queries
- Wipe store: Fix 6 wrong routes (/profiles → /wipes/profiles, etc.)
and remove redundant manual license_id guards
- Changelog module: Add stub controller/service returning empty array
to eliminate 404 on /api/changelog
- ChangelogView: Handle both array and {entries} response shapes
- AGENTS.md: Streamlined 3-tier roster (Opus/Sonnet/Haiku)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import { WebstoreModule } from './modules/webstore/webstore.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { SetupModule } from './modules/setup/setup.module';
|
||||
import { MigrationModule } from './modules/migration/migration.module';
|
||||
import { ChangelogModule } from './modules/changelog/changelog.module';
|
||||
|
||||
// Shared Services
|
||||
import { NatsService } from './services/nats.service';
|
||||
@@ -103,6 +104,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway';
|
||||
AdminModule,
|
||||
SetupModule,
|
||||
MigrationModule,
|
||||
ChangelogModule,
|
||||
],
|
||||
providers: [
|
||||
// Global guards (order matters: auth first, then license, then permissions)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
export const CurrentTenant = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user?.license_id;
|
||||
const licenseId = request.user?.license_id;
|
||||
if (!licenseId) {
|
||||
throw new UnauthorizedException(
|
||||
'No license associated with this account. Create or join a server first.',
|
||||
);
|
||||
}
|
||||
return licenseId;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -52,16 +52,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
last_login_at: new Date(),
|
||||
});
|
||||
|
||||
// If super admin, return basic payload
|
||||
if (user.is_super_admin) {
|
||||
return {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
is_super_admin: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Find user's license - either as owner or team member
|
||||
let license: License | null = null;
|
||||
let role: Role | null = null;
|
||||
@@ -76,8 +66,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
role = await this.roleRepository.findOne({
|
||||
where: { role_name: 'Owner', is_system_default: true },
|
||||
});
|
||||
} else {
|
||||
// Check if user is a team member
|
||||
} else if (!user.is_super_admin) {
|
||||
// Check if user is a team member (skip for super admins without a license)
|
||||
const teamMember = await this.teamMemberRepository.findOne({
|
||||
where: { user_id: user.id },
|
||||
relations: ['license', 'role'],
|
||||
@@ -93,7 +83,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
is_super_admin: false,
|
||||
is_super_admin: user.is_super_admin,
|
||||
license_id: license?.id,
|
||||
permissions: role?.permissions || {},
|
||||
};
|
||||
|
||||
24
backend-nest/src/modules/changelog/changelog.controller.ts
Normal file
24
backend-nest/src/modules/changelog/changelog.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { ChangelogService } from './changelog.service';
|
||||
import { Public } from '../../common/decorators/public.decorator';
|
||||
|
||||
@ApiTags('changelog')
|
||||
@Controller('changelog')
|
||||
export class ChangelogController {
|
||||
constructor(private readonly changelogService: ChangelogService) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get changelog entries' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
async getEntries(
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const p = parseInt(page || '1', 10);
|
||||
const l = parseInt(limit || '20', 10);
|
||||
return this.changelogService.getEntries(p, l);
|
||||
}
|
||||
}
|
||||
9
backend-nest/src/modules/changelog/changelog.module.ts
Normal file
9
backend-nest/src/modules/changelog/changelog.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ChangelogController } from './changelog.controller';
|
||||
import { ChangelogService } from './changelog.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ChangelogController],
|
||||
providers: [ChangelogService],
|
||||
})
|
||||
export class ChangelogModule {}
|
||||
21
backend-nest/src/modules/changelog/changelog.service.ts
Normal file
21
backend-nest/src/modules/changelog/changelog.service.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export interface ChangelogEntry {
|
||||
id: string;
|
||||
version: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'feature' | 'fix' | 'improvement' | 'breaking';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ChangelogService {
|
||||
async getEntries(
|
||||
page: number,
|
||||
limit: number,
|
||||
): Promise<{ entries: ChangelogEntry[]; total: number }> {
|
||||
// Stub — returns empty until changelog entries are seeded or managed
|
||||
return { entries: [], total: 0 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user