chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PHASE_A_ENTITIES } from '../database/entities';
|
||||
import { AuditEventsRepository } from './repositories/audit-events.repository';
|
||||
import { AuthSessionsRepository } from './repositories/auth-sessions.repository';
|
||||
import { RolesRepository } from './repositories/roles.repository';
|
||||
import { UsersRepository } from './repositories/users.repository';
|
||||
|
||||
const repositories = [
|
||||
UsersRepository,
|
||||
RolesRepository,
|
||||
AuthSessionsRepository,
|
||||
AuditEventsRepository,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature(PHASE_A_ENTITIES)],
|
||||
providers: repositories,
|
||||
exports: repositories,
|
||||
})
|
||||
export class PhaseADataModule {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DeepPartial, Repository } from 'typeorm';
|
||||
import { AuditEvent } from '../../database/entities';
|
||||
|
||||
@Injectable()
|
||||
export class AuditEventsRepository {
|
||||
constructor(
|
||||
@InjectRepository(AuditEvent)
|
||||
private readonly repository: Repository<AuditEvent>,
|
||||
) {}
|
||||
|
||||
create(input: DeepPartial<AuditEvent>): AuditEvent {
|
||||
return this.repository.create(input);
|
||||
}
|
||||
|
||||
save(event: AuditEvent): Promise<AuditEvent> {
|
||||
return this.repository.save(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
DeepPartial,
|
||||
EntityManager,
|
||||
IsNull,
|
||||
MoreThan,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { AuthSession } from '../../database/entities';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSessionsRepository {
|
||||
constructor(
|
||||
@InjectRepository(AuthSession)
|
||||
private readonly repository: Repository<AuthSession>,
|
||||
) {}
|
||||
|
||||
create(input: DeepPartial<AuthSession>): AuthSession {
|
||||
return this.repository.create(input);
|
||||
}
|
||||
|
||||
save(session: AuthSession): Promise<AuthSession> {
|
||||
return this.repository.save(session);
|
||||
}
|
||||
|
||||
findActiveById(id: string, now = new Date()): Promise<AuthSession | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
revokedAt: IsNull(),
|
||||
expiresAt: MoreThan(now),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findByIdWithRefreshTokenHash(id: string): Promise<AuthSession | null> {
|
||||
return this.repository
|
||||
.createQueryBuilder('session')
|
||||
.addSelect('session.refreshTokenHash')
|
||||
.where('session.id = :id', { id })
|
||||
.getOne();
|
||||
}
|
||||
|
||||
async revokeUserSessions(
|
||||
userId: string,
|
||||
exceptSessionId?: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const parameters: unknown[] = [userId];
|
||||
let exception = '';
|
||||
if (exceptSessionId) {
|
||||
parameters.push(exceptSessionId);
|
||||
exception = 'AND id <> $2';
|
||||
}
|
||||
await (manager ?? this.repository.manager).query(
|
||||
`
|
||||
UPDATE auth_sessions
|
||||
SET revoked_at = COALESCE(revoked_at, CURRENT_TIMESTAMP)
|
||||
WHERE user_id = $1
|
||||
AND revoked_at IS NULL
|
||||
${exception}
|
||||
`,
|
||||
parameters,
|
||||
);
|
||||
}
|
||||
|
||||
async revokeSessionFamily(
|
||||
sessionId: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
await (manager ?? this.repository.manager).query(
|
||||
`
|
||||
WITH RECURSIVE family AS (
|
||||
SELECT id, replaced_by_session_id
|
||||
FROM auth_sessions
|
||||
WHERE id = $1
|
||||
UNION
|
||||
SELECT session.id, session.replaced_by_session_id
|
||||
FROM auth_sessions session
|
||||
INNER JOIN family member
|
||||
ON session.id = member.replaced_by_session_id
|
||||
OR session.replaced_by_session_id = member.id
|
||||
)
|
||||
UPDATE auth_sessions
|
||||
SET revoked_at = COALESCE(revoked_at, CURRENT_TIMESTAMP)
|
||||
WHERE id IN (SELECT id FROM family)
|
||||
`,
|
||||
[sessionId],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async revokeSession(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
await (manager ?? this.repository.manager).query(
|
||||
`
|
||||
UPDATE auth_sessions
|
||||
SET
|
||||
revoked_at = COALESCE(revoked_at, CURRENT_TIMESTAMP),
|
||||
last_used_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`,
|
||||
[sessionId, userId],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Role } from '../../database/entities';
|
||||
|
||||
interface PermissionCodeRow {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface RoleCodeRow {
|
||||
code: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RolesRepository {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly repository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
findByCodes(codes: string[]): Promise<Role[]> {
|
||||
if (codes.length === 0) return Promise.resolve([]);
|
||||
return this.repository.find({ where: { code: In(codes) } });
|
||||
}
|
||||
|
||||
async findRoleCodesForUser(userId: string): Promise<string[]> {
|
||||
const rows = (await this.repository.query(
|
||||
`
|
||||
SELECT role.code
|
||||
FROM roles role
|
||||
INNER JOIN user_roles user_role ON user_role.role_id = role.id
|
||||
WHERE user_role.user_id = $1
|
||||
ORDER BY role.code ASC
|
||||
`,
|
||||
[userId],
|
||||
)) as RoleCodeRow[];
|
||||
return rows.map((row) => row.code);
|
||||
}
|
||||
|
||||
async findPermissionCodesForUser(userId: string): Promise<string[]> {
|
||||
const rows = (await this.repository.query(
|
||||
`
|
||||
SELECT DISTINCT permission.code
|
||||
FROM permissions permission
|
||||
INNER JOIN role_permissions role_permission
|
||||
ON role_permission.permission_id = permission.id
|
||||
INNER JOIN user_roles user_role
|
||||
ON user_role.role_id = role_permission.role_id
|
||||
WHERE user_role.user_id = $1
|
||||
ORDER BY permission.code ASC
|
||||
`,
|
||||
[userId],
|
||||
)) as PermissionCodeRow[];
|
||||
|
||||
return rows.map((row) => row.code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DeepPartial, Repository } from 'typeorm';
|
||||
import { User } from '../../database/entities';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly repository: Repository<User>,
|
||||
) {}
|
||||
|
||||
create(input: DeepPartial<User>): User {
|
||||
return this.repository.create(input);
|
||||
}
|
||||
|
||||
save(user: User): Promise<User> {
|
||||
return this.repository.save(user);
|
||||
}
|
||||
|
||||
findById(id: string): Promise<User | null> {
|
||||
return this.repository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
findForAuthentication(identifier: string): Promise<User | null> {
|
||||
const normalizedIdentifier = identifier.trim().toLowerCase();
|
||||
|
||||
return this.repository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('LOWER(user.username) = :identifier', {
|
||||
identifier: normalizedIdentifier,
|
||||
})
|
||||
.orWhere('LOWER(user.email) = :identifier', {
|
||||
identifier: normalizedIdentifier,
|
||||
})
|
||||
.getOne();
|
||||
}
|
||||
|
||||
findByIdForPasswordVerification(id: string): Promise<User | null> {
|
||||
return this.repository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.id = :id', { id })
|
||||
.getOne();
|
||||
}
|
||||
|
||||
async registerFailedLogin(
|
||||
userId: string,
|
||||
maxAttempts: number,
|
||||
lockoutSeconds: number,
|
||||
): Promise<void> {
|
||||
await this.repository.query(
|
||||
`
|
||||
UPDATE users
|
||||
SET
|
||||
failed_login_attempts = failed_login_attempts + 1,
|
||||
locked_until = CASE
|
||||
WHEN failed_login_attempts + 1 >= $2
|
||||
THEN CURRENT_TIMESTAMP + ($3 * INTERVAL '1 second')
|
||||
ELSE locked_until
|
||||
END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
`,
|
||||
[userId, maxAttempts, lockoutSeconds],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user