Skip to content

Commit 7e7449b

Browse files
feat: replaced SendGrid integration with Mail module for emai registration handling instead
1 parent 3ca517d commit 7e7449b

File tree

7 files changed

+187
-59
lines changed

7 files changed

+187
-59
lines changed

apps/api/src/app.module.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { SkillsModule } from './skills/skills.module';
1010
import { CacheModule } from '@nestjs/cache-manager';
1111
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
1212
import { APP_GUARD } from '@nestjs/core';
13-
import { SendGridModule } from './sendgrid/sendgrid.module';
13+
import { MailModule } from './mail/mail.module';
1414
import { ChatGateway } from './chat/chat.gateway';
1515
import { ChatModule } from './chat/chat.module';
1616
import { RoomsModule } from './chat/rooms.module';
@@ -36,15 +36,17 @@ import firebaseConfig from './config/firebase.config';
3636
}),
3737

3838
// ThrottlerModule global configuration
39-
ThrottlerModule.forRoot([{
40-
ttl: 60 * 1000, // Time window in milliseconds (60 seconds)
41-
limit: 10, // Max number of requests within the time window
42-
}]),
39+
ThrottlerModule.forRoot([
40+
{
41+
ttl: 60 * 1000, // Time window in milliseconds (60 seconds)
42+
limit: 10, // Max number of requests within the time window
43+
},
44+
]),
4345

4446
AuthModule,
4547
UsersModule,
4648
SkillsModule,
47-
SendGridModule,
49+
MailModule,
4850
ChatModule,
4951
RoomsModule,
5052
FirebaseModule,

apps/api/src/auth/auth.module.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { JwtModule } from '@nestjs/jwt';
77
import { PassportModule } from '@nestjs/passport';
88
import { JwtStrategy } from './jwt.strategy';
99
import { JwtAuthGuard } from './jwt.auth.guard';
10-
import { SendGridModule } from '../sendgrid/sendgrid.module';
10+
import { MailModule } from '../mail/mail.module';
1111
import { RefreshTokenStrategy } from './refresh-token.strategy';
1212

1313
@Module({
@@ -22,15 +22,10 @@ import { RefreshTokenStrategy } from './refresh-token.strategy';
2222
signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN') },
2323
}),
2424
}),
25-
SendGridModule
25+
MailModule,
2626
],
2727
controllers: [AuthController],
28-
providers: [
29-
AuthService,
30-
JwtStrategy,
31-
JwtAuthGuard,
32-
RefreshTokenStrategy
33-
],
28+
providers: [AuthService, JwtStrategy, JwtAuthGuard, RefreshTokenStrategy],
3429
exports: [AuthService, JwtAuthGuard, JwtModule],
3530
})
36-
export class AuthModule {}
31+
export class AuthModule {}

apps/api/src/auth/auth.service.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,24 @@ import { JwtService } from '@nestjs/jwt';
99
import * as bcrypt from 'bcrypt';
1010
import { UsersService } from '../users/users.service';
1111
import { ConfigService } from '@nestjs/config';
12-
import { SendGridService } from '../sendgrid/sendgrid.service';
12+
import { MailService } from '../mail/mail.service';
1313

1414
@Injectable()
1515
export class AuthService {
1616
constructor(
1717
private readonly usersService: UsersService,
1818
private readonly jwtService: JwtService,
1919
private readonly configService: ConfigService,
20-
private readonly sendGridService: SendGridService
20+
private readonly mailService: MailService
2121
) {}
2222

2323
async validateUser(email: string, password: string): Promise<any> {
2424
const user = await this.usersService.findByEmail(email);
2525
if (user && (await bcrypt.compare(password, user.password))) {
2626
if (!user.isEmailVerified) {
27-
throw new UnauthorizedException('Please verify your email before logging in.');
27+
throw new UnauthorizedException(
28+
'Please verify your email before logging in.'
29+
);
2830
}
2931
// User verified, continue login
3032
return user;
@@ -35,23 +37,28 @@ export class AuthService {
3537

3638
async login(user: any) {
3739
const foundUser = await this.usersService.findByEmail(user.email);
38-
40+
3941
if (!foundUser) {
4042
throw new UnauthorizedException('Invalid credentials');
4143
}
42-
43-
const passwordMatches = await bcrypt.compare(user.password, foundUser.password);
44+
45+
const passwordMatches = await bcrypt.compare(
46+
user.password,
47+
foundUser.password
48+
);
4449
if (!passwordMatches) {
4550
throw new UnauthorizedException('Invalid credentials');
4651
}
47-
52+
4853
if (!foundUser.isEmailVerified) {
49-
throw new UnauthorizedException('Email not verified. Please check your email to verify your account.');
54+
throw new UnauthorizedException(
55+
'Email not verified. Please check your email to verify your account.'
56+
);
5057
}
51-
58+
5259
// Set user as online
5360
await this.usersService.update(foundUser.id, { isOnline: true });
54-
61+
5562
const payload = {
5663
email: foundUser.email,
5764
sub: foundUser.id,
@@ -62,7 +69,7 @@ export class AuthService {
6269
userId: foundUser.id,
6370
};
6471
}
65-
72+
6673
async logout(userId: string): Promise<void> {
6774
const user = await this.usersService.findOne(userId);
6875
if (!user) {
@@ -71,8 +78,7 @@ export class AuthService {
7178
user.isOnline = false;
7279
await this.usersService.update(userId, { isOnline: false });
7380
}
74-
75-
81+
7682
async register(user: any) {
7783
const hashedPassword = await bcrypt.hash(user.password, 10);
7884
await this.usersService.create({
@@ -98,37 +104,39 @@ export class AuthService {
98104

99105
const resetLink = `${this.configService.get<string>('FRONTEND_URL')}/reset-password?token=${resetToken}`;
100106

101-
await this.sendGridService.sendPasswordResetEmail(user.email, resetLink);
107+
await this.mailService.sendPasswordResetEmail(user.email, resetLink);
102108
}
103109

104110
async resetPassword(token: string, newPassword: string): Promise<void> {
105111
try {
106112
const payload = this.jwtService.verify(token, {
107113
secret: this.configService.get<string>('RESET_PASSWORD_SECRET'),
108114
});
109-
115+
110116
const user = await this.usersService.findByEmail(payload.email);
111-
117+
112118
if (!user) {
113119
throw new NotFoundException('User not found');
114120
}
115121

116-
117-
118-
await this.usersService.update(user.id, { password: newPassword });
119-
122+
await this.usersService.update(user.id, { password: newPassword });
123+
120124
const updatedUser = await this.usersService.findByEmail(payload.email);
121-
122-
const passwordMatches = await bcrypt.compare(newPassword, updatedUser.password);
125+
126+
const passwordMatches = await bcrypt.compare(
127+
newPassword,
128+
updatedUser.password
129+
);
123130
if (!passwordMatches) {
124-
throw new InternalServerErrorException('Password update verification failed');
131+
throw new InternalServerErrorException(
132+
'Password update verification failed'
133+
);
125134
}
126-
127135
} catch (error) {
128136
console.error('Error during password reset:', error.message);
129137
throw new BadRequestException('Invalid or expired token');
130138
}
131-
}
139+
}
132140

133141
async generateTokens(userId: string, email: string) {
134142
const [accessToken, refreshToken] = await Promise.all([
@@ -137,14 +145,14 @@ export class AuthService {
137145
{
138146
secret: this.configService.get<string>('JWT_SECRET'),
139147
expiresIn: this.configService.get<string>('JWT_EXPIRES_IN'),
140-
},
148+
}
141149
),
142150
this.jwtService.signAsync(
143151
{ sub: userId, email },
144152
{
145153
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
146154
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN'),
147-
},
155+
}
148156
),
149157
]);
150158

@@ -161,7 +169,7 @@ export class AuthService {
161169
await this.jwtService.verifyAsync(refreshToken, {
162170
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
163171
});
164-
172+
165173
return this.generateTokens(userId, refreshToken);
166174
} catch {
167175
throw new UnauthorizedException('Invalid refresh token');

apps/api/src/mail/mail.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { MailService } from './mail.service';
4+
5+
@Module({
6+
imports: [ConfigModule],
7+
providers: [MailService],
8+
exports: [MailService],
9+
})
10+
export class MailModule {}

apps/api/src/mail/mail.service.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import * as nodemailer from 'nodemailer';
4+
5+
@Injectable()
6+
export class MailService {
7+
private transporter: nodemailer.Transporter;
8+
9+
constructor(private readonly configService: ConfigService) {
10+
const host =
11+
this.configService.get<string>('SMTP_HOST') || 'smtp.gmail.com';
12+
const portEnv = this.configService.get<string>('SMTP_PORT');
13+
const port = portEnv ? Number(portEnv) : 465;
14+
const secureEnv = this.configService.get<string>('SMTP_SECURE');
15+
const secure =
16+
typeof secureEnv === 'string' ? secureEnv === 'true' : port === 465;
17+
18+
const authUser = this.configService.get<string>('SMTP_USER');
19+
const authPass = this.configService.get<string>('SMTP_PASS');
20+
21+
const transportOptions: any = {
22+
host,
23+
port,
24+
secure,
25+
auth: {
26+
user: authUser,
27+
pass: authPass,
28+
},
29+
};
30+
31+
// For STARTTLS (port 587) ensure TLS upgrade is required
32+
if (!secure) {
33+
transportOptions.requireTLS = true;
34+
// Require modern TLS version
35+
transportOptions.tls = { minVersion: 'TLSv1.2' };
36+
}
37+
38+
console.log('Creating SMTP transporter', {
39+
host,
40+
port,
41+
secure,
42+
user: !!authUser,
43+
});
44+
this.transporter = nodemailer.createTransport(transportOptions);
45+
46+
// Verify connection configuration early to log helpful errors
47+
this.transporter.verify((err, success) => {
48+
if (err) {
49+
console.error('SMTP connection verify failed:', err);
50+
} else {
51+
console.log('SMTP connection verified');
52+
}
53+
});
54+
}
55+
56+
async sendVerificationEmail(
57+
to: string,
58+
verificationLink: string
59+
): Promise<void> {
60+
const msg = {
61+
from:
62+
this.configService.get<string>('EMAIL_FROM') ||
63+
this.configService.get<string>('SMTP_USER'),
64+
to,
65+
subject: 'Email Verification',
66+
html: `
67+
<p>Thank you for registering. Please verify your email by clicking the link below:</p>
68+
<a href="${verificationLink}">Verify Email</a>
69+
<p>If you did not request this, please ignore this email.</p>
70+
`,
71+
};
72+
73+
try {
74+
const info = await this.transporter.sendMail(msg as any);
75+
console.log(`Verification email sent to ${to}: ${info.messageId}`);
76+
} catch (error: any) {
77+
console.error('Error sending verification email:', error);
78+
throw new Error('Failed to send verification email');
79+
}
80+
}
81+
82+
async sendPasswordResetEmail(to: string, resetLink: string): Promise<void> {
83+
const msg = {
84+
from:
85+
this.configService.get<string>('EMAIL_FROM') ||
86+
this.configService.get<string>('SMTP_USER'),
87+
to,
88+
subject: 'Password Reset Request',
89+
html: `
90+
<p>You requested a password reset. Click the link below to reset your password:</p>
91+
<a href="${resetLink}">Reset Password</a>
92+
<p>If you did not request this, please ignore this email.</p>
93+
`,
94+
};
95+
96+
try {
97+
const info = await this.transporter.sendMail(msg as any);
98+
console.log(`Password reset email sent to ${to}: ${info.messageId}`);
99+
} catch (error: any) {
100+
console.error('Error sending password reset email:', error);
101+
throw new Error('Failed to send password reset email');
102+
}
103+
}
104+
}

apps/api/src/users/users.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import { User } from './entities/user.entity';
66
import { Skill } from '../skills/entities/skill.entity';
77
import { CloudinaryModule } from '../cloudinary/cloudinary.module';
88
import { ConfigModule } from '@nestjs/config';
9-
import { SendGridModule } from '@/sendgrid/sendgrid.module';
9+
import { MailModule } from '@/mail/mail.module';
1010

1111
@Module({
1212
imports: [
1313
TypeOrmModule.forFeature([User, Skill]),
1414
CloudinaryModule,
1515
ConfigModule,
16-
SendGridModule, // Add SendGridModule
16+
MailModule,
1717
],
1818
providers: [UsersService],
1919
controllers: [UsersController],

0 commit comments

Comments
 (0)