AI Implementation feature(846): First-Start Create Admin Modal 1.05 (#8)

This commit was merged in pull request #8.
This commit is contained in:
2026-07-21 17:18:54 +00:00
parent cabd393288
commit 2ae930f325
8 changed files with 208 additions and 203 deletions
+66 -30
View File
@@ -20,6 +20,7 @@ const REFRESH_TTL_DEFAULT = 7 * 24 * 60 * 60;
export class SetupService implements OnModuleInit {
private readonly logger = new Logger(SetupService.name);
private refreshTtlSeconds = REFRESH_TTL_DEFAULT;
private bootstrapChain: Promise<unknown> = Promise.resolve();
constructor(
@InjectRepository(UserEntity) private readonly users: Repository<UserEntity>,
@@ -55,42 +56,77 @@ export class SetupService implements OnModuleInit {
);
}
return this.dataSource.transaction(async (manager) => {
const adminCount = await manager.count(UserEntity, { where: { role: 'admin' } });
if (adminCount > 0) {
throw ApiError.notFound('Setup endpoint not available');
}
return this.runBootstrap(() =>
this.dataSource.transaction(async (manager) => {
const adminCount = await manager.count(UserEntity, { where: { role: 'admin' } });
if (adminCount > 0) {
throw new ApiError(ERROR_CODES.SYSTEM_INITIALIZED, 'System already initialized', 409);
}
const existing = await manager.findOne(UserEntity, { where: { username } });
if (existing) {
throw ApiError.conflict(ERROR_CODES.USERNAME_TAKEN, 'Username already exists');
}
const existing = await manager.findOne(UserEntity, { where: { username } });
if (existing) {
throw ApiError.conflict(ERROR_CODES.USERNAME_TAKEN, 'Username already exists');
}
const passwordHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: this.config.get<number>('ARGON2_MEMORY_COST'),
timeCost: this.config.get<number>('ARGON2_TIME_COST'),
parallelism: this.config.get<number>('ARGON2_PARALLELISM'),
});
const passwordHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: this.config.get<number>('ARGON2_MEMORY_COST'),
timeCost: this.config.get<number>('ARGON2_TIME_COST'),
parallelism: this.config.get<number>('ARGON2_PARALLELISM'),
});
const user = manager.create(UserEntity, {
id: uuid(),
username,
passwordHash,
role: 'admin',
status: 'enabled',
});
await manager.save(user);
const user = manager.create(UserEntity, {
id: uuid(),
username,
passwordHash,
role: 'admin',
status: 'enabled',
});
await manager.save(user);
this.registrationRateLimit.record(ip);
this.logger.log(`First admin created (username=${username})`);
this.registrationRateLimit.record(ip);
this.logger.log(`First admin created (username=${username})`);
return AuthService.createSession(
manager,
{ jwt: this.jwt, config: this.config, refreshTtlSeconds: this.refreshTtlSeconds },
user,
);
return AuthService.createSession(
manager,
{ jwt: this.jwt, config: this.config, refreshTtlSeconds: this.refreshTtlSeconds },
user,
);
}),
);
}
/**
* Serialize first-admin bootstrap attempts within this Nest process so
* that two concurrent callers cannot both observe an empty admin table
* and create two enabled admins. The winner is whichever call the
* promise chain reaches first; subsequent callers wait for that call to
* settle, then re-execute and observe the now-non-empty admin table and
* receive a 409 SYSTEM_INITIALIZED error.
*
* The chain never short-circuits: every caller runs its callback exactly
* once, which keeps validation/rate-limit semantics identical to the
* single-request case while still guaranteeing serialized execution.
*/
private async runBootstrap<T>(task: () => Promise<T>): Promise<T> {
const previous = this.bootstrapChain;
let release: () => void = () => {};
const gate = new Promise<void>((resolve) => {
release = resolve;
});
this.bootstrapChain = previous.then(() => gate, () => gate);
try {
await previous;
} catch {
// Swallow previous bootstrap errors so they do not poison the chain;
// the throwing caller already observed its own error.
}
try {
return await task();
} finally {
release();
}
}
private parseTtl(ttl: string): number {