Skip to content

Commit 681b9a8

Browse files
committed
refactor to factory pattern
1 parent b7fc7ec commit 681b9a8

File tree

11 files changed

+260
-221
lines changed

11 files changed

+260
-221
lines changed

src/app.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { HealthController } from './health/health.controller';
2222
CacheModule.register(),
2323
ScheduleModule.forRoot(),
2424
AuthModule,
25-
UsersModule.register(),
25+
UsersModule,
2626
BuildsModule,
2727
ProjectsModule,
2828
TestRunsModule,

src/builds/builds.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AuthModule } from '../auth/auth.module';
99
import { ProjectsModule } from '../projects/projects.module';
1010

1111
@Module({
12-
imports: [SharedModule, UsersModule.register(), forwardRef(() => TestRunsModule), AuthModule, forwardRef(() => ProjectsModule)],
12+
imports: [SharedModule, UsersModule, forwardRef(() => TestRunsModule), AuthModule, forwardRef(() => ProjectsModule)],
1313
providers: [BuildsService, PrismaService],
1414
controllers: [BuildsController],
1515
exports: [BuildsService],

src/users/dbusers.service.ts renamed to src/users/db/dbusers.service.ts

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
2-
import { CreateUserDto } from './dto/user-create.dto';
3-
import { UserLoginResponseDto } from './dto/user-login-response.dto';
4-
import { PrismaService } from '../prisma/prisma.service';
1+
import { HttpException, HttpStatus } from '@nestjs/common';
2+
import { CreateUserDto } from '../dto/user-create.dto';
3+
import { UserLoginResponseDto } from '../dto/user-login-response.dto';
4+
import { PrismaService } from '../../prisma/prisma.service';
55
import { User } from '@prisma/client';
6-
import { UserDto } from './dto/user.dto';
7-
import { UpdateUserDto } from './dto/user-update.dto';
8-
import { AuthService } from '../auth/auth.service';
9-
import { UserLoginRequestDto } from './dto/user-login-request.dto';
10-
import { AssignRoleDto } from './dto/assign-role.dto';
6+
import { UpdateUserDto } from '../dto/user-update.dto';
7+
import { AuthService } from '../../auth/auth.service';
8+
import { UserLoginRequestDto } from '../dto/user-login-request.dto';
119
import { Logger } from '@nestjs/common';
12-
import { UsersService } from './users.service';
10+
import { Users } from '../users.interface';
1311

14-
@Injectable()
15-
export class DbUsersService implements UsersService {
12+
export class DbUsersService implements Users {
1613
private readonly logger: Logger = new Logger(DbUsersService.name);
1714

1815
constructor(
@@ -36,31 +33,6 @@ export class DbUsersService implements UsersService {
3633
return new UserLoginResponseDto(userData, null);
3734
}
3835

39-
async findOne(id: string): Promise<User> {
40-
return this.prismaService.user.findUnique({ where: { id } });
41-
}
42-
43-
async delete(id: string): Promise<User> {
44-
this.logger.debug(`Removing User: ${id}`);
45-
return this.prismaService.user.delete({ where: { id } });
46-
}
47-
48-
async get(id: string): Promise<UserDto> {
49-
const user = await this.findOne(id);
50-
return new UserDto(user);
51-
}
52-
53-
async assignRole(data: AssignRoleDto): Promise<UserDto> {
54-
const { id, role } = data;
55-
this.logger.debug(`Assigning role ${role} to User: ${id}`);
56-
57-
const user = await this.prismaService.user.update({
58-
where: { id },
59-
data: { role },
60-
});
61-
return new UserDto(user);
62-
}
63-
6436
async update(id: string, userDto: UpdateUserDto): Promise<UserLoginResponseDto> {
6537
const user = await this.prismaService.user.update({
6638
where: { id },
@@ -74,17 +46,6 @@ export class DbUsersService implements UsersService {
7446
return new UserLoginResponseDto(user, token);
7547
}
7648

77-
async generateNewApiKey(user: User): Promise<string> {
78-
const newApiKey = this.authService.generateApiKey();
79-
await this.prismaService.user.update({
80-
where: { id: user.id },
81-
data: {
82-
apiKey: newApiKey,
83-
},
84-
});
85-
return newApiKey;
86-
}
87-
8849
async changePassword(user: User, newPassword: string): Promise<boolean> {
8950
await this.prismaService.user.update({
9051
where: { id: user.id },

src/users/ldapusers.service.ts renamed to src/users/ldap/ldapusers.service.ts

Lines changed: 49 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,52 @@
1-
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
2-
import { CreateUserDto } from './dto/user-create.dto';
3-
import { UserLoginResponseDto } from './dto/user-login-response.dto';
4-
import { PrismaService } from '../prisma/prisma.service';
1+
import { HttpException, HttpStatus } from '@nestjs/common';
2+
import { CreateUserDto } from '../dto/user-create.dto';
3+
import { UserLoginResponseDto } from '../dto/user-login-response.dto';
4+
import { PrismaService } from '../../prisma/prisma.service';
55
import { Role, User } from '@prisma/client';
6-
import { UserDto } from './dto/user.dto';
7-
import { UpdateUserDto } from './dto/user-update.dto';
8-
import { AuthService } from '../auth/auth.service';
9-
import { UserLoginRequestDto } from './dto/user-login-request.dto';
10-
import { AssignRoleDto } from './dto/assign-role.dto';
6+
import { UpdateUserDto } from '../dto/user-update.dto';
7+
import { AuthService } from '../../auth/auth.service';
8+
import { UserLoginRequestDto } from '../dto/user-login-request.dto';
119
import { Logger } from '@nestjs/common';
1210
import { Entry as LdapEntry, Client as LdapClient } from 'ldapts';
13-
import { UsersService } from './users.service';
14-
15-
@Injectable()
16-
export class LdapUsersService implements UsersService {
11+
import { Users } from '../users.interface';
12+
import { ConfigService } from '@nestjs/config';
13+
14+
type LDAPConfig = {
15+
url: string;
16+
bindUser: string;
17+
bindPassword: string;
18+
searchDN: string;
19+
usersSearchFilter: string;
20+
attributeMail: string;
21+
attributeFirstName: string;
22+
attributeLastName: string;
23+
tlsNoVerify: boolean;
24+
};
25+
export class LdapUsersService implements Users {
1726
private readonly ldapClient: LdapClient;
1827
private readonly logger: Logger = new Logger(LdapUsersService.name);
19-
private readonly assertRequiredEnvVarsAreSet = () => {
20-
const requiredEnvVars = [
21-
'LDAP_URL',
22-
'LDAP_BIND_USER',
23-
'LDAP_BIND_PASSWORD',
24-
'LDAP_SEARCH_DN',
25-
'LDAP_USERS_SEARCH_FILTER',
26-
'LDAP_ATTRIBUTE_MAIL',
27-
'LDAP_ATTRIBUTE_FIRST_NAME',
28-
'LDAP_ATTRIBUTE_LAST_NAME',
29-
];
30-
31-
for (const envVar of requiredEnvVars) {
32-
if (!process.env[envVar]) {
33-
throw new Error(`${envVar} is required.`);
34-
}
35-
}
36-
};
28+
private readonly ldapConfig: LDAPConfig;
3729

3830
constructor(
31+
private configService: ConfigService,
3932
private prismaService: PrismaService,
4033
private authService: AuthService
4134
) {
42-
this.assertRequiredEnvVarsAreSet();
35+
this.ldapConfig = {
36+
url: this.configService.getOrThrow<string>('LDAP_URL'),
37+
bindUser: this.configService.getOrThrow<string>('LDAP_BIND_USER'),
38+
bindPassword: this.configService.getOrThrow<string>('LDAP_BIND_PASSWORD'),
39+
searchDN: this.configService.getOrThrow<string>('LDAP_SEARCH_DN'),
40+
usersSearchFilter: this.configService.getOrThrow<string>('LDAP_USERS_SEARCH_FILTER'),
41+
attributeMail: this.configService.getOrThrow<string>('LDAP_ATTRIBUTE_MAIL'),
42+
attributeFirstName: this.configService.getOrThrow<string>('LDAP_ATTRIBUTE_FIRST_NAME'),
43+
attributeLastName: this.configService.getOrThrow<string>('LDAP_ATTRIBUTE_LAST_NAME'),
44+
tlsNoVerify: this.configService.get<boolean>('LDAP_TLS_NO_VERIFY', false),
45+
};
4346
this.ldapClient = new LdapClient({
44-
url: process.env.LDAP_URL,
47+
url: this.ldapConfig.url,
4548
tlsOptions: {
46-
rejectUnauthorized: process.env.LDAP_TLS_NO_VERIFY !== 'true',
49+
rejectUnauthorized: !this.ldapConfig.tlsNoVerify,
4750
},
4851
});
4952
}
@@ -53,16 +56,16 @@ export class LdapUsersService implements UsersService {
5356
email = this.escapeLdap(email.trim());
5457
this.logger.verbose(`search '${email}' in LDAP`);
5558
try {
56-
await this.ldapClient.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PASSWORD);
59+
await this.ldapClient.bind(this.ldapConfig.bindUser, this.ldapConfig.bindPassword);
5760
const attributes = [
5861
'dn',
59-
process.env.LDAP_ATTRIBUTE_MAIL,
60-
process.env.LDAP_ATTRIBUTE_FIRST_NAME,
61-
process.env.LDAP_ATTRIBUTE_LAST_NAME,
62+
this.ldapConfig.attributeMail,
63+
this.ldapConfig.attributeFirstName,
64+
this.ldapConfig.attributeLastName,
6265
];
6366

64-
const { searchEntries } = await this.ldapClient.search(process.env.LDAP_SEARCH_DN, {
65-
filter: process.env.LDAP_USERS_SEARCH_FILTER.replaceAll('{{email}}', email),
67+
const { searchEntries } = await this.ldapClient.search(this.ldapConfig.searchDN, {
68+
filter: this.ldapConfig.usersSearchFilter.replaceAll('{{email}}', email),
6669
sizeLimit: 1,
6770
attributes: attributes,
6871
});
@@ -84,9 +87,9 @@ export class LdapUsersService implements UsersService {
8487

8588
private async createUserFromLdapEntry(ldapEntry: LdapEntry): Promise<User> {
8689
const userForVRTDb = {
87-
email: ldapEntry[process.env.LDAP_ATTRIBUTE_MAIL].toString().trim().toLowerCase(),
88-
firstName: ldapEntry[process.env.LDAP_ATTRIBUTE_FIRST_NAME].toString(),
89-
lastName: ldapEntry[process.env.LDAP_ATTRIBUTE_LAST_NAME].toString(),
90+
email: ldapEntry[this.ldapConfig.attributeMail].toString().trim().toLowerCase(),
91+
firstName: ldapEntry[this.ldapConfig.attributeFirstName].toString(),
92+
lastName: ldapEntry[this.ldapConfig.attributeLastName].toString(),
9093
apiKey: this.authService.generateApiKey(),
9194
password: await this.authService.encryptPassword(Math.random().toString(36).slice(-8)),
9295
role: Role.editor,
@@ -97,35 +100,11 @@ export class LdapUsersService implements UsersService {
97100
});
98101
}
99102

103+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
100104
async create(createUserDto: CreateUserDto): Promise<UserLoginResponseDto> {
101105
throw new HttpException('User creation is disabled. Use your LDAP-Credentials to login.', HttpStatus.BAD_REQUEST);
102106
}
103107

104-
async findOne(id: string): Promise<User> {
105-
return this.prismaService.user.findUnique({ where: { id } });
106-
}
107-
108-
async delete(id: string): Promise<User> {
109-
this.logger.debug(`Removing User: ${id}`);
110-
return this.prismaService.user.delete({ where: { id } });
111-
}
112-
113-
async get(id: string): Promise<UserDto> {
114-
const user = await this.findOne(id);
115-
return new UserDto(user);
116-
}
117-
118-
async assignRole(data: AssignRoleDto): Promise<UserDto> {
119-
const { id, role } = data;
120-
this.logger.debug(`Assigning role ${role} to User: ${id}`);
121-
122-
const user = await this.prismaService.user.update({
123-
where: { id },
124-
data: { role },
125-
});
126-
return new UserDto(user);
127-
}
128-
129108
async update(id: string, userDto: UpdateUserDto): Promise<UserLoginResponseDto> {
130109
const userFromLdap = await this.findUserInLdap(userDto.email);
131110
const user = await this.prismaService.user.update({
@@ -140,17 +119,7 @@ export class LdapUsersService implements UsersService {
140119
return new UserLoginResponseDto(user, token);
141120
}
142121

143-
async generateNewApiKey(user: User): Promise<string> {
144-
const newApiKey = this.authService.generateApiKey();
145-
await this.prismaService.user.update({
146-
where: { id: user.id },
147-
data: {
148-
apiKey: newApiKey,
149-
},
150-
});
151-
return newApiKey;
152-
}
153-
122+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
154123
async changePassword(user: User, newPassword: string): Promise<boolean> {
155124
this.logger.warn(`${user.email} tied to change password - this is not supported for LDAP users`);
156125
return true;
@@ -171,7 +140,7 @@ export class LdapUsersService implements UsersService {
171140
await this.ldapClient.unbind();
172141
}
173142

174-
const userEmailFromLdap = userFromLdap[process.env.LDAP_ATTRIBUTE_MAIL].toString().trim().toLowerCase();
143+
const userEmailFromLdap = userFromLdap[this.ldapConfig.attributeMail].toString().trim().toLowerCase();
175144

176145
let user = await this.prismaService.user.findUnique({
177146
where: { email: userEmailFromLdap },

src/users/ldapusers.service.spec.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/users/users.factory.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { ConfigService } from '@nestjs/config';
3+
import { UsersFactoryService } from './users.factory';
4+
import { DbUsersService } from './db/dbusers.service';
5+
import { LdapUsersService } from './ldap/ldapusers.service';
6+
import { PrismaService } from '../prisma/prisma.service';
7+
import { AuthService } from '../auth/auth.service';
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9+
import { Client } from 'ldapts';
10+
jest.mock('ldapts');
11+
12+
describe('UsersFactoryService', () => {
13+
let service: UsersFactoryService;
14+
let configService: ConfigService;
15+
16+
beforeEach(async () => {
17+
const module: TestingModule = await Test.createTestingModule({
18+
providers: [
19+
UsersFactoryService,
20+
{
21+
provide: ConfigService,
22+
useValue: {
23+
get: jest.fn(),
24+
getOrThrow: jest.fn(),
25+
},
26+
},
27+
{
28+
provide: PrismaService,
29+
useValue: {
30+
user: {
31+
findMany: jest.fn().mockResolvedValueOnce([]),
32+
},
33+
},
34+
},
35+
{ provide: AuthService, useValue: {} },
36+
],
37+
}).compile();
38+
39+
service = module.get<UsersFactoryService>(UsersFactoryService);
40+
configService = module.get<ConfigService>(ConfigService);
41+
});
42+
43+
it('should be defined', () => {
44+
expect(service).toBeDefined();
45+
});
46+
47+
it('should return DbUsersService when LDAP_ENABLED is false', () => {
48+
jest.spyOn(configService, 'get').mockReturnValue('false');
49+
const result = service.getUsersService();
50+
expect(result).toBeInstanceOf(DbUsersService);
51+
});
52+
53+
it('should return LdapUsersService when LDAP_ENABLED is true', () => {
54+
jest.spyOn(configService, 'get').mockReturnValue('true');
55+
jest.spyOn(configService, 'getOrThrow').mockReturnValue('mockedValue');
56+
const result = service.getUsersService();
57+
expect(result).toBeInstanceOf(LdapUsersService);
58+
});
59+
});

0 commit comments

Comments
 (0)