refactor: move auth persistence into repository (WealthLedger/backend#171)
This commit is contained in:
@@ -2,6 +2,8 @@ import { ConflictException, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
import { AuthRepository } from '../infrastructure/auth.repository';
|
||||
|
||||
jest.mock('bcrypt', () => ({
|
||||
hash: jest.fn(() => Promise.resolve('password-hash')),
|
||||
compare: jest.fn(() => Promise.resolve(true)),
|
||||
@@ -51,7 +53,7 @@ describe('AuthService', () => {
|
||||
delete: jest.fn(),
|
||||
},
|
||||
};
|
||||
const service = new AuthService(prisma as never, jwt);
|
||||
const service = new AuthService(new AuthRepository(prisma as never), jwt);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
@@ -9,13 +9,10 @@ import { createHash, randomUUID } from 'crypto';
|
||||
|
||||
import { Session, User } from '../../../../generated/prisma/client';
|
||||
import { HttpErrorCode } from '../../../common/error-code';
|
||||
import { isPrismaErrorCode, PrismaErrorCode } from '../../../prisma/prisma-error-code';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { AuthRepository } from '../infrastructure/auth.repository';
|
||||
import { AuthErrorCode } from './auth-error-code';
|
||||
import { AuthenticationResult, AuthUser, RefreshPayload } from './auth.types';
|
||||
|
||||
const EXPECTED_ROTATED_SESSIONS = 1;
|
||||
|
||||
const PASSWORD_HASH_ROUNDS = 12;
|
||||
export const REFRESH_TTL_MS = 604_800_000;
|
||||
|
||||
@@ -28,7 +25,7 @@ const DUMMY_PASSWORD_HASH =
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly repository: AuthRepository,
|
||||
private readonly jwt: JwtService,
|
||||
) {}
|
||||
|
||||
@@ -36,27 +33,18 @@ export class AuthService {
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<AuthenticationResult> {
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
const existing = await this.repository.findUserByEmail(email);
|
||||
if (existing) {
|
||||
throw new ConflictException({ code: AuthErrorCode.EmailAlreadyRegistered });
|
||||
}
|
||||
|
||||
let user: User;
|
||||
try {
|
||||
user = await this.prisma.user.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
email,
|
||||
passwordHash: await bcrypt.hash(password, PASSWORD_HASH_ROUNDS),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUniqueConstraintError(error)) {
|
||||
throw new ConflictException({ code: AuthErrorCode.EmailAlreadyRegistered });
|
||||
}
|
||||
throw error;
|
||||
const user = await this.repository.createUser({
|
||||
id: randomUUID(),
|
||||
email,
|
||||
passwordHash: await bcrypt.hash(password, PASSWORD_HASH_ROUNDS),
|
||||
});
|
||||
if (!user) {
|
||||
throw new ConflictException({ code: AuthErrorCode.EmailAlreadyRegistered });
|
||||
}
|
||||
return this.createSession(user);
|
||||
}
|
||||
@@ -65,9 +53,7 @@ export class AuthService {
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<AuthenticationResult> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
const user = await this.repository.findUserByEmail(email);
|
||||
const valid = await bcrypt.compare(
|
||||
password,
|
||||
user?.passwordHash ?? DUMMY_PASSWORD_HASH,
|
||||
@@ -92,10 +78,7 @@ export class AuthService {
|
||||
if (payload.type !== 'refresh') {
|
||||
throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
||||
}
|
||||
const session = await this.prisma.session.findUnique({
|
||||
where: { id: payload.sid },
|
||||
include: { user: true },
|
||||
});
|
||||
const session = await this.repository.findSessionWithUser(payload.sid);
|
||||
if (
|
||||
!session ||
|
||||
session.userId !== payload.sub ||
|
||||
@@ -103,7 +86,7 @@ export class AuthService {
|
||||
session.expiresAt <= new Date()
|
||||
) {
|
||||
if (session && !session.revokedAt) {
|
||||
await this.prisma.session.delete({ where: { id: session.id } });
|
||||
await this.repository.deleteSession(session.id);
|
||||
}
|
||||
throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
||||
}
|
||||
@@ -124,18 +107,13 @@ export class AuthService {
|
||||
return;
|
||||
}
|
||||
if (payload.type !== 'refresh') return;
|
||||
const session = await this.prisma.session.findUnique({
|
||||
where: { id: payload.sid },
|
||||
});
|
||||
const session = await this.repository.findSession(payload.sid);
|
||||
if (
|
||||
session &&
|
||||
!session.revokedAt &&
|
||||
session.refreshTokenHash === this.hash(token)
|
||||
) {
|
||||
await this.prisma.session.update({
|
||||
where: { id: session.id },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
await this.repository.revokeSession(session.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +122,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async userById(id: string): Promise<User> {
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
const user = await this.repository.findUserById(id);
|
||||
if (!user) throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
||||
return user;
|
||||
}
|
||||
@@ -152,14 +130,7 @@ export class AuthService {
|
||||
private async createSession(
|
||||
user: User,
|
||||
): Promise<AuthenticationResult> {
|
||||
const session = await this.prisma.session.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
userId: user.id,
|
||||
refreshTokenHash: '',
|
||||
expiresAt: new Date(),
|
||||
},
|
||||
});
|
||||
const session = await this.repository.createSession(user.id, randomUUID());
|
||||
return this.rotateSession({ ...session, user });
|
||||
}
|
||||
|
||||
@@ -180,16 +151,8 @@ export class AuthService {
|
||||
refreshTokenHash: this.hash(refreshToken),
|
||||
expiresAt: new Date(Date.now() + REFRESH_TTL_MS),
|
||||
};
|
||||
if (expectedRefreshTokenHash) {
|
||||
const updated = await this.prisma.session.updateMany({
|
||||
where: { id: session.id, refreshTokenHash: expectedRefreshTokenHash },
|
||||
data,
|
||||
});
|
||||
if (updated.count !== EXPECTED_ROTATED_SESSIONS) {
|
||||
throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
||||
}
|
||||
} else {
|
||||
await this.prisma.session.update({ where: { id: session.id }, data });
|
||||
if (!(await this.repository.rotateSession(session.id, data, expectedRefreshTokenHash))) {
|
||||
throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
||||
}
|
||||
return {
|
||||
user: this.toUser(session.user),
|
||||
@@ -205,12 +168,3 @@ export class AuthService {
|
||||
return createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
function isUniqueConstraintError(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
isPrismaErrorCode(error.code, PrismaErrorCode.UniqueConstraint)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user