AI Implementation feature(823): Landing Page and Login/Register Modal (#11)

This commit was merged in pull request #11.
This commit is contained in:
2026-07-21 18:34:45 +00:00
parent 8476e4a59f
commit 685a8bca84
51 changed files with 2354 additions and 226 deletions
+50 -1
View File
@@ -16,8 +16,10 @@ import { ApiError } from '../../common/errors/api-error';
import { ERROR_CODES } from '../../common/errors/error-codes';
import { LoginBackoffService } from '../../common/services/login-backoff.service';
import { RegistrationRateLimitService } from '../../common/services/registration-rate-limit.service';
import { LoginDto, LoginResponse } from './dto/auth.dto';
import { LoginDto, LoginResponse, RegisterDto } from './dto/auth.dto';
import { validatePassword } from '../../common/utils/password-policy';
import { SettingsService } from '../settings/settings.module';
import { SETTINGS_KEYS } from '../../config/env.schema';
const REFRESH_TTL_SECONDS_DEFAULT = 7 * 24 * 60 * 60;
@@ -34,6 +36,7 @@ export class AuthService implements OnModuleInit {
private readonly dataSource: DataSource,
private readonly backoff: LoginBackoffService,
private readonly registrationRateLimit: RegistrationRateLimitService,
private readonly settings: SettingsService,
) {}
onModuleInit(): void {
@@ -130,6 +133,52 @@ export class AuthService implements OnModuleInit {
});
}
async registerPlayer(dto: RegisterDto, ip: string): Promise<LoginResponse> {
validatePassword(dto.password, this.config);
if (!this.registrationRateLimit.isAllowed(ip)) {
throw new ApiError(
ERROR_CODES.RATE_LIMITED,
'Too many registration attempts; please wait a minute before trying again.',
429,
);
}
const enabled = (await this.settings.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false')) === 'true';
if (!enabled) {
throw new ApiError(
ERROR_CODES.REGISTRATIONS_DISABLED,
'Registrations are currently disabled',
403,
);
}
return this.dataSource.transaction(async (manager) => {
const existing = await manager.findOne(UserEntity, { where: { username: dto.username } });
if (existing) {
throw new ApiError(ERROR_CODES.USERNAME_TAKEN, 'Username already taken', 409);
}
const passwordHash = await argon2.hash(dto.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: dto.username,
passwordHash,
role: 'player',
status: 'enabled',
});
await manager.save(user);
this.registrationRateLimit.record(ip);
return this.mintSession(manager, user);
});
}
/**
* Mint a fresh access JWT and a new refresh token row inside the supplied
* transaction. The refresh token (raw) is returned alongside the access