nest-authme 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/bin/cli.js +11 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1619 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/templates/decorators/current-user.decorator.ts.hbs +8 -0
- package/dist/generator/templates/decorators/public.decorator.ts.hbs +4 -0
- package/dist/generator/templates/decorators/roles.decorator.ts.hbs +4 -0
- package/dist/generator/templates/dto/auth-response.dto.ts.hbs +42 -0
- package/dist/generator/templates/dto/change-password.dto.ts.hbs +22 -0
- package/dist/generator/templates/dto/create-user.dto.ts.hbs +38 -0
- package/dist/generator/templates/dto/forgot-password.dto.ts.hbs +13 -0
- package/dist/generator/templates/dto/login.dto.ts.hbs +21 -0
- package/dist/generator/templates/dto/register.dto.ts.hbs +33 -0
- package/dist/generator/templates/dto/reset-password.dto.ts.hbs +22 -0
- package/dist/generator/templates/entities/refresh-token.entity.typeorm.hbs +24 -0
- package/dist/generator/templates/entities/user.entity.typeorm.hbs +51 -0
- package/dist/generator/templates/jwt/auth.controller.ts.hbs +177 -0
- package/dist/generator/templates/jwt/auth.module.ts.hbs +81 -0
- package/dist/generator/templates/jwt/auth.service.ts.hbs +416 -0
- package/dist/generator/templates/jwt/jwt-auth.guard.ts.hbs +24 -0
- package/dist/generator/templates/jwt/jwt.strategy.ts.hbs +61 -0
- package/dist/generator/templates/jwt/local-auth.guard.ts.hbs +5 -0
- package/dist/generator/templates/jwt/local.strategy.ts.hbs +22 -0
- package/dist/generator/templates/prisma/prisma.module.ts.hbs +9 -0
- package/dist/generator/templates/prisma/prisma.service.ts.hbs +9 -0
- package/dist/generator/templates/prisma/schema.prisma.additions.hbs +40 -0
- package/dist/generator/templates/rbac/role.enum.ts.hbs +5 -0
- package/dist/generator/templates/rbac/roles.guard.ts.hbs +22 -0
- package/dist/generator/templates/shared/README.auth.md.hbs +306 -0
- package/dist/generator/templates/shared/env.hbs +36 -0
- package/dist/generator/templates/shared/env.template.hbs +36 -0
- package/dist/generator/templates/shared/main.ts.snippet.hbs +49 -0
- package/dist/generator/templates/tests/auth.controller.spec.ts.hbs +189 -0
- package/dist/generator/templates/tests/auth.service.spec.ts.hbs +334 -0
- package/dist/generator/templates/users/users.controller.ts.hbs +55 -0
- package/dist/generator/templates/users/users.module.ts.hbs +31 -0
- package/dist/generator/templates/users/users.service.ts.hbs +192 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1566 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { AuthController } from './auth.controller';
|
|
3
|
+
import { AuthService } from './auth.service';
|
|
4
|
+
|
|
5
|
+
describe('AuthController', () => {
|
|
6
|
+
let controller: AuthController;
|
|
7
|
+
let authService: AuthService;
|
|
8
|
+
|
|
9
|
+
const mockAuthService = {
|
|
10
|
+
login: jest.fn(),
|
|
11
|
+
register: jest.fn(),
|
|
12
|
+
changePassword: jest.fn(),
|
|
13
|
+
{{#if features.emailVerification}}
|
|
14
|
+
verifyEmail: jest.fn(),
|
|
15
|
+
resendVerification: jest.fn(),
|
|
16
|
+
{{/if}}
|
|
17
|
+
{{#if features.resetPassword}}
|
|
18
|
+
forgotPassword: jest.fn(),
|
|
19
|
+
resetPassword: jest.fn(),
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if features.refreshTokens}}
|
|
22
|
+
refreshAccessToken: jest.fn(),
|
|
23
|
+
logout: jest.fn(),
|
|
24
|
+
logoutAll: jest.fn(),
|
|
25
|
+
{{/if}}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
30
|
+
controllers: [AuthController],
|
|
31
|
+
providers: [
|
|
32
|
+
{ provide: AuthService, useValue: mockAuthService },
|
|
33
|
+
],
|
|
34
|
+
}).compile();
|
|
35
|
+
|
|
36
|
+
controller = module.get<AuthController>(AuthController);
|
|
37
|
+
authService = module.get<AuthService>(AuthService);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('POST /auth/login', () => {
|
|
45
|
+
it('should call authService.login and return result', async () => {
|
|
46
|
+
const loginDto = { email: 'test@example.com', password: 'password123' };
|
|
47
|
+
const expectedResult = {
|
|
48
|
+
accessToken: 'mock-token',
|
|
49
|
+
user: { id: 'uuid', email: 'test@example.com' },
|
|
50
|
+
};
|
|
51
|
+
mockAuthService.login.mockResolvedValue(expectedResult);
|
|
52
|
+
|
|
53
|
+
const result = await controller.login(loginDto);
|
|
54
|
+
|
|
55
|
+
expect(authService.login).toHaveBeenCalledWith(loginDto);
|
|
56
|
+
expect(result).toEqual(expectedResult);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('POST /auth/register', () => {
|
|
61
|
+
it('should call authService.register and return result', async () => {
|
|
62
|
+
const registerDto = {
|
|
63
|
+
email: 'new@example.com',
|
|
64
|
+
{{#if features.useUsername}}
|
|
65
|
+
username: 'newuser',
|
|
66
|
+
{{/if}}
|
|
67
|
+
password: 'StrongP@ss1',
|
|
68
|
+
};
|
|
69
|
+
const expectedResult = {
|
|
70
|
+
accessToken: 'mock-token',
|
|
71
|
+
user: { id: 'uuid', email: 'new@example.com' },
|
|
72
|
+
};
|
|
73
|
+
mockAuthService.register.mockResolvedValue(expectedResult);
|
|
74
|
+
|
|
75
|
+
const result = await controller.register(registerDto);
|
|
76
|
+
|
|
77
|
+
expect(authService.register).toHaveBeenCalledWith(registerDto);
|
|
78
|
+
expect(result).toEqual(expectedResult);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('POST /auth/change-password', () => {
|
|
83
|
+
it('should call authService.changePassword and return success message', async () => {
|
|
84
|
+
const user = { id: 'test-uuid', email: 'test@example.com' };
|
|
85
|
+
const changePasswordDto = { currentPassword: 'old', newPassword: 'NewP@ssword1' };
|
|
86
|
+
mockAuthService.changePassword.mockResolvedValue(undefined);
|
|
87
|
+
|
|
88
|
+
const result = await controller.changePassword(user, changePasswordDto);
|
|
89
|
+
|
|
90
|
+
expect(authService.changePassword).toHaveBeenCalledWith('test-uuid', changePasswordDto);
|
|
91
|
+
expect(result).toEqual({ message: 'Password changed successfully' });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
{{#if features.emailVerification}}
|
|
96
|
+
describe('GET /auth/verify-email', () => {
|
|
97
|
+
it('should call authService.verifyEmail and return result', async () => {
|
|
98
|
+
mockAuthService.verifyEmail.mockResolvedValue({ message: 'Email verified successfully' });
|
|
99
|
+
|
|
100
|
+
const result = await controller.verifyEmail('valid-token');
|
|
101
|
+
|
|
102
|
+
expect(authService.verifyEmail).toHaveBeenCalledWith('valid-token');
|
|
103
|
+
expect(result).toEqual({ message: 'Email verified successfully' });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('POST /auth/resend-verification', () => {
|
|
108
|
+
it('should call authService.resendVerification and return result', async () => {
|
|
109
|
+
mockAuthService.resendVerification.mockResolvedValue({
|
|
110
|
+
verificationToken: 'new-token',
|
|
111
|
+
message: 'Verification token generated',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await controller.resendVerification('test@example.com');
|
|
115
|
+
|
|
116
|
+
expect(authService.resendVerification).toHaveBeenCalledWith('test@example.com');
|
|
117
|
+
expect(result.verificationToken).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
{{/if}}
|
|
121
|
+
|
|
122
|
+
{{#if features.resetPassword}}
|
|
123
|
+
describe('POST /auth/forgot-password', () => {
|
|
124
|
+
it('should call authService.forgotPassword and return result', async () => {
|
|
125
|
+
mockAuthService.forgotPassword.mockResolvedValue({
|
|
126
|
+
resetToken: 'reset-token',
|
|
127
|
+
message: 'Password reset token generated',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await controller.forgotPassword({ email: 'test@example.com' });
|
|
131
|
+
|
|
132
|
+
expect(authService.forgotPassword).toHaveBeenCalledWith({ email: 'test@example.com' });
|
|
133
|
+
expect(result.resetToken).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('POST /auth/reset-password', () => {
|
|
138
|
+
it('should call authService.resetPassword and return result', async () => {
|
|
139
|
+
mockAuthService.resetPassword.mockResolvedValue({ message: 'Password reset successfully' });
|
|
140
|
+
|
|
141
|
+
const result = await controller.resetPassword({ token: 'valid-token', newPassword: 'NewP@ssword1' });
|
|
142
|
+
|
|
143
|
+
expect(authService.resetPassword).toHaveBeenCalledWith({ token: 'valid-token', newPassword: 'NewP@ssword1' });
|
|
144
|
+
expect(result).toEqual({ message: 'Password reset successfully' });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
{{/if}}
|
|
148
|
+
|
|
149
|
+
{{#if features.refreshTokens}}
|
|
150
|
+
describe('POST /auth/refresh', () => {
|
|
151
|
+
it('should call authService.refreshAccessToken and return result', async () => {
|
|
152
|
+
const expectedResult = {
|
|
153
|
+
accessToken: 'new-token',
|
|
154
|
+
refreshToken: 'new-refresh-token',
|
|
155
|
+
user: { id: 'uuid', email: 'test@example.com' },
|
|
156
|
+
};
|
|
157
|
+
mockAuthService.refreshAccessToken.mockResolvedValue(expectedResult);
|
|
158
|
+
|
|
159
|
+
const result = await controller.refresh('old-refresh-token');
|
|
160
|
+
|
|
161
|
+
expect(authService.refreshAccessToken).toHaveBeenCalledWith('old-refresh-token');
|
|
162
|
+
expect(result).toEqual(expectedResult);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('POST /auth/logout', () => {
|
|
167
|
+
it('should call authService.logout and return success message', async () => {
|
|
168
|
+
mockAuthService.logout.mockResolvedValue(undefined);
|
|
169
|
+
|
|
170
|
+
const result = await controller.logout('refresh-token');
|
|
171
|
+
|
|
172
|
+
expect(authService.logout).toHaveBeenCalledWith('refresh-token');
|
|
173
|
+
expect(result).toEqual({ message: 'Logged out successfully' });
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('POST /auth/logout-all', () => {
|
|
178
|
+
it('should call authService.logoutAll and return success message', async () => {
|
|
179
|
+
const req = { user: { id: 'test-uuid' } };
|
|
180
|
+
mockAuthService.logoutAll.mockResolvedValue(undefined);
|
|
181
|
+
|
|
182
|
+
const result = await controller.logoutAll(req);
|
|
183
|
+
|
|
184
|
+
expect(authService.logoutAll).toHaveBeenCalledWith('test-uuid');
|
|
185
|
+
expect(result).toEqual({ message: 'Logged out from all devices' });
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
{{/if}}
|
|
189
|
+
});
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { JwtService } from '@nestjs/jwt';
|
|
3
|
+
import { ConfigService } from '@nestjs/config';
|
|
4
|
+
import { UnauthorizedException } from '@nestjs/common';
|
|
5
|
+
import * as bcrypt from 'bcrypt';
|
|
6
|
+
import { AuthService } from './auth.service';
|
|
7
|
+
import { UsersService } from '../users/users.service';
|
|
8
|
+
{{#if (eq orm "typeorm")}}
|
|
9
|
+
{{#if features.refreshTokens}}
|
|
10
|
+
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
11
|
+
import { RefreshToken } from '../users/entities/refresh-token.entity';
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{/if}}
|
|
14
|
+
|
|
15
|
+
jest.mock('bcrypt', () => ({
|
|
16
|
+
compare: jest.fn(),
|
|
17
|
+
hash: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('AuthService', () => {
|
|
21
|
+
let authService: AuthService;
|
|
22
|
+
let usersService: UsersService;
|
|
23
|
+
let jwtService: JwtService;
|
|
24
|
+
{{#if (eq orm "typeorm")}}
|
|
25
|
+
{{#if features.refreshTokens}}
|
|
26
|
+
let refreshTokenRepository: any;
|
|
27
|
+
{{/if}}
|
|
28
|
+
{{/if}}
|
|
29
|
+
|
|
30
|
+
const mockUser = {
|
|
31
|
+
id: 'test-uuid',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
{{#if features.useUsername}}
|
|
34
|
+
username: 'testuser',
|
|
35
|
+
{{/if}}
|
|
36
|
+
password: '$2b$10$hashedpassword',
|
|
37
|
+
{{#if rbac.enabled}}
|
|
38
|
+
roles: ['User'],
|
|
39
|
+
{{/if}}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const mockUsersService = {
|
|
43
|
+
findByEmail: jest.fn(),
|
|
44
|
+
findById: jest.fn(),
|
|
45
|
+
create: jest.fn(),
|
|
46
|
+
updatePassword: jest.fn(),
|
|
47
|
+
{{#if features.emailVerification}}
|
|
48
|
+
setVerificationToken: jest.fn(),
|
|
49
|
+
findByVerificationToken: jest.fn(),
|
|
50
|
+
markEmailVerified: jest.fn(),
|
|
51
|
+
{{/if}}
|
|
52
|
+
{{#if features.resetPassword}}
|
|
53
|
+
setResetToken: jest.fn(),
|
|
54
|
+
findByResetToken: jest.fn(),
|
|
55
|
+
clearResetToken: jest.fn(),
|
|
56
|
+
{{/if}}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mockJwtService = {
|
|
60
|
+
sign: jest.fn().mockReturnValue('mock-jwt-token'),
|
|
61
|
+
verify: jest.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const mockConfigService = {
|
|
65
|
+
get: jest.fn((key: string, defaultValue?: string) => {
|
|
66
|
+
const config: Record<string, string> = {
|
|
67
|
+
BCRYPT_ROUNDS: '10',
|
|
68
|
+
JWT_SECRET: 'test-secret',
|
|
69
|
+
};
|
|
70
|
+
return config[key] || defaultValue;
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
{{#if (eq orm "typeorm")}}
|
|
75
|
+
{{#if features.refreshTokens}}
|
|
76
|
+
const mockRefreshTokenRepository = {
|
|
77
|
+
create: jest.fn(),
|
|
78
|
+
save: jest.fn(),
|
|
79
|
+
findOne: jest.fn(),
|
|
80
|
+
remove: jest.fn(),
|
|
81
|
+
delete: jest.fn(),
|
|
82
|
+
};
|
|
83
|
+
{{/if}}
|
|
84
|
+
{{/if}}
|
|
85
|
+
|
|
86
|
+
beforeEach(async () => {
|
|
87
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
88
|
+
providers: [
|
|
89
|
+
AuthService,
|
|
90
|
+
{ provide: UsersService, useValue: mockUsersService },
|
|
91
|
+
{ provide: JwtService, useValue: mockJwtService },
|
|
92
|
+
{ provide: ConfigService, useValue: mockConfigService },
|
|
93
|
+
{{#if (eq orm "typeorm")}}
|
|
94
|
+
{{#if features.refreshTokens}}
|
|
95
|
+
{ provide: getRepositoryToken(RefreshToken), useValue: mockRefreshTokenRepository },
|
|
96
|
+
{{/if}}
|
|
97
|
+
{{/if}}
|
|
98
|
+
],
|
|
99
|
+
}).compile();
|
|
100
|
+
|
|
101
|
+
authService = module.get<AuthService>(AuthService);
|
|
102
|
+
usersService = module.get<UsersService>(UsersService);
|
|
103
|
+
jwtService = module.get<JwtService>(JwtService);
|
|
104
|
+
{{#if (eq orm "typeorm")}}
|
|
105
|
+
{{#if features.refreshTokens}}
|
|
106
|
+
refreshTokenRepository = module.get(getRepositoryToken(RefreshToken));
|
|
107
|
+
{{/if}}
|
|
108
|
+
{{/if}}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
jest.clearAllMocks();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('validateUser', () => {
|
|
116
|
+
it('should return user without password for valid credentials', async () => {
|
|
117
|
+
mockUsersService.findByEmail.mockResolvedValue(mockUser);
|
|
118
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
|
|
119
|
+
|
|
120
|
+
const result = await authService.validateUser('test@example.com', 'password123');
|
|
121
|
+
|
|
122
|
+
expect(result).toBeDefined();
|
|
123
|
+
expect(result.email).toBe('test@example.com');
|
|
124
|
+
expect(result.password).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return null if user not found', async () => {
|
|
128
|
+
mockUsersService.findByEmail.mockResolvedValue(null);
|
|
129
|
+
|
|
130
|
+
const result = await authService.validateUser('wrong@example.com', 'password123');
|
|
131
|
+
|
|
132
|
+
expect(result).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return null if password is invalid', async () => {
|
|
136
|
+
mockUsersService.findByEmail.mockResolvedValue(mockUser);
|
|
137
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(false);
|
|
138
|
+
|
|
139
|
+
const result = await authService.validateUser('test@example.com', 'wrongpassword');
|
|
140
|
+
|
|
141
|
+
expect(result).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('login', () => {
|
|
146
|
+
it('should return access token and user for valid credentials', async () => {
|
|
147
|
+
mockUsersService.findByEmail.mockResolvedValue(mockUser);
|
|
148
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
|
|
149
|
+
{{#if features.refreshTokens}}
|
|
150
|
+
mockJwtService.sign.mockReturnValue('mock-jwt-token');
|
|
151
|
+
{{#if (eq orm "typeorm")}}
|
|
152
|
+
mockRefreshTokenRepository.create.mockReturnValue({ token: 'mock-refresh-token' });
|
|
153
|
+
mockRefreshTokenRepository.save.mockResolvedValue({ token: 'mock-refresh-token' });
|
|
154
|
+
{{/if}}
|
|
155
|
+
{{/if}}
|
|
156
|
+
|
|
157
|
+
const result = await authService.login({ email: 'test@example.com', password: 'password123' });
|
|
158
|
+
|
|
159
|
+
expect(result.accessToken).toBeDefined();
|
|
160
|
+
expect(result.user.email).toBe('test@example.com');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should throw UnauthorizedException for invalid credentials', async () => {
|
|
164
|
+
mockUsersService.findByEmail.mockResolvedValue(null);
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
authService.login({ email: 'wrong@example.com', password: 'wrongpassword' }),
|
|
168
|
+
).rejects.toThrow(UnauthorizedException);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('register', () => {
|
|
173
|
+
it('should create user and return access token', async () => {
|
|
174
|
+
const newUser = { ...mockUser };
|
|
175
|
+
mockUsersService.create.mockResolvedValue(newUser);
|
|
176
|
+
(bcrypt.hash as jest.Mock).mockResolvedValue('$2b$10$hashedpassword');
|
|
177
|
+
{{#if features.refreshTokens}}
|
|
178
|
+
mockJwtService.sign.mockReturnValue('mock-jwt-token');
|
|
179
|
+
{{#if (eq orm "typeorm")}}
|
|
180
|
+
mockRefreshTokenRepository.create.mockReturnValue({ token: 'mock-refresh-token' });
|
|
181
|
+
mockRefreshTokenRepository.save.mockResolvedValue({ token: 'mock-refresh-token' });
|
|
182
|
+
{{/if}}
|
|
183
|
+
{{/if}}
|
|
184
|
+
|
|
185
|
+
const result = await authService.register({
|
|
186
|
+
email: 'new@example.com',
|
|
187
|
+
{{#if features.useUsername}}
|
|
188
|
+
username: 'newuser',
|
|
189
|
+
{{/if}}
|
|
190
|
+
password: 'StrongP@ss1',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.accessToken).toBeDefined();
|
|
194
|
+
expect(result.user.email).toBe('test@example.com');
|
|
195
|
+
expect(mockUsersService.create).toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('changePassword', () => {
|
|
200
|
+
it('should update password when current password is valid', async () => {
|
|
201
|
+
mockUsersService.findById.mockResolvedValue(mockUser);
|
|
202
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
|
|
203
|
+
(bcrypt.hash as jest.Mock).mockResolvedValue('$2b$10$newhashedpassword');
|
|
204
|
+
mockUsersService.updatePassword.mockResolvedValue(undefined);
|
|
205
|
+
|
|
206
|
+
await authService.changePassword('test-uuid', {
|
|
207
|
+
currentPassword: 'oldpassword',
|
|
208
|
+
newPassword: 'NewP@ssword1',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(mockUsersService.updatePassword).toHaveBeenCalledWith('test-uuid', '$2b$10$newhashedpassword');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should throw UnauthorizedException for wrong current password', async () => {
|
|
215
|
+
mockUsersService.findById.mockResolvedValue(mockUser);
|
|
216
|
+
(bcrypt.compare as jest.Mock).mockResolvedValue(false);
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
authService.changePassword('test-uuid', {
|
|
220
|
+
currentPassword: 'wrongpassword',
|
|
221
|
+
newPassword: 'NewP@ssword1',
|
|
222
|
+
}),
|
|
223
|
+
).rejects.toThrow(UnauthorizedException);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should throw UnauthorizedException if user not found', async () => {
|
|
227
|
+
mockUsersService.findById.mockResolvedValue(null);
|
|
228
|
+
|
|
229
|
+
await expect(
|
|
230
|
+
authService.changePassword('nonexistent-uuid', {
|
|
231
|
+
currentPassword: 'oldpassword',
|
|
232
|
+
newPassword: 'NewP@ssword1',
|
|
233
|
+
}),
|
|
234
|
+
).rejects.toThrow(UnauthorizedException);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
{{#if features.emailVerification}}
|
|
239
|
+
describe('verifyEmail', () => {
|
|
240
|
+
it('should verify email with valid token', async () => {
|
|
241
|
+
mockUsersService.findByVerificationToken.mockResolvedValue(mockUser);
|
|
242
|
+
mockUsersService.markEmailVerified.mockResolvedValue(undefined);
|
|
243
|
+
|
|
244
|
+
const result = await authService.verifyEmail('valid-token');
|
|
245
|
+
|
|
246
|
+
expect(result.message).toBe('Email verified successfully');
|
|
247
|
+
expect(mockUsersService.markEmailVerified).toHaveBeenCalledWith('test-uuid');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should throw NotFoundException for invalid token', async () => {
|
|
251
|
+
mockUsersService.findByVerificationToken.mockResolvedValue(null);
|
|
252
|
+
|
|
253
|
+
await expect(authService.verifyEmail('invalid-token')).rejects.toThrow();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('resendVerification', () => {
|
|
258
|
+
it('should generate new verification token', async () => {
|
|
259
|
+
mockUsersService.findByEmail.mockResolvedValue({ ...mockUser, isEmailVerified: false });
|
|
260
|
+
mockUsersService.setVerificationToken.mockResolvedValue(undefined);
|
|
261
|
+
|
|
262
|
+
const result = await authService.resendVerification('test@example.com');
|
|
263
|
+
|
|
264
|
+
expect(result.verificationToken).toBeDefined();
|
|
265
|
+
expect(mockUsersService.setVerificationToken).toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
{{/if}}
|
|
269
|
+
|
|
270
|
+
{{#if features.resetPassword}}
|
|
271
|
+
describe('forgotPassword', () => {
|
|
272
|
+
it('should generate reset token for existing user', async () => {
|
|
273
|
+
mockUsersService.findByEmail.mockResolvedValue(mockUser);
|
|
274
|
+
mockUsersService.setResetToken.mockResolvedValue(undefined);
|
|
275
|
+
|
|
276
|
+
const result = await authService.forgotPassword({ email: 'test@example.com' });
|
|
277
|
+
|
|
278
|
+
expect(result.resetToken).toBeDefined();
|
|
279
|
+
expect(mockUsersService.setResetToken).toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return success even if user not found (prevent enumeration)', async () => {
|
|
283
|
+
mockUsersService.findByEmail.mockResolvedValue(null);
|
|
284
|
+
|
|
285
|
+
const result = await authService.forgotPassword({ email: 'unknown@example.com' });
|
|
286
|
+
|
|
287
|
+
expect(result.message).toBeDefined();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('resetPassword', () => {
|
|
292
|
+
it('should reset password with valid token', async () => {
|
|
293
|
+
const futureDate = new Date();
|
|
294
|
+
futureDate.setHours(futureDate.getHours() + 1);
|
|
295
|
+
mockUsersService.findByResetToken.mockResolvedValue({ ...mockUser, passwordResetExpires: futureDate });
|
|
296
|
+
(bcrypt.hash as jest.Mock).mockResolvedValue('$2b$10$newhashedpassword');
|
|
297
|
+
mockUsersService.updatePassword.mockResolvedValue(undefined);
|
|
298
|
+
mockUsersService.clearResetToken.mockResolvedValue(undefined);
|
|
299
|
+
|
|
300
|
+
const result = await authService.resetPassword({ token: 'valid-token', newPassword: 'NewP@ssword1' });
|
|
301
|
+
|
|
302
|
+
expect(result.message).toBe('Password reset successfully');
|
|
303
|
+
expect(mockUsersService.updatePassword).toHaveBeenCalled();
|
|
304
|
+
expect(mockUsersService.clearResetToken).toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should throw BadRequestException for invalid token', async () => {
|
|
308
|
+
mockUsersService.findByResetToken.mockResolvedValue(null);
|
|
309
|
+
|
|
310
|
+
await expect(
|
|
311
|
+
authService.resetPassword({ token: 'invalid-token', newPassword: 'NewP@ssword1' }),
|
|
312
|
+
).rejects.toThrow();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
{{/if}}
|
|
316
|
+
|
|
317
|
+
{{#if features.refreshTokens}}
|
|
318
|
+
describe('logout', () => {
|
|
319
|
+
it('should delete the refresh token', async () => {
|
|
320
|
+
{{#if (eq orm "typeorm")}}
|
|
321
|
+
const storedToken = { id: '1', token: 'refresh-token' };
|
|
322
|
+
mockRefreshTokenRepository.findOne.mockResolvedValue(storedToken);
|
|
323
|
+
mockRefreshTokenRepository.remove.mockResolvedValue(undefined);
|
|
324
|
+
{{/if}}
|
|
325
|
+
|
|
326
|
+
await authService.logout('refresh-token');
|
|
327
|
+
|
|
328
|
+
{{#if (eq orm "typeorm")}}
|
|
329
|
+
expect(mockRefreshTokenRepository.findOne).toHaveBeenCalled();
|
|
330
|
+
{{/if}}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
{{/if}}
|
|
334
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Controller, Get{{#if rbac.enabled}}, UseGuards{{/if}} } from '@nestjs/common';
|
|
2
|
+
{{#if rbac.enabled}}
|
|
3
|
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
4
|
+
import { RolesGuard } from '../auth/guards/roles.guard';
|
|
5
|
+
import { Roles } from '../auth/decorators/roles.decorator';
|
|
6
|
+
{{/if}}
|
|
7
|
+
{{#if features.rateLimiting}}
|
|
8
|
+
import { SkipThrottle } from '@nestjs/throttler';
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if features.swagger}}
|
|
11
|
+
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
|
12
|
+
{{/if}}
|
|
13
|
+
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
|
14
|
+
import { UsersService } from './users.service';
|
|
15
|
+
|
|
16
|
+
@Controller('users')
|
|
17
|
+
{{#if features.rateLimiting}}
|
|
18
|
+
@SkipThrottle()
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{#if features.swagger}}
|
|
21
|
+
@ApiTags('Users')
|
|
22
|
+
@ApiBearerAuth()
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if rbac.enabled}}
|
|
25
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
26
|
+
{{else}}
|
|
27
|
+
@UseGuards(JwtAuthGuard)
|
|
28
|
+
{{/if}}
|
|
29
|
+
export class UsersController {
|
|
30
|
+
constructor(private readonly usersService: UsersService) {}
|
|
31
|
+
|
|
32
|
+
@Get('profile')
|
|
33
|
+
{{#if features.swagger}}
|
|
34
|
+
@ApiOperation({ summary: 'Get current user profile' })
|
|
35
|
+
@ApiResponse({ status: 200, description: 'User profile returned' })
|
|
36
|
+
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
37
|
+
{{/if}}
|
|
38
|
+
getProfile(@CurrentUser() user: any) {
|
|
39
|
+
return user;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
{{#if rbac.enabled}}
|
|
43
|
+
@Get()
|
|
44
|
+
@Roles('Admin')
|
|
45
|
+
{{#if features.swagger}}
|
|
46
|
+
@ApiOperation({ summary: 'Get all users (Admin only)' })
|
|
47
|
+
@ApiResponse({ status: 200, description: 'List of users returned' })
|
|
48
|
+
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
49
|
+
@ApiResponse({ status: 403, description: 'Forbidden - Admin role required' })
|
|
50
|
+
{{/if}}
|
|
51
|
+
findAll() {
|
|
52
|
+
return this.usersService.findAll();
|
|
53
|
+
}
|
|
54
|
+
{{/if}}
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
{{#if (eq orm "typeorm")}}
|
|
3
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
4
|
+
import { User } from './entities/user.entity';
|
|
5
|
+
{{#if features.refreshTokens}}
|
|
6
|
+
import { RefreshToken } from './entities/refresh-token.entity';
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{else if (eq orm "prisma")}}
|
|
9
|
+
import { PrismaModule } from '../prisma/prisma.module';
|
|
10
|
+
{{/if}}
|
|
11
|
+
import { UsersService } from './users.service';
|
|
12
|
+
import { UsersController } from './users.controller';
|
|
13
|
+
|
|
14
|
+
@Module({
|
|
15
|
+
{{#if (eq orm "typeorm")}}
|
|
16
|
+
imports: [
|
|
17
|
+
TypeOrmModule.forFeature([
|
|
18
|
+
User,
|
|
19
|
+
{{#if features.refreshTokens}}
|
|
20
|
+
RefreshToken,
|
|
21
|
+
{{/if}}
|
|
22
|
+
]),
|
|
23
|
+
],
|
|
24
|
+
{{else if (eq orm "prisma")}}
|
|
25
|
+
imports: [PrismaModule],
|
|
26
|
+
{{/if}}
|
|
27
|
+
controllers: [UsersController],
|
|
28
|
+
providers: [UsersService],
|
|
29
|
+
exports: [UsersService],
|
|
30
|
+
})
|
|
31
|
+
export class UsersModule {}
|