fix(audit): kill fake install cmds + dead demo CTA; production fonts; scoped error boundary; admin bootstrap seed
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

Full-site fake-data audit findings:
- SetupWizard showed a curl|sh installer (get.corrosionmgmt.com) and a
  'corrosion-agent' binary that don't exist -> real host-agent commands
- 'View live demo' CTA on 5 marketing pages linked to a login, not a
  demo -> honest 'Sign in'
- Google Fonts @import was silently dropped from the production CSS
  bundle (mid-bundle @import) -> <link> tags in index.html; prod was
  shipping system fallback fonts
- App-root ErrorBoundary bricked the entire SPA (incl. marketing) on a
  single failed fetch until manual reload -> resets on route change +
  content-scoped boundary inside DashboardLayout so nav chrome survives
- Status page KPIs showed fake zeros while the fetch failed -> em dash
- Login lacked the forgot-password link (flow already existed end-to-end)
- AdminSeedService: fresh DB had schema but no login possible; seeds
  super-admin + license from ADMIN_* env when users table is empty

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 09:23:44 -04:00
parent 180631989a
commit a8722a7a07
15 changed files with 162 additions and 32 deletions

View File

@@ -0,0 +1,82 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as argon2 from 'argon2';
import { randomBytes } from 'crypto';
import { User } from '../../entities/user.entity';
import { License } from '../../entities/license.entity';
/**
* Bootstraps the first admin account on a fresh database.
*
* A fresh deploy builds the schema via docker-entrypoint-initdb.d but contains
* zero users, so the panel has no possible login. If ADMIN_EMAIL and
* ADMIN_PASSWORD are set and the users table is empty, this creates a
* super-admin user plus an active license — the same rows the register flow
* would create. It never runs against a database that already has users.
*/
@Injectable()
export class AdminSeedService implements OnApplicationBootstrap {
private readonly logger = new Logger(AdminSeedService.name);
constructor(
private readonly config: ConfigService,
@InjectRepository(User) private readonly userRepository: Repository<User>,
@InjectRepository(License) private readonly licenseRepository: Repository<License>,
) {}
async onApplicationBootstrap(): Promise<void> {
try {
await this.seedAdminIfEmpty();
} catch (err) {
// A failed seed must not take the API down — surface it loudly and move on
this.logger.error(`Admin bootstrap failed: ${(err as Error).message}`, (err as Error).stack);
}
}
private async seedAdminIfEmpty(): Promise<void> {
const email = this.config.get<string>('admin.email');
const password = this.config.get<string>('admin.password');
const username = this.config.get<string>('admin.username') || 'Commander';
if (!email || !password) {
this.logger.log('Admin bootstrap skipped: ADMIN_EMAIL / ADMIN_PASSWORD not set');
return;
}
const userCount = await this.userRepository.count();
if (userCount > 0) {
return;
}
const password_hash = await argon2.hash(password);
const user = this.userRepository.create({
email: email.toLowerCase(),
username,
password_hash,
email_verified: true,
is_super_admin: true,
});
await this.userRepository.save(user);
const licenseKey = this.config.get<string>('admin.licenseKey') || this.generateLicenseKey();
const license = this.licenseRepository.create({
license_key: licenseKey,
owner_user_id: user.id,
status: 'active',
modules_enabled: [],
webstore_active: false,
});
await this.licenseRepository.save(license);
this.logger.log(`Bootstrap admin created: ${user.email} (license ${license.license_key})`);
}
private generateLicenseKey(): string {
const part1 = randomBytes(2).toString('hex').toUpperCase();
const part2 = randomBytes(2).toString('hex').toUpperCase();
const part3 = randomBytes(2).toString('hex').toUpperCase();
return `CORR-${part1}-${part2}-${part3}`;
}
}

View File

@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AdminSeedService } from './admin-seed.service';
import { JwtStrategy } from './jwt.strategy';
import { User } from '../../entities/user.entity';
import { License } from '../../entities/license.entity';
@@ -27,7 +28,7 @@ import { TeamMember } from '../../entities/team-member.entity';
TypeOrmModule.forFeature([User, License, Role, TeamMember]),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
providers: [AuthService, AdminSeedService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}