@ciscode/authentication-kit 1.4.0 → 1.4.2
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/README.md +58 -9
- package/dist/auth-kit.module.js +2 -0
- package/dist/controllers/auth.controller.d.ts +1 -0
- package/dist/controllers/auth.controller.js +17 -0
- package/dist/controllers/health.controller.d.ts +48 -0
- package/dist/controllers/health.controller.js +77 -0
- package/dist/services/auth.service.d.ts +42 -0
- package/dist/services/auth.service.js +79 -9
- package/dist/services/mail.service.d.ts +7 -0
- package/dist/services/mail.service.js +76 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,17 +107,18 @@ export class AppModule implements OnModuleInit {
|
|
|
107
107
|
|
|
108
108
|
## API Routes
|
|
109
109
|
|
|
110
|
-
### Local Auth Routes
|
|
110
|
+
### Local Auth Routes
|
|
111
111
|
|
|
112
112
|
```
|
|
113
|
-
POST /api/auth/register
|
|
114
|
-
POST /api/auth/verify-email
|
|
115
|
-
POST /api/auth/resend-verification
|
|
116
|
-
POST /api/auth/login
|
|
117
|
-
POST /api/auth/refresh-token
|
|
118
|
-
POST /api/auth/forgot-password
|
|
119
|
-
POST /api/auth/reset-password
|
|
120
|
-
|
|
113
|
+
POST /api/auth/register | Register new user (public)
|
|
114
|
+
POST /api/auth/verify-email | Verify email with token (public)
|
|
115
|
+
POST /api/auth/resend-verification | Resend verification email (public)
|
|
116
|
+
POST /api/auth/login | Login with credentials (public)
|
|
117
|
+
POST /api/auth/refresh-token | Refresh access token (public)
|
|
118
|
+
POST /api/auth/forgot-password | Request password reset (public)
|
|
119
|
+
POST /api/auth/reset-password | Reset password with token (public)
|
|
120
|
+
GET /api/auth/me | Get current user profile (protected)
|
|
121
|
+
DELETE /api/auth/account | Delete own account (protected)
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
### OAuth Routes - Mobile Exchange (Public)
|
|
@@ -321,6 +322,54 @@ Content-Type: application/json
|
|
|
321
322
|
}
|
|
322
323
|
```
|
|
323
324
|
|
|
325
|
+
### Get Current User Profile
|
|
326
|
+
|
|
327
|
+
**Request:**
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
GET /api/auth/me
|
|
331
|
+
Authorization: Bearer access-token
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Response:**
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"ok": true,
|
|
339
|
+
"data": {
|
|
340
|
+
"_id": "507f1f77bcf86cd799439011",
|
|
341
|
+
"fullname": {
|
|
342
|
+
"fname": "Test",
|
|
343
|
+
"lname": "User"
|
|
344
|
+
},
|
|
345
|
+
"username": "test-user",
|
|
346
|
+
"email": "user@example.com",
|
|
347
|
+
"avatar": "https://example.com/avatar.jpg",
|
|
348
|
+
"phoneNumber": "+1234567890",
|
|
349
|
+
"jobTitle": "Software Engineer",
|
|
350
|
+
"company": "Ciscode",
|
|
351
|
+
"isVerified": true,
|
|
352
|
+
"isBanned": false,
|
|
353
|
+
"roles": [
|
|
354
|
+
{
|
|
355
|
+
"_id": "507f1f77bcf86cd799439012",
|
|
356
|
+
"name": "user",
|
|
357
|
+
"permissions": [
|
|
358
|
+
{
|
|
359
|
+
"_id": "507f1f77bcf86cd799439013",
|
|
360
|
+
"name": "read:profile"
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
"createdAt": "2026-01-28T10:00:00.000Z",
|
|
366
|
+
"updatedAt": "2026-01-28T10:00:00.000Z"
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Note:** Sensitive fields like `password` and `passwordChangedAt` are automatically excluded from the response.
|
|
372
|
+
|
|
324
373
|
### Delete Account
|
|
325
374
|
|
|
326
375
|
**Request:**
|
package/dist/auth-kit.module.js
CHANGED
|
@@ -22,6 +22,7 @@ const auth_controller_1 = require("./controllers/auth.controller");
|
|
|
22
22
|
const users_controller_1 = require("./controllers/users.controller");
|
|
23
23
|
const roles_controller_1 = require("./controllers/roles.controller");
|
|
24
24
|
const permissions_controller_1 = require("./controllers/permissions.controller");
|
|
25
|
+
const health_controller_1 = require("./controllers/health.controller");
|
|
25
26
|
const user_model_1 = require("./models/user.model");
|
|
26
27
|
const role_model_1 = require("./models/role.model");
|
|
27
28
|
const permission_model_1 = require("./models/permission.model");
|
|
@@ -70,6 +71,7 @@ exports.AuthKitModule = AuthKitModule = __decorate([
|
|
|
70
71
|
users_controller_1.UsersController,
|
|
71
72
|
roles_controller_1.RolesController,
|
|
72
73
|
permissions_controller_1.PermissionsController,
|
|
74
|
+
health_controller_1.HealthController,
|
|
73
75
|
],
|
|
74
76
|
providers: [
|
|
75
77
|
{
|
|
@@ -19,6 +19,7 @@ export declare class AuthController {
|
|
|
19
19
|
refresh(dto: RefreshTokenDto, req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
20
20
|
forgotPassword(dto: ForgotPasswordDto, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
21
21
|
resetPassword(dto: ResetPasswordDto, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
22
|
+
getMe(req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
22
23
|
deleteAccount(req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
23
24
|
microsoftExchange(body: {
|
|
24
25
|
idToken: string;
|
|
@@ -84,6 +84,14 @@ let AuthController = class AuthController {
|
|
|
84
84
|
const result = await this.auth.resetPassword(dto.token, dto.newPassword);
|
|
85
85
|
return res.status(200).json(result);
|
|
86
86
|
}
|
|
87
|
+
async getMe(req, res) {
|
|
88
|
+
var _a;
|
|
89
|
+
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.sub;
|
|
90
|
+
if (!userId)
|
|
91
|
+
return res.status(401).json({ message: 'Unauthorized.' });
|
|
92
|
+
const result = await this.auth.getMe(userId);
|
|
93
|
+
return res.status(200).json(result);
|
|
94
|
+
}
|
|
87
95
|
async deleteAccount(req, res) {
|
|
88
96
|
var _a;
|
|
89
97
|
const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.sub;
|
|
@@ -202,6 +210,15 @@ __decorate([
|
|
|
202
210
|
__metadata("design:paramtypes", [reset_password_dto_1.ResetPasswordDto, Object]),
|
|
203
211
|
__metadata("design:returntype", Promise)
|
|
204
212
|
], AuthController.prototype, "resetPassword", null);
|
|
213
|
+
__decorate([
|
|
214
|
+
(0, common_1.Get)('me'),
|
|
215
|
+
(0, common_1.UseGuards)(authenticate_guard_1.AuthenticateGuard),
|
|
216
|
+
__param(0, (0, common_1.Req)()),
|
|
217
|
+
__param(1, (0, common_1.Res)()),
|
|
218
|
+
__metadata("design:type", Function),
|
|
219
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
220
|
+
__metadata("design:returntype", Promise)
|
|
221
|
+
], AuthController.prototype, "getMe", null);
|
|
205
222
|
__decorate([
|
|
206
223
|
(0, common_1.Delete)('account'),
|
|
207
224
|
(0, common_1.UseGuards)(authenticate_guard_1.AuthenticateGuard),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MailService } from '../services/mail.service';
|
|
2
|
+
import { LoggerService } from '../services/logger.service';
|
|
3
|
+
export declare class HealthController {
|
|
4
|
+
private readonly mail;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(mail: MailService, logger: LoggerService);
|
|
7
|
+
checkSmtp(): Promise<{
|
|
8
|
+
config: {
|
|
9
|
+
host: string;
|
|
10
|
+
port: string;
|
|
11
|
+
secure: string;
|
|
12
|
+
user: string;
|
|
13
|
+
fromEmail: string;
|
|
14
|
+
};
|
|
15
|
+
error: string;
|
|
16
|
+
service: string;
|
|
17
|
+
status: string;
|
|
18
|
+
} | {
|
|
19
|
+
service: string;
|
|
20
|
+
status: string;
|
|
21
|
+
error: any;
|
|
22
|
+
}>;
|
|
23
|
+
checkAll(): Promise<{
|
|
24
|
+
status: string;
|
|
25
|
+
checks: {
|
|
26
|
+
smtp: {
|
|
27
|
+
config: {
|
|
28
|
+
host: string;
|
|
29
|
+
port: string;
|
|
30
|
+
secure: string;
|
|
31
|
+
user: string;
|
|
32
|
+
fromEmail: string;
|
|
33
|
+
};
|
|
34
|
+
error: string;
|
|
35
|
+
service: string;
|
|
36
|
+
status: string;
|
|
37
|
+
} | {
|
|
38
|
+
service: string;
|
|
39
|
+
status: string;
|
|
40
|
+
error: any;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
environment: {
|
|
44
|
+
nodeEnv: string;
|
|
45
|
+
frontendUrl: string;
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.HealthController = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const mail_service_1 = require("../services/mail.service");
|
|
15
|
+
const logger_service_1 = require("../services/logger.service");
|
|
16
|
+
let HealthController = class HealthController {
|
|
17
|
+
constructor(mail, logger) {
|
|
18
|
+
this.mail = mail;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
}
|
|
21
|
+
async checkSmtp() {
|
|
22
|
+
try {
|
|
23
|
+
const result = await this.mail.verifyConnection();
|
|
24
|
+
return {
|
|
25
|
+
service: 'smtp',
|
|
26
|
+
status: result.connected ? 'connected' : 'disconnected',
|
|
27
|
+
...(result.error && { error: result.error }),
|
|
28
|
+
config: {
|
|
29
|
+
host: process.env.SMTP_HOST || 'not set',
|
|
30
|
+
port: process.env.SMTP_PORT || 'not set',
|
|
31
|
+
secure: process.env.SMTP_SECURE || 'not set',
|
|
32
|
+
user: process.env.SMTP_USER ? '***' + process.env.SMTP_USER.slice(-4) : 'not set',
|
|
33
|
+
fromEmail: process.env.FROM_EMAIL || 'not set',
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
this.logger.error(`SMTP health check failed: ${error.message}`, error.stack, 'HealthController');
|
|
39
|
+
return {
|
|
40
|
+
service: 'smtp',
|
|
41
|
+
status: 'error',
|
|
42
|
+
error: error.message
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async checkAll() {
|
|
47
|
+
const smtp = await this.checkSmtp();
|
|
48
|
+
return {
|
|
49
|
+
status: smtp.status === 'connected' ? 'healthy' : 'degraded',
|
|
50
|
+
checks: {
|
|
51
|
+
smtp
|
|
52
|
+
},
|
|
53
|
+
environment: {
|
|
54
|
+
nodeEnv: process.env.NODE_ENV || 'not set',
|
|
55
|
+
frontendUrl: process.env.FRONTEND_URL || 'not set',
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
exports.HealthController = HealthController;
|
|
61
|
+
__decorate([
|
|
62
|
+
(0, common_1.Get)('smtp'),
|
|
63
|
+
__metadata("design:type", Function),
|
|
64
|
+
__metadata("design:paramtypes", []),
|
|
65
|
+
__metadata("design:returntype", Promise)
|
|
66
|
+
], HealthController.prototype, "checkSmtp", null);
|
|
67
|
+
__decorate([
|
|
68
|
+
(0, common_1.Get)(),
|
|
69
|
+
__metadata("design:type", Function),
|
|
70
|
+
__metadata("design:paramtypes", []),
|
|
71
|
+
__metadata("design:returntype", Promise)
|
|
72
|
+
], HealthController.prototype, "checkAll", null);
|
|
73
|
+
exports.HealthController = HealthController = __decorate([
|
|
74
|
+
(0, common_1.Controller)('api/health'),
|
|
75
|
+
__metadata("design:paramtypes", [mail_service_1.MailService,
|
|
76
|
+
logger_service_1.LoggerService])
|
|
77
|
+
], HealthController);
|
|
@@ -21,9 +21,17 @@ export declare class AuthService {
|
|
|
21
21
|
accessToken: string;
|
|
22
22
|
refreshToken: string;
|
|
23
23
|
}>;
|
|
24
|
+
getMe(userId: string): Promise<{
|
|
25
|
+
ok: boolean;
|
|
26
|
+
data: any;
|
|
27
|
+
}>;
|
|
24
28
|
register(dto: RegisterDto): Promise<{
|
|
29
|
+
emailError: string;
|
|
30
|
+
emailHint: string;
|
|
31
|
+
ok: boolean;
|
|
25
32
|
id: any;
|
|
26
33
|
email: string;
|
|
34
|
+
emailSent: boolean;
|
|
27
35
|
}>;
|
|
28
36
|
verifyEmail(token: string): Promise<{
|
|
29
37
|
ok: boolean;
|
|
@@ -32,6 +40,23 @@ export declare class AuthService {
|
|
|
32
40
|
resendVerification(email: string): Promise<{
|
|
33
41
|
ok: boolean;
|
|
34
42
|
message: string;
|
|
43
|
+
emailSent?: undefined;
|
|
44
|
+
error?: undefined;
|
|
45
|
+
} | {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
emailSent: boolean;
|
|
49
|
+
error?: undefined;
|
|
50
|
+
} | {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
message: string;
|
|
53
|
+
emailSent: boolean;
|
|
54
|
+
error: any;
|
|
55
|
+
} | {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
message: string;
|
|
58
|
+
error: any;
|
|
59
|
+
emailSent?: undefined;
|
|
35
60
|
}>;
|
|
36
61
|
login(dto: LoginDto): Promise<{
|
|
37
62
|
accessToken: string;
|
|
@@ -44,6 +69,23 @@ export declare class AuthService {
|
|
|
44
69
|
forgotPassword(email: string): Promise<{
|
|
45
70
|
ok: boolean;
|
|
46
71
|
message: string;
|
|
72
|
+
emailSent?: undefined;
|
|
73
|
+
error?: undefined;
|
|
74
|
+
} | {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
message: string;
|
|
77
|
+
emailSent: boolean;
|
|
78
|
+
error?: undefined;
|
|
79
|
+
} | {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
message: string;
|
|
82
|
+
emailSent: boolean;
|
|
83
|
+
error: any;
|
|
84
|
+
} | {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
message: string;
|
|
87
|
+
error: any;
|
|
88
|
+
emailSent?: undefined;
|
|
47
89
|
}>;
|
|
48
90
|
resetPassword(token: string, newPassword: string): Promise<{
|
|
49
91
|
ok: boolean;
|
|
@@ -113,6 +113,31 @@ let AuthService = class AuthService {
|
|
|
113
113
|
const refreshToken = this.signRefreshToken({ sub: userId, purpose: 'refresh' });
|
|
114
114
|
return { accessToken, refreshToken };
|
|
115
115
|
}
|
|
116
|
+
async getMe(userId) {
|
|
117
|
+
try {
|
|
118
|
+
const user = await this.users.findByIdWithRolesAndPermissions(userId);
|
|
119
|
+
if (!user) {
|
|
120
|
+
throw new common_1.NotFoundException('User not found');
|
|
121
|
+
}
|
|
122
|
+
if (user.isBanned) {
|
|
123
|
+
throw new common_1.ForbiddenException('Account has been banned. Please contact support');
|
|
124
|
+
}
|
|
125
|
+
// Return user data without sensitive information
|
|
126
|
+
const userObject = user.toObject ? user.toObject() : user;
|
|
127
|
+
const { password, passwordChangedAt, ...safeUser } = userObject;
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
data: safeUser
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof common_1.NotFoundException || error instanceof common_1.ForbiddenException) {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
this.logger.error(`Get profile failed: ${error.message}`, error.stack, 'AuthService');
|
|
138
|
+
throw new common_1.InternalServerErrorException('Failed to retrieve profile');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
116
141
|
async register(dto) {
|
|
117
142
|
try {
|
|
118
143
|
// Generate username from fname-lname if not provided
|
|
@@ -160,15 +185,25 @@ let AuthService = class AuthService {
|
|
|
160
185
|
passwordChangedAt: new Date()
|
|
161
186
|
});
|
|
162
187
|
// Send verification email (don't let email failures crash registration)
|
|
188
|
+
let emailSent = true;
|
|
189
|
+
let emailError;
|
|
163
190
|
try {
|
|
164
191
|
const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' });
|
|
165
192
|
await this.mail.sendVerificationEmail(user.email, emailToken);
|
|
166
193
|
}
|
|
167
194
|
catch (error) {
|
|
195
|
+
emailSent = false;
|
|
196
|
+
emailError = error.message || 'Failed to send verification email';
|
|
168
197
|
this.logger.error(`Failed to send verification email: ${error.message}`, error.stack, 'AuthService');
|
|
169
198
|
// Continue - user is created, they can resend verification
|
|
170
199
|
}
|
|
171
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
id: user._id,
|
|
203
|
+
email: user.email,
|
|
204
|
+
emailSent,
|
|
205
|
+
...(emailError && { emailError, emailHint: 'User created successfully. You can resend verification email later.' })
|
|
206
|
+
};
|
|
172
207
|
}
|
|
173
208
|
catch (error) {
|
|
174
209
|
// Re-throw HTTP exceptions
|
|
@@ -222,13 +257,29 @@ let AuthService = class AuthService {
|
|
|
222
257
|
return { ok: true, message: 'If the email exists and is unverified, a verification email has been sent' };
|
|
223
258
|
}
|
|
224
259
|
const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' });
|
|
225
|
-
|
|
226
|
-
|
|
260
|
+
try {
|
|
261
|
+
await this.mail.sendVerificationEmail(user.email, emailToken);
|
|
262
|
+
return { ok: true, message: 'Verification email sent successfully', emailSent: true };
|
|
263
|
+
}
|
|
264
|
+
catch (emailError) {
|
|
265
|
+
// Log the actual error but return generic message
|
|
266
|
+
this.logger.error(`Failed to send verification email: ${emailError.message}`, emailError.stack, 'AuthService');
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
message: 'Failed to send verification email',
|
|
270
|
+
emailSent: false,
|
|
271
|
+
error: emailError.message || 'Email service error'
|
|
272
|
+
};
|
|
273
|
+
}
|
|
227
274
|
}
|
|
228
275
|
catch (error) {
|
|
229
276
|
this.logger.error(`Resend verification failed: ${error.message}`, error.stack, 'AuthService');
|
|
230
|
-
// Return
|
|
231
|
-
return {
|
|
277
|
+
// Return error details for debugging
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
message: 'Failed to resend verification email',
|
|
281
|
+
error: error.message
|
|
282
|
+
};
|
|
232
283
|
}
|
|
233
284
|
}
|
|
234
285
|
async login(dto) {
|
|
@@ -303,17 +354,36 @@ let AuthService = class AuthService {
|
|
|
303
354
|
async forgotPassword(email) {
|
|
304
355
|
try {
|
|
305
356
|
const user = await this.users.findByEmail(email);
|
|
306
|
-
// Always return success to prevent email enumeration
|
|
357
|
+
// Always return success to prevent email enumeration (in production)
|
|
307
358
|
if (!user) {
|
|
308
359
|
return { ok: true, message: 'If the email exists, a password reset link has been sent' };
|
|
309
360
|
}
|
|
310
361
|
const resetToken = this.signResetToken({ sub: user._id.toString(), purpose: 'reset' });
|
|
311
|
-
|
|
312
|
-
|
|
362
|
+
try {
|
|
363
|
+
await this.mail.sendPasswordResetEmail(user.email, resetToken);
|
|
364
|
+
return { ok: true, message: 'Password reset link sent successfully', emailSent: true };
|
|
365
|
+
}
|
|
366
|
+
catch (emailError) {
|
|
367
|
+
// Log the actual error but return generic message for security
|
|
368
|
+
this.logger.error(`Failed to send reset email: ${emailError.message}`, emailError.stack, 'AuthService');
|
|
369
|
+
// In development, return error details; in production, hide for security
|
|
370
|
+
if (process.env.NODE_ENV === 'development') {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
message: 'Failed to send password reset email',
|
|
374
|
+
emailSent: false,
|
|
375
|
+
error: emailError.message
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, message: 'If the email exists, a password reset link has been sent' };
|
|
379
|
+
}
|
|
313
380
|
}
|
|
314
381
|
catch (error) {
|
|
315
382
|
this.logger.error(`Forgot password failed: ${error.message}`, error.stack, 'AuthService');
|
|
316
|
-
//
|
|
383
|
+
// In development, return error; in production, hide for security
|
|
384
|
+
if (process.env.NODE_ENV === 'development') {
|
|
385
|
+
return { ok: false, message: 'Failed to process password reset', error: error.message };
|
|
386
|
+
}
|
|
317
387
|
return { ok: true, message: 'If the email exists, a password reset link has been sent' };
|
|
318
388
|
}
|
|
319
389
|
}
|
|
@@ -2,7 +2,14 @@ import { LoggerService } from './logger.service';
|
|
|
2
2
|
export declare class MailService {
|
|
3
3
|
private readonly logger;
|
|
4
4
|
private transporter;
|
|
5
|
+
private smtpConfigured;
|
|
5
6
|
constructor(logger: LoggerService);
|
|
7
|
+
private initializeTransporter;
|
|
8
|
+
verifyConnection(): Promise<{
|
|
9
|
+
connected: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
}>;
|
|
6
12
|
sendVerificationEmail(email: string, token: string): Promise<void>;
|
|
7
13
|
sendPasswordResetEmail(email: string, token: string): Promise<void>;
|
|
14
|
+
private getDetailedSmtpError;
|
|
8
15
|
}
|
|
@@ -19,17 +19,56 @@ const logger_service_1 = require("./logger.service");
|
|
|
19
19
|
let MailService = class MailService {
|
|
20
20
|
constructor(logger) {
|
|
21
21
|
this.logger = logger;
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
this.smtpConfigured = false;
|
|
23
|
+
this.initializeTransporter();
|
|
24
|
+
}
|
|
25
|
+
initializeTransporter() {
|
|
26
|
+
try {
|
|
27
|
+
// Check if SMTP is configured
|
|
28
|
+
if (!process.env.SMTP_HOST || !process.env.SMTP_PORT) {
|
|
29
|
+
this.logger.warn('SMTP not configured - email functionality will be disabled', 'MailService');
|
|
30
|
+
this.smtpConfigured = false;
|
|
31
|
+
return;
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
this.transporter = nodemailer_1.default.createTransport({
|
|
34
|
+
host: process.env.SMTP_HOST,
|
|
35
|
+
port: parseInt(process.env.SMTP_PORT, 10),
|
|
36
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
37
|
+
auth: {
|
|
38
|
+
user: process.env.SMTP_USER,
|
|
39
|
+
pass: process.env.SMTP_PASS
|
|
40
|
+
},
|
|
41
|
+
connectionTimeout: 10000, // 10 seconds
|
|
42
|
+
greetingTimeout: 10000,
|
|
43
|
+
});
|
|
44
|
+
this.smtpConfigured = true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.logger.error(`Failed to initialize SMTP transporter: ${error.message}`, error.stack, 'MailService');
|
|
48
|
+
this.smtpConfigured = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async verifyConnection() {
|
|
52
|
+
if (!this.smtpConfigured) {
|
|
53
|
+
return { connected: false, error: 'SMTP not configured' };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await this.transporter.verify();
|
|
57
|
+
this.logger.log('SMTP connection verified successfully', 'MailService');
|
|
58
|
+
return { connected: true };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const errorMsg = `SMTP connection failed: ${error.message}`;
|
|
62
|
+
this.logger.error(errorMsg, error.stack, 'MailService');
|
|
63
|
+
return { connected: false, error: errorMsg };
|
|
64
|
+
}
|
|
31
65
|
}
|
|
32
66
|
async sendVerificationEmail(email, token) {
|
|
67
|
+
if (!this.smtpConfigured) {
|
|
68
|
+
const error = new common_1.InternalServerErrorException('SMTP not configured - cannot send emails');
|
|
69
|
+
this.logger.error('Attempted to send email but SMTP is not configured', '', 'MailService');
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
33
72
|
try {
|
|
34
73
|
const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`;
|
|
35
74
|
await this.transporter.sendMail({
|
|
@@ -42,11 +81,17 @@ let MailService = class MailService {
|
|
|
42
81
|
this.logger.log(`Verification email sent to ${email}`, 'MailService');
|
|
43
82
|
}
|
|
44
83
|
catch (error) {
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
const detailedError = this.getDetailedSmtpError(error);
|
|
85
|
+
this.logger.error(`Failed to send verification email to ${email}: ${detailedError}`, error.stack, 'MailService');
|
|
86
|
+
throw new common_1.InternalServerErrorException(detailedError);
|
|
47
87
|
}
|
|
48
88
|
}
|
|
49
89
|
async sendPasswordResetEmail(email, token) {
|
|
90
|
+
if (!this.smtpConfigured) {
|
|
91
|
+
const error = new common_1.InternalServerErrorException('SMTP not configured - cannot send emails');
|
|
92
|
+
this.logger.error('Attempted to send email but SMTP is not configured', '', 'MailService');
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
50
95
|
try {
|
|
51
96
|
const url = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
|
|
52
97
|
await this.transporter.sendMail({
|
|
@@ -59,9 +104,28 @@ let MailService = class MailService {
|
|
|
59
104
|
this.logger.log(`Password reset email sent to ${email}`, 'MailService');
|
|
60
105
|
}
|
|
61
106
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
const detailedError = this.getDetailedSmtpError(error);
|
|
108
|
+
this.logger.error(`Failed to send password reset email to ${email}: ${detailedError}`, error.stack, 'MailService');
|
|
109
|
+
throw new common_1.InternalServerErrorException(detailedError);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
getDetailedSmtpError(error) {
|
|
113
|
+
if (error.code === 'EAUTH') {
|
|
114
|
+
return 'SMTP authentication failed. Check SMTP_USER and SMTP_PASS environment variables.';
|
|
115
|
+
}
|
|
116
|
+
if (error.code === 'ESOCKET' || error.code === 'ECONNECTION') {
|
|
117
|
+
return `Cannot connect to SMTP server at ${process.env.SMTP_HOST}:${process.env.SMTP_PORT}. Check network/firewall settings.`;
|
|
118
|
+
}
|
|
119
|
+
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
|
120
|
+
return 'SMTP connection timed out. Server may be unreachable or firewalled.';
|
|
121
|
+
}
|
|
122
|
+
if (error.responseCode >= 500) {
|
|
123
|
+
return `SMTP server error (${error.responseCode}): ${error.response}`;
|
|
124
|
+
}
|
|
125
|
+
if (error.responseCode >= 400) {
|
|
126
|
+
return `SMTP client error (${error.responseCode}): Check FROM_EMAIL and recipient addresses.`;
|
|
64
127
|
}
|
|
128
|
+
return error.message || 'Unknown SMTP error';
|
|
65
129
|
}
|
|
66
130
|
};
|
|
67
131
|
exports.MailService = MailService;
|