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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user