AI Implementation feature(857): Authenticated Shell: Header, Quick Tabs and Change Password (#14)

This commit was merged in pull request #14.
This commit is contained in:
2026-07-21 22:26:47 +00:00
parent 8dc8cee769
commit b6dbfcc511
46 changed files with 2705 additions and 211 deletions
+63 -1
View File
@@ -2,6 +2,8 @@ import {
Injectable,
OnModuleInit,
Logger,
Inject,
forwardRef,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@@ -16,10 +18,11 @@ 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, RegisterDto } from './dto/auth.dto';
import { ChangePasswordDto, 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';
import { UsersRankService } from '../users/users-rank.service';
const REFRESH_TTL_SECONDS_DEFAULT = 7 * 24 * 60 * 60;
@@ -37,6 +40,7 @@ export class AuthService implements OnModuleInit {
private readonly backoff: LoginBackoffService,
private readonly registrationRateLimit: RegistrationRateLimitService,
private readonly settings: SettingsService,
private readonly rank: UsersRankService,
) {}
onModuleInit(): void {
@@ -177,6 +181,64 @@ export class AuthService implements OnModuleInit {
});
}
async getMe(userId: string): Promise<{ id: string; username: string; role: 'admin' | 'player'; rank: number | null; points: number }> {
const user = await this.users.findOne({ where: { id: userId } });
if (!user) throw new ApiError(ERROR_CODES.NOT_FOUND, 'User not found', 404);
const rankInfo = await this.rank.rankOfUser(this.dataSource.manager, user.id);
return {
id: user.id,
username: user.username,
role: user.role,
rank: rankInfo.rank,
points: rankInfo.points,
};
}
async changePassword(userId: string, dto: ChangePasswordDto): Promise<void> {
if (dto.newPassword !== dto.confirmNewPassword) {
throw new ApiError(ERROR_CODES.PASSWORDS_DO_NOT_MATCH, 'Passwords do not match', 400);
}
if (dto.oldPassword === dto.newPassword) {
throw new ApiError(ERROR_CODES.PASSWORD_POLICY, 'New password must differ from the old one', 400);
}
return this.dataSource.transaction(async (manager) => {
const user = await manager.findOne(UserEntity, { where: { id: userId } });
if (!user) throw new ApiError(ERROR_CODES.NOT_FOUND, 'User not found', 404);
const ok = await argon2.verify(user.passwordHash, dto.oldPassword);
if (!ok) {
throw new ApiError(ERROR_CODES.INVALID_OLD_PASSWORD, 'Old password is incorrect', 401);
}
try {
validatePassword(dto.newPassword, this.config);
} catch (e) {
if (e instanceof ApiError) {
throw new ApiError(ERROR_CODES.PASSWORD_POLICY, e.message, 400);
}
throw e;
}
user.passwordHash = await argon2.hash(dto.newPassword, {
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'),
});
await manager.save(user);
// Invalidate all existing refresh tokens for this user.
await manager
.createQueryBuilder()
.update(RefreshTokenEntity)
.set({ revokedAt: new Date().toISOString() })
.where('userId = :userId', { userId: user.id })
.andWhere('revokedAt IS NULL')
.execute();
});
}
/**
* Mint a fresh access JWT and a new refresh token row inside the supplied
* transaction. The refresh token (raw) is returned alongside the access