Skip to content

Commit 5a082ea

Browse files
Merge pull request #220 from sublime247/refactor/eliminate-duplicate-user-code
refactor: eliminate duplicate user lookup patterns
2 parents 41e8ec1 + 59ef3e1 commit 5a082ea

4 files changed

Lines changed: 137 additions & 59 deletions

File tree

src/auth/auth.service.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { randomBytes } from 'crypto';
88
import { SessionService } from '../session/session.service';
99
import { TransactionService } from '../common/database/transaction.service';
1010
import { UserRole } from '../users/entities/user.entity';
11+
import {
12+
ensureValidCredentials,
13+
ensureUserIsActive,
14+
ensureValidUserToken,
15+
} from '../common/utils/user.utils';
1116

1217
interface JwtTokenPayload {
1318
sub: string;
@@ -102,10 +107,8 @@ export class AuthService {
102107

103108
async login(loginDto: LoginDto): Promise<LoginResponse> {
104109
// Find user
105-
const user = await this.usersService.findByEmail(loginDto.email);
106-
if (!user) {
107-
throw new UnauthorizedException('Invalid credentials');
108-
}
110+
const userOrNull = await this.usersService.findByEmail(loginDto.email);
111+
const user = ensureValidCredentials(userOrNull);
109112

110113
// Verify password
111114
const isPasswordValid = await bcrypt.compare(loginDto.password, user.password);
@@ -114,9 +117,7 @@ export class AuthService {
114117
}
115118

116119
// Check if user is active
117-
if (user.status !== 'active') {
118-
throw new UnauthorizedException('Account is not active');
119-
}
120+
ensureUserIsActive(user);
120121

121122
// Update last login
122123
await this.usersService.updateLastLogin(user.id);
@@ -221,16 +222,13 @@ export class AuthService {
221222

222223
async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<{ message: string }> {
223224
// Find user by reset token
224-
const user = await this.usersService.findByPasswordResetToken(resetPasswordDto.token);
225-
226-
if (!user || !user.passwordResetToken || !user.passwordResetExpires) {
227-
throw new BadRequestException('Invalid or expired reset token');
228-
}
229-
230-
// Check if token is expired
231-
if (new Date() > user.passwordResetExpires) {
232-
throw new BadRequestException('Invalid or expired reset token');
233-
}
225+
const userOrNull = await this.usersService.findByPasswordResetToken(resetPasswordDto.token);
226+
const user = ensureValidUserToken(
227+
userOrNull,
228+
'passwordResetToken',
229+
'passwordResetExpires',
230+
'Invalid or expired reset token',
231+
);
234232

235233
// Update password
236234
await this.usersService.update(user.id, { password: resetPasswordDto.newPassword });
@@ -261,16 +259,13 @@ export class AuthService {
261259

262260
async verifyEmail(token: string): Promise<{ message: string }> {
263261
// Find user by verification token
264-
const user = await this.usersService.findByEmailVerificationToken(token);
265-
266-
if (!user || !user.emailVerificationToken || !user.emailVerificationExpires) {
267-
throw new BadRequestException('Invalid or expired verification token');
268-
}
269-
270-
// Check if token is expired
271-
if (new Date() > user.emailVerificationExpires) {
272-
throw new BadRequestException('Invalid or expired verification token');
273-
}
262+
const userOrNull = await this.usersService.findByEmailVerificationToken(token);
263+
const user = ensureValidUserToken(
264+
userOrNull,
265+
'emailVerificationToken',
266+
'emailVerificationExpires',
267+
'Invalid or expired verification token',
268+
);
274269

275270
// Update user as verified
276271
await this.usersService.update(user.id, { isEmailVerified: true });

src/common/utils/user.utils.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
NotFoundException,
3+
UnauthorizedException,
4+
BadRequestException,
5+
ConflictException,
6+
} from '@nestjs/common';
7+
import { User } from '../../users/entities/user.entity';
8+
9+
/**
10+
* Ensures a user exists, throwing a NotFoundException otherwise.
11+
* @param user The user object to check
12+
* @param message Optional custom error message
13+
* @returns The guaranteed non-null user object
14+
*/
15+
export function ensureUserExists(user: User | null | undefined, message = 'User not found'): User {
16+
if (!user) {
17+
throw new NotFoundException(message);
18+
}
19+
return user;
20+
}
21+
22+
/**
23+
* Ensures a user exists for authentication purposes, throwing an UnauthorizedException otherwise.
24+
* @param user The user object to check
25+
* @param message Optional custom error message
26+
* @returns The guaranteed non-null user object
27+
*/
28+
export function ensureValidCredentials(
29+
user: User | null | undefined,
30+
message = 'Invalid credentials',
31+
): User {
32+
if (!user) {
33+
throw new UnauthorizedException(message);
34+
}
35+
return user;
36+
}
37+
38+
/**
39+
* Ensures a user's account is active.
40+
* @param user The user object to check
41+
* @param message Optional custom error message
42+
*/
43+
export function ensureUserIsActive(user: User, message = 'Account is not active'): void {
44+
if (user.status !== 'active') {
45+
throw new UnauthorizedException(message);
46+
}
47+
}
48+
49+
/**
50+
* Ensures a user has a valid and unexpired token for a specific field.
51+
* @param user The user object to check
52+
* @param tokenField The property name of the token on the user object
53+
* @param expiresField The property name of the expiration date on the user object
54+
* @param message Optional custom error message
55+
* @returns The guaranteed non-null user object
56+
*/
57+
export function ensureValidUserToken(
58+
user: User | null | undefined,
59+
tokenField: keyof User,
60+
expiresField: keyof User,
61+
message = 'Invalid or expired token',
62+
): User {
63+
if (!user || !user[tokenField] || !user[expiresField]) {
64+
throw new BadRequestException(message);
65+
}
66+
67+
const expireDate = user[expiresField] as Date;
68+
if (new Date() > expireDate) {
69+
throw new BadRequestException(message);
70+
}
71+
72+
return user;
73+
}
74+
75+
/**
76+
* Ensures a user does not exist, throwing a ConflictException otherwise.
77+
* Useful for registration or email updates.
78+
* @param user The user object to check
79+
* @param message Optional custom error message
80+
*/
81+
export function ensureUserDoesNotExist(
82+
user: User | null | undefined,
83+
message = 'User already exists',
84+
): void {
85+
if (user) {
86+
throw new ConflictException(message);
87+
}
88+
}

src/payments/payments.service.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Invoice, InvoiceStatus } from './entities/invoice.entity';
1010
import { RefundDto } from './dto/refund.dto';
1111
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
1212
import { TransactionService } from '../common/database/transaction.service';
13+
import { ensureUserExists } from '../common/utils/user.utils';
1314
import {
1415
PaymentProvider,
1516
PaymentMetadata,
@@ -71,12 +72,10 @@ export class PaymentsService {
7172
const { courseId, amount, currency, provider, metadata } = createPaymentDto;
7273

7374
// Verify user exists
74-
const user = await this.userRepository.findOne({
75+
const userOrNull = await this.userRepository.findOne({
7576
where: { id: userId },
7677
});
77-
if (!user) {
78-
throw new NotFoundException('User not found');
79-
}
78+
const user = ensureUserExists(userOrNull);
8079

8180
// Get payment provider
8281
const paymentProvider = this.getProvider(provider ?? 'stripe');
@@ -119,13 +118,11 @@ export class PaymentsService {
119118
const { interval } = createSubscriptionDto;
120119

121120
// Verify user exists
122-
const user = await this.userRepository.findOne({
121+
const userOrNull = await this.userRepository.findOne({
123122
where: { id: userId },
124123
});
125124

126-
if (!user) {
127-
throw new NotFoundException('User not found');
128-
}
125+
ensureUserExists(userOrNull);
129126

130127
// Get payment provider
131128
// const paymentProvider = this.getProvider(provider);

src/users/users.service.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
1+
import { Injectable } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
44
import { User } from './entities/user.entity';
55
import { CreateUserDto } from './dto/create-user.dto';
66
import { UpdateUserDto } from './dto/update-user.dto';
77
import * as bcrypt from 'bcryptjs';
8+
import { ensureUserExists, ensureUserDoesNotExist } from '../common/utils/user.utils';
89
import { paginate, PaginatedResponse } from '../common/utils/pagination.util';
10+
import { PaginationQueryDto } from '../common/dto/pagination.dto';
911
import { GetUsersDto } from './dto/get-users.dto';
1012
import { CachingService } from '../caching/caching.service';
1113
import { CACHE_TTL, CACHE_PREFIXES, CACHE_EVENTS } from '../caching/caching.constants';
@@ -25,10 +27,7 @@ export class UsersService {
2527
const existingUser = await this.userRepository.findOne({
2628
where: { email: createUserDto.email },
2729
});
28-
29-
if (existingUser) {
30-
throw new ConflictException('User with this email already exists');
31-
}
30+
ensureUserDoesNotExist(existingUser, 'User with this email already exists');
3231

3332
// Hash password
3433
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
@@ -65,7 +64,7 @@ export class UsersService {
6564
);
6665
}
6766

68-
return await paginate(query, filter);
67+
return await paginate(query, filter || new PaginationQueryDto());
6968
},
7069
CACHE_TTL.USER_PROFILE,
7170
);
@@ -76,17 +75,22 @@ export class UsersService {
7675
return await this.userRepository.findByIds(ids);
7776
}
7877

78+
/**
79+
* Helper method to find a user by ID or throw NotFoundException.
80+
* Can be used internally to eliminate duplication.
81+
*/
82+
async findUserOrThrow(id: string): Promise<User> {
83+
const user = await this.userRepository.findOne({ where: { id } });
84+
return ensureUserExists(user, 'User not found');
85+
}
86+
7987
async findOne(id: string): Promise<User> {
8088
const cacheKey = `${CACHE_PREFIXES.USER_PROFILE}:${id}`;
8189

8290
return this.cachingService.getOrSet(
8391
cacheKey,
8492
async () => {
85-
const user = await this.userRepository.findOne({ where: { id } });
86-
if (!user) {
87-
throw new NotFoundException('User not found');
88-
}
89-
return user;
93+
return await this.findUserOrThrow(id);
9094
},
9195
CACHE_TTL.USER_PROFILE,
9296
);
@@ -109,10 +113,7 @@ export class UsersService {
109113
}
110114

111115
async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
112-
const user = await this.userRepository.findOne({ where: { id } });
113-
if (!user) {
114-
throw new NotFoundException('User not found');
115-
}
116+
const user = await this.findUserOrThrow(id);
116117

117118
// If updating password, hash it
118119
if (updateUserDto.password) {
@@ -129,7 +130,7 @@ export class UsersService {
129130
}
130131

131132
async updateRefreshToken(userId: string, refreshToken: string | null): Promise<void> {
132-
await this.userRepository.update(userId, { refreshToken });
133+
await this.userRepository.update(userId, { refreshToken: refreshToken as unknown as string });
133134
// Invalidate user cache
134135
this.eventEmitter.emit(CACHE_EVENTS.USER_UPDATED, { userId });
135136
}
@@ -140,8 +141,8 @@ export class UsersService {
140141
expires: Date | null,
141142
): Promise<void> {
142143
await this.userRepository.update(userId, {
143-
passwordResetToken: token,
144-
passwordResetExpires: expires,
144+
passwordResetToken: token as unknown as string,
145+
passwordResetExpires: expires as unknown as Date,
145146
});
146147
}
147148

@@ -151,8 +152,8 @@ export class UsersService {
151152
expires: Date | null,
152153
): Promise<void> {
153154
await this.userRepository.update(userId, {
154-
emailVerificationToken: token,
155-
emailVerificationExpires: expires,
155+
emailVerificationToken: token as unknown as string,
156+
emailVerificationExpires: expires as unknown as Date,
156157
});
157158
}
158159

@@ -161,10 +162,7 @@ export class UsersService {
161162
}
162163

163164
async remove(id: string): Promise<void> {
164-
const user = await this.userRepository.findOne({ where: { id } });
165-
if (!user) {
166-
throw new NotFoundException('User not found');
167-
}
165+
const user = await this.findUserOrThrow(id);
168166
await this.userRepository.remove(user);
169167

170168
// Invalidate cache after delete

0 commit comments

Comments
 (0)