@ciscode/authentication-kit 1.4.1 → 1.5.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.
@@ -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
  {
@@ -14,6 +14,7 @@ export declare class AuthController {
14
14
  constructor(auth: AuthService, oauth: OAuthService);
15
15
  register(dto: RegisterDto, res: Response): Promise<Response<any, Record<string, any>>>;
16
16
  verifyEmail(dto: VerifyEmailDto, res: Response): Promise<Response<any, Record<string, any>>>;
17
+ verifyEmailGet(token: string, res: Response): Promise<void>;
17
18
  resendVerification(dto: ResendVerificationDto, res: Response): Promise<Response<any, Record<string, any>>>;
18
19
  login(dto: LoginDto, res: Response): Promise<Response<any, Record<string, any>>>;
19
20
  refresh(dto: RefreshTokenDto, req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
@@ -42,6 +42,20 @@ let AuthController = class AuthController {
42
42
  const result = await this.auth.verifyEmail(dto.token);
43
43
  return res.status(200).json(result);
44
44
  }
45
+ async verifyEmailGet(token, res) {
46
+ try {
47
+ const result = await this.auth.verifyEmail(token);
48
+ // Redirect to frontend with success
49
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
50
+ return res.redirect(`${frontendUrl}/email-verified?success=true&message=${encodeURIComponent(result.message)}`);
51
+ }
52
+ catch (error) {
53
+ // Redirect to frontend with error
54
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
55
+ const errorMsg = error.message || 'Email verification failed';
56
+ return res.redirect(`${frontendUrl}/email-verified?success=false&message=${encodeURIComponent(errorMsg)}`);
57
+ }
58
+ }
45
59
  async resendVerification(dto, res) {
46
60
  const result = await this.auth.resendVerification(dto.email);
47
61
  return res.status(200).json(result);
@@ -169,6 +183,14 @@ __decorate([
169
183
  __metadata("design:paramtypes", [verify_email_dto_1.VerifyEmailDto, Object]),
170
184
  __metadata("design:returntype", Promise)
171
185
  ], AuthController.prototype, "verifyEmail", null);
186
+ __decorate([
187
+ (0, common_1.Get)('verify-email/:token'),
188
+ __param(0, (0, common_1.Param)('token')),
189
+ __param(1, (0, common_1.Res)()),
190
+ __metadata("design:type", Function),
191
+ __metadata("design:paramtypes", [String, Object]),
192
+ __metadata("design:returntype", Promise)
193
+ ], AuthController.prototype, "verifyEmailGet", null);
172
194
  __decorate([
173
195
  (0, common_1.Post)('resend-verification'),
174
196
  __param(0, (0, common_1.Body)()),
@@ -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);
@@ -26,8 +26,12 @@ export declare class AuthService {
26
26
  data: any;
27
27
  }>;
28
28
  register(dto: RegisterDto): Promise<{
29
+ emailError: string;
30
+ emailHint: string;
31
+ ok: boolean;
29
32
  id: any;
30
33
  email: string;
34
+ emailSent: boolean;
31
35
  }>;
32
36
  verifyEmail(token: string): Promise<{
33
37
  ok: boolean;
@@ -36,6 +40,23 @@ export declare class AuthService {
36
40
  resendVerification(email: string): Promise<{
37
41
  ok: boolean;
38
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;
39
60
  }>;
40
61
  login(dto: LoginDto): Promise<{
41
62
  accessToken: string;
@@ -48,6 +69,23 @@ export declare class AuthService {
48
69
  forgotPassword(email: string): Promise<{
49
70
  ok: boolean;
50
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;
51
89
  }>;
52
90
  resetPassword(token: string, newPassword: string): Promise<{
53
91
  ok: boolean;
@@ -185,15 +185,25 @@ let AuthService = class AuthService {
185
185
  passwordChangedAt: new Date()
186
186
  });
187
187
  // Send verification email (don't let email failures crash registration)
188
+ let emailSent = true;
189
+ let emailError;
188
190
  try {
189
191
  const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' });
190
192
  await this.mail.sendVerificationEmail(user.email, emailToken);
191
193
  }
192
194
  catch (error) {
195
+ emailSent = false;
196
+ emailError = error.message || 'Failed to send verification email';
193
197
  this.logger.error(`Failed to send verification email: ${error.message}`, error.stack, 'AuthService');
194
198
  // Continue - user is created, they can resend verification
195
199
  }
196
- return { id: user._id, email: user.email };
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
+ };
197
207
  }
198
208
  catch (error) {
199
209
  // Re-throw HTTP exceptions
@@ -247,13 +257,29 @@ let AuthService = class AuthService {
247
257
  return { ok: true, message: 'If the email exists and is unverified, a verification email has been sent' };
248
258
  }
249
259
  const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' });
250
- await this.mail.sendVerificationEmail(user.email, emailToken);
251
- return { ok: true, message: 'Verification email sent successfully' };
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
+ }
252
274
  }
253
275
  catch (error) {
254
276
  this.logger.error(`Resend verification failed: ${error.message}`, error.stack, 'AuthService');
255
- // Return success to prevent email enumeration
256
- return { ok: true, message: 'If the email exists and is unverified, a verification email has been sent' };
277
+ // Return error details for debugging
278
+ return {
279
+ ok: false,
280
+ message: 'Failed to resend verification email',
281
+ error: error.message
282
+ };
257
283
  }
258
284
  }
259
285
  async login(dto) {
@@ -328,17 +354,36 @@ let AuthService = class AuthService {
328
354
  async forgotPassword(email) {
329
355
  try {
330
356
  const user = await this.users.findByEmail(email);
331
- // Always return success to prevent email enumeration
357
+ // Always return success to prevent email enumeration (in production)
332
358
  if (!user) {
333
359
  return { ok: true, message: 'If the email exists, a password reset link has been sent' };
334
360
  }
335
361
  const resetToken = this.signResetToken({ sub: user._id.toString(), purpose: 'reset' });
336
- await this.mail.sendPasswordResetEmail(user.email, resetToken);
337
- return { ok: true, message: 'Password reset link sent successfully' };
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
+ }
338
380
  }
339
381
  catch (error) {
340
382
  this.logger.error(`Forgot password failed: ${error.message}`, error.stack, 'AuthService');
341
- // Return success to prevent email enumeration
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
+ }
342
387
  return { ok: true, message: 'If the email exists, a password reset link has been sent' };
343
388
  }
344
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,19 +19,63 @@ const logger_service_1 = require("./logger.service");
19
19
  let MailService = class MailService {
20
20
  constructor(logger) {
21
21
  this.logger = logger;
22
- this.transporter = nodemailer_1.default.createTransport({
23
- host: process.env.SMTP_HOST,
24
- port: parseInt(process.env.SMTP_PORT, 10),
25
- secure: process.env.SMTP_SECURE === 'true',
26
- auth: {
27
- user: process.env.SMTP_USER,
28
- pass: process.env.SMTP_PASS
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
+ var _a;
68
+ if (!this.smtpConfigured) {
69
+ const error = new common_1.InternalServerErrorException('SMTP not configured - cannot send emails');
70
+ this.logger.error('Attempted to send email but SMTP is not configured', '', 'MailService');
71
+ throw error;
72
+ }
33
73
  try {
34
- const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`;
74
+ // Option 1: Link to frontend (frontend must call POST /api/auth/verify-email)
75
+ // const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`;
76
+ // Option 2: Link directly to backend API (backend verifies and redirects)
77
+ const backendUrl = process.env.BACKEND_URL || ((_a = process.env.FRONTEND_URL) === null || _a === void 0 ? void 0 : _a.replace(/:\d+$/, ':3000')) || 'http://localhost:3000';
78
+ const url = `${backendUrl}/api/auth/verify-email/${token}`;
35
79
  await this.transporter.sendMail({
36
80
  from: process.env.FROM_EMAIL,
37
81
  to: email,
@@ -42,11 +86,17 @@ let MailService = class MailService {
42
86
  this.logger.log(`Verification email sent to ${email}`, 'MailService');
43
87
  }
44
88
  catch (error) {
45
- this.logger.error(`Failed to send verification email to ${email}: ${error.message}`, error.stack, 'MailService');
46
- throw error; // Re-throw so caller can handle
89
+ const detailedError = this.getDetailedSmtpError(error);
90
+ this.logger.error(`Failed to send verification email to ${email}: ${detailedError}`, error.stack, 'MailService');
91
+ throw new common_1.InternalServerErrorException(detailedError);
47
92
  }
48
93
  }
49
94
  async sendPasswordResetEmail(email, token) {
95
+ if (!this.smtpConfigured) {
96
+ const error = new common_1.InternalServerErrorException('SMTP not configured - cannot send emails');
97
+ this.logger.error('Attempted to send email but SMTP is not configured', '', 'MailService');
98
+ throw error;
99
+ }
50
100
  try {
51
101
  const url = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
52
102
  await this.transporter.sendMail({
@@ -59,9 +109,28 @@ let MailService = class MailService {
59
109
  this.logger.log(`Password reset email sent to ${email}`, 'MailService');
60
110
  }
61
111
  catch (error) {
62
- this.logger.error(`Failed to send password reset email to ${email}: ${error.message}`, error.stack, 'MailService');
63
- throw error; // Re-throw so caller can handle
112
+ const detailedError = this.getDetailedSmtpError(error);
113
+ this.logger.error(`Failed to send password reset email to ${email}: ${detailedError}`, error.stack, 'MailService');
114
+ throw new common_1.InternalServerErrorException(detailedError);
115
+ }
116
+ }
117
+ getDetailedSmtpError(error) {
118
+ if (error.code === 'EAUTH') {
119
+ return 'SMTP authentication failed. Check SMTP_USER and SMTP_PASS environment variables.';
120
+ }
121
+ if (error.code === 'ESOCKET' || error.code === 'ECONNECTION') {
122
+ return `Cannot connect to SMTP server at ${process.env.SMTP_HOST}:${process.env.SMTP_PORT}. Check network/firewall settings.`;
123
+ }
124
+ if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
125
+ return 'SMTP connection timed out. Server may be unreachable or firewalled.';
126
+ }
127
+ if (error.responseCode >= 500) {
128
+ return `SMTP server error (${error.responseCode}): ${error.response}`;
129
+ }
130
+ if (error.responseCode >= 400) {
131
+ return `SMTP client error (${error.responseCode}): Check FROM_EMAIL and recipient addresses.`;
64
132
  }
133
+ return error.message || 'Unknown SMTP error';
65
134
  }
66
135
  };
67
136
  exports.MailService = MailService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciscode/authentication-kit",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "NestJS auth kit with local + OAuth, JWT, RBAC, password reset.",
5
5
  "publishConfig": {
6
6
  "access": "public"