8.10-bob: Email (Nodemailer), guard va tokenlar amaliyoti
8-QISM — NestJS (chuqur) · 10-mavzu
1. Kirish va motivatsiya
Auth'ni 8.9-bob bildik. Endi uni amaliy to'ldiramiz: NestJS'da email (Nodemailer — 5.19) va auth jarayonining muhim qismlarini — email tasdiqlash va parolni tiklash — guard/token bilan birlashtiramiz. Bu — har real auth tizimining ajralmas qismi: ro'yxatdan o'tgach email tasdiqlash, parolni unutganda tiklash havolasi. 5.19'da Nodemailer'ni Express'da o'rgandik; endi NestJS'da — MailerModule, Handlebars shablon, va auth bilan integratsiyalashgan holda.
NestJS'da email — @nestjs-modules/mailer (Nodemailer ustida) bilan: MailerModule.forRootAsync (SMTP sozlash — env), MailerService (DI bilan inject), va Handlebars (HTML shablon — 5.19: 2.6). Bu — 5.19'ning NestJS versiyasi (DI, shablon, toza). Email — alohida MailModule (separation of concerns — 8.1).
Bu bob auth'ni (8.9) to'liq qiladi: email tasdiqlash (signup'dan keyin tasdiqlash tokeni — email), parol tiklash (token + havola — 5.19: 2.13), va bularni guard/token bilan amaliyot. Va og'ir email'ni fonda (navbat — 8.22). Bu bob: MailerModule, Handlebars, email tasdiqlash, parol tiklash, va xavfsizlik — chuqur. Bu — auth tizimini production-tayyor qiladi.
O'xshatish: email tasdiqlash — telefon raqamini SMS bilan tasdiqlash 5.18-bob ning email versiyasi. Ro'yxatdan o'tasiz emailingizga tasdiqlash havolasi keladi bosasiz "haqiqatan siz, email sizniki" (egalik isboti). Parol tiklash — "kalit yo'qotdim" emailingizga vaqtinchalik tiklash havolasi (qisqa muddat, bir martalik) yangi parol o'rnatasiz. Email — ishonchli kanal (auth'ning to'ldiruvchisi).
Nega muhim?
- Auth to'ldiruvchisi — email tasdiqlash, parol tiklash — har auth'da.
- Email kanali — tasdiqlash, bildirishnoma 5.19-bob.
- MailerModule — NestJS email integratsiyasi (DI, shablon).
- Production auth — to'liq tizim (8.9 + email).
2. Nazariya — chuqur tushuntirish
2.1. NestJS email — MailerModule
npm install @nestjs-modules/mailer nodemailer handlebars// mail.module.ts (5.19 — NestJS versiyasi)
import { MailerModule } from "@nestjs-modules/mailer";
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter";
@Module({
imports: [
MailerModule.forRootAsync({ // async (ConfigService — 8.3: 2.4)
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
transport: { // SMTP (5.19: 2.3)
host: config.get("MAIL_HOST"),
port: 587,
auth: { user: config.get("MAIL_USER"), pass: config.get("MAIL_PASS") }, // (14)
},
defaults: { from: `"Mana" <${config.get("MAIL_FROM")}>` }, // kimdan (5.19: 2.5)
template: { // Handlebars (5.19: 2.6)
dir: join(__dirname, "templates"),
adapter: new HandlebarsAdapter(),
options: { strict: true },
},
}),
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}MailerModule — Nodemailer 5.19-bob ustida NestJS modul.
forRootAsync(SMTP env'dan — 14),transport(SMTP — 5.19: 2.3),defaults.from,template(Handlebars — HTML shablon — 5.19: 2.6). AlohidaMailModule(separation — 8.1).MailService(DI — 2.2).
2.2. MailService (DI bilan)
import { MailerService } from "@nestjs-modules/mailer";
@Injectable()
export class MailService {
constructor(private mailerService: MailerService) {} // DI (8.2)
async tasdiqlashYubor(email: string, token: string) {
const url = `${this.config.get("CLIENT_URL")}/verify?token=${token}`;
await this.mailerService.sendMail({
to: email,
subject: "Email manzilingizni tasdiqlang",
template: "verification", // verification.hbs (2.3)
context: { url, email }, // shablon o'zgaruvchilari
});
}
}MailService —
MailerService(DI) ustida.sendMail(5.19: 2.5): to/subject/template (Handlebars fayl) + context (shablon o'zgaruvchilari). Maxsus metodlar (tasdiqlash, parol tiklash, xush kelibsiz). Service'da (controller emas — 8.1). Bu — 5.19'ning NestJS amaliyoti.
2.3. Handlebars shablon (HTML email — 5.19: 2.6)
<!-- templates/verification.hbs -->
<div style="font-family: Arial; max-width: 600px; margin: 0 auto;">
<h1 style="color: #4f46e5;">Mana Do'kon</h1>
<p>Salom! Email manzilingizni tasdiqlang:</p>
<a href="{{ url }}" style="background: #4f46e5; color: #fff; padding: 12px 24px;
border-radius: 6px; text-decoration: none;">Tasdiqlash</a>
<p>Havola 24 soat amal qiladi.</p>
</div>Handlebars shablon (
.hbs) — HTML email (inline style — 5.19: 2.6).{{ url }},{{ email }}— context'dan o'zgaruvchilar. Alohida fayl (kod toza). MailerModuledirda topadi. Build'datemplates/nidist/ga ko'chirish kerak (nest-cli.json assets).
Shablon adapterlari (Handlebars muqobillari). @nestjs-modules/mailer faqat Handlebars'ga bog'lanmagan — u adapter naqshi 8.1-bob orqali bir necha shablon mexanizmini qo'llab-quvvatlaydi. Loyihangizga qulayini tanlaysiz:
// Handlebars (eng keng tarqalgan — logikasiz, xavfsiz)
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter";
adapter: new HandlebarsAdapter(),
// Pug (chuqurlik-asosli sintaksis — qisqa)
import { PugAdapter } from "@nestjs-modules/mailer/dist/adapters/pug.adapter";
adapter: new PugAdapter(), // template: "verification.pug"
// EJS (oddiy JS ichma-ich — <%= url %>)
import { EjsAdapter } from "@nestjs-modules/mailer/dist/adapters/ejs.adapter";
adapter: new EjsAdapter({ inlineCssEnabled: true }), // CSS'ni inline'ga aylantirishAdapter tanlash. Handlebars — logikasiz (
{{}}faqat qiymat, minimal shart), email uchun eng xavfsiz va ommaviy tanlov (biz shuni ishlatamiz). Pug — chuqurlik bilan yozish (kam belgi), lekin HTML'ni yashiradi. EJS — to'liq JS (<%= %>), moslashuvchan,inlineCssEnabledbilan<style>bloklarini har elementga inline qiladi (email mijozlari<style>ni ko'pincha o'chiradi — inline CSS ishonchliroq). Amaliyotda Handlebars + inline style yetarli. Muhimi — adapter interfeysi bir xil (DI o'zgarmaydi), faqat.hbs/.pug/.ejskengaytmasi va sintaksis farq qiladi.
2.4. Email tasdiqlash oqimi (token bilan)
1. Signup 8.9-bob user yaratiladi (emailTasdiqlangan: false)
2. Tasdiqlash TOKENI yaratiladi (crypto — 5.3, yoki JWT qisqa muddat)
3. Token DB'ga (yoki JWT — 2.5) + email yuboriladi (havola)
4. Foydalanuvchi havolani bosadi GET /auth/verify?token=...
5. Token tekshiriladi emailTasdiqlangan: true
6. Token bekor (bir martalik — 5.18 ruhida)Email tasdiqlash (5.19: 2.13) — token + havola. Token: crypto (DB'da — 5.3) yoki JWT (qisqa muddat — 2.5). Foydalanuvchi havolani bosadi tasdiqlanadi. Tasdiqlanmagan user'larni cheklash (guard — 2.7). Bu — auth'ning muhim qismi.
2.5. Tasdiqlash tokeni (JWT yoki crypto)
// Variant 1: JWT (qisqa muddat — saqlash kerak emas)
const token = await this.jwtService.signAsync(
{ sub: user.id, type: "verify" },
{ secret: config.get("VERIFY_SECRET"), expiresIn: "24h" },
);
// Variant 2: crypto (DB'da — bir martalik, bekor qilinadi — 5.3)
const token = crypto.randomBytes(32).toString("hex");
await this.usersService.tasdiqTokenSaqla(user.id, token, expiresAt);Token turi: JWT (qisqa muddat —
type: "verify"— saqlash kerak emas, lekin bekor qilib bo'lmaydi — 5.16: 2.6). crypto (DB'da — bir martalik, bekor qilinadi — xavfsizroq, lekin DB). Tasdiqlash uchun JWT yetadi (qisqa muddat); parol tiklash uchun crypto+DB (bir martalik — 2.8) afzal.
2.5a. Token muddati, DB'da saqlash va xeshlash
Crypto tokenni DB'da saqlaganda uchta narsa muhim: muddat (expiresAt), xeshlash (ochiq saqlama) va bir martalik (ishlatilgach tozala). Foydalanuvchi jadvaliga (yoki alohida tokens jadvaliga) qo'shiladigan maydonlar:
// user.entity.ts (TypeORM — 8.4) yoki Prisma model (8.4)
@Entity()
export class User {
@Column({ default: false })
emailTasdiqlangan: boolean; // tasdiqlash holati (2.4)
// Parol tiklash tokeni — XESHLANGAN (ochiq emas — 14)
@Column({ type: "varchar", nullable: true })
resetToken: string | null; // bcrypt hash saqlanadi
@Column({ type: "timestamp", nullable: true })
resetExpires: Date | null; // muddat (masalan +1 soat)
// Email tasdiqlash tokeni — agar crypto ishlatsangiz (JWT'da shart emas)
@Column({ type: "varchar", nullable: true })
verifyToken: string | null;
@Column({ type: "timestamp", nullable: true })
verifyExpires: Date | null;
// Qayta yuborish cooldown belgisi (2.9a)
@Column({ type: "timestamp", nullable: true })
oxirgiTasdiqYuborilgan: Date | null;
}// UsersService — token saqlash/tekshirish/tozalash metodlari
async resetTokenSaqla(id: number, hash: string, expiresAt: Date) {
await this.repo.update(id, { resetToken: hash, resetExpires: expiresAt });
}
async resetTokenBilan(id: number) {
return this.repo.findOne({ where: { id } }); // token+muddat bilan qaytadi
}
async resetTokenTozala(id: number) { // bir martalik — ishlatilgach null
await this.repo.update(id, { resetToken: null, resetExpires: null });
}Uchta qoida:
- Muddat (expiry) — token
expiresAtbilan saqlanadi; tekshirishdaresetExpires < new Date()bo'lsa — rad et. Tasdiqlash odatda 24 soat, parol tiklash 1 soat (qisqaroq — xavfliroq amal).- Xeshlash — tokenni DB'da ochiq saqlama (
bcrypt.hash(token, 10)). DB sizib chiqsa (SQL injection, zaxira o'g'irlik), hujumchi ochiq tokenni ishlatib akkauntni egallaydi. Xeshlangan bo'lsa — foydasiz. Foydalanuvchiga ochiq token yuboriladi (email/URL'da), DB'da esa uning xeshi turadi — parol saqlash mantiqi bilan bir xil 5.15-bob.- Bir martalik — token ishlatilgach (
resetTokenTozala) yoki muddati tugagach yaroqsiz. Bu — takroriy ishlatishni (replay) va o'g'irlangan eski havolani to'sadi (5.18 ruhida).JWT'da farq: JWT stateless —
expiresInichida muddat, DB kerak emas, lekin bir martalik emas (24 soat ichida bir necha marta ishlaydi) va bekor qilib bo'lmaydi (5.16: 2.6). Shu bois muhim amal (parol tiklash) uchun crypto+DB, oddiy amal (email tasdiqlash) uchun JWT afzal.
2.6. Email tasdiqlash service va controller
// AuthService
async signup(dto: SignupDto) {
const user = await this.usersService.yarat({ ...dto, parol: hash, emailTasdiqlangan: false });
const token = await this.jwtService.signAsync({ sub: user.id, type: "verify" },
{ secret: this.config.get("VERIFY_SECRET"), expiresIn: "24h" });
await this.mailService.tasdiqlashYubor(user.email, token); // email (2.2)
return { message: "Tasdiqlash emaili yuborildi" };
}
async emailTasdiqla(token: string) {
try {
const payload = await this.jwtService.verifyAsync(token, { secret: this.config.get("VERIFY_SECRET") });
if (payload.type !== "verify") throw new Error();
await this.usersService.emailTasdiqla(payload.sub); // emailTasdiqlangan: true
return { message: "Email tasdiqlandi" };
} catch {
throw new BadRequestException("Havola yaroqsiz yoki muddati tugagan");
}
}Email tasdiqlash service: signup (token + email), emailTasdiqla (token verify tasdiqlash).
type: "verify"— tokenni ajratish (access token bilan aralashmasin). Controller@Public()(token URL'da — 8.9: 2.12).
2.7. EmailVerified guard (tasdiqlanmaganni cheklash)
// Tasdiqlangan email talab qiluvchi guard (8.6)
@Injectable()
export class EmailVerifiedGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const { user } = context.switchToHttp().getRequest();
const dbUser = await this.usersService.bitta(user.id);
if (!dbUser.emailTasdiqlangan) {
throw new ForbiddenException("Iltimos, avval emailingizni tasdiqlang"); // 403
}
return true;
}
}
// @UseGuards(JwtAuthGuard, EmailVerifiedGuard) — tasdiqlangan user kerakEmailVerifiedGuard 8.6-bob — tasdiqlanmagan foydalanuvchini muayyan amallardan cheklaydi (buyurtma berish — tasdiqlangan email kerak). JwtAuthGuard'dan keyin. Yoki tokenda
emailTasdiqlangan(DB so'rovsiz). Auth + email integratsiyasi.
2.8. Parol tiklash oqimi (5.19: 2.13)
1. POST /auth/forgot-password { email } token (crypto — bir martalik)
2. Token HASH qilib DB'ga + email (tiklash havolasi) — oshkor qilma! (14)
3. Foydalanuvchi havolani bosadi POST /auth/reset-password { token, yangiParol }
4. Token tekshir (DB hash, muddat) yangi parol (bcrypt) token bekor
5. Barcha refresh tokenlarni bekor (xavfsizlik — 5.16: 2.6)Parol tiklash (5.19: 2.13, 5.15: Misol 5) — token + havola. Foydalanuvchi bor/yo'qligini oshkor qilma (14 — har doim "yuborildi"). Token: crypto + DB hash (bir martalik, qisqa muddat — 5.18 ruhida). Tiklangach — barcha refresh bekor (5.16: 2.6 — xavfsizlik).
2.9. Parol tiklash service
async parolTiklashSorov(email: string) {
const user = await this.usersService.emailBilan(email);
if (user) { // bor bo'lsa (oshkor qilma — 14)
const token = crypto.randomBytes(32).toString("hex"); // (5.3)
await this.usersService.resetTokenSaqla(user.id, await bcrypt.hash(token, 10), // hash (14)
new Date(Date.now() + 3600000)); // 1 soat
const url = `${this.config.get("CLIENT_URL")}/reset?token=${token}&id=${user.id}`;
await this.mailService.parolTiklashYubor(email, url); // email (2.2)
}
return { message: "Agar email mavjud bo'lsa, havola yuborildi" }; // doim bir xil (14)
}
async parolTiklash(userId: number, token: string, yangiParol: string) {
const user = await this.usersService.resetTokenBilan(userId);
if (!user?.resetToken || user.resetExpires < new Date() ||
!(await bcrypt.compare(token, user.resetToken))) {
throw new BadRequestException("Havola yaroqsiz yoki muddati tugagan");
}
await this.usersService.parolYangila(userId, await bcrypt.hash(yangiParol, 12)); // (5.15)
await this.usersService.resetTokenTozala(userId); // bir martalik
await this.usersService.refreshBekor(userId); // barcha refresh bekor (5.16: 2.6, 14)
return { message: "Parol tiklandi" };
}Parol tiklash (5.15: Misol 5): token crypto + DB hash (14), oshkor qilmaslik (doim "yuborildi"), tiklangach yangi parol + token bekor + barcha refresh bekor (5.16: 2.6 — boshqa qurilmalar chiqsin). Xavfsiz amaliyot.
2.9a. Tasdiqlash emailini qayta yuborish (resend + cooldown)
Foydalanuvchi tasdiqlash emailini ololmasa (spam'ga tushdi, o'chirdi) — qayta yuborish (resend) kerak. Ammo bu ikki xavfni ochadi: (1) spam — kimdir tugmani ko'p bosib emailingizni bombardimon qiladi; (2) SMTP xarajati. Shuning uchun cooldown (ketma-ket yuborishlar orasida majburiy tanaffus) va rate limit 8.16-bob qo'yiladi.
// AuthService — Misol 5 dagi controller chaqiradigan metod
async tasdiqlashQaytaYubor(userId: number) {
const user = await this.usersService.bitta(userId);
if (user.emailTasdiqlangan) { // allaqachon tasdiqlangan
throw new BadRequestException("Email allaqachon tasdiqlangan");
}
// COOLDOWN — oxirgi yuborilishdan 60 soniya o'tmagan bo'lsa — rad et
const COOLDOWN_MS = 60 * 1000;
if (user.oxirgiTasdiqYuborilgan &&
Date.now() - user.oxirgiTasdiqYuborilgan.getTime() < COOLDOWN_MS) {
const qoldi = Math.ceil(
(COOLDOWN_MS - (Date.now() - user.oxirgiTasdiqYuborilgan.getTime())) / 1000,
);
throw new HttpException( // 429 Too Many Requests
`Iltimos, ${qoldi} soniyadan keyin qayta urinib ko'ring`,
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Yangi token + email (fonda — 2.10)
const token = await this.jwtService.signAsync(
{ sub: user.id, type: "verify" },
{ secret: this.config.get("VERIFY_SECRET"), expiresIn: "24h" },
);
await this.usersService.oxirgiTasdiqYangila(user.id, new Date()); // cooldown belgisi
await this.emailQueue.add("verification", { email: user.email, token });
return { message: "Tasdiqlash emaili qayta yuborildi" };
}Ikki qatlamli himoya:
- Cooldown (foydalanuvchi darajasida) — DB'da
oxirgiTasdiqYuborilganvaqtini saqlab, keyingi yuborishni 60 soniyaga to'sadi. Bu — aynan bir foydalanuvchining spam qilishini bloklaydi (429 status + qolgan soniya). Muddat maydoni@Column({ type: "timestamp", nullable: true }) oxirgiTasdiqYuborilgan(2.5a).- Rate limit (IP/route darajasida) — controller'da
@Throttle8.16-bob so'rovlar sonini cheklaydi (masalan 1 daqiqada 3 marta). Bu — cooldownni chetlab o'tishga urinuvchi avtomatlashtirilgan hujumni to'sadi.Ikkalasi birga — foydalanuvchi tajribasi (aniq "N soniyadan keyin") + tizim himoyasi (spam/SMTP xarajat). Xuddi shu naqsh parol tiklash so'roviga ham qo'llanadi (forgot-password ham
@Throttlebilan — Misol 5).
2.10. Og'ir email fonda (navbat — 8.22 kirish)
// Email — fonda (so'rovni bloklamasin — 5.19: 2.9)
@Injectable()
export class AuthService {
constructor(@InjectQueue("email") private emailQueue: Queue) {} // BullMQ (8.22)
async signup(dto: SignupDto) {
const user = await this.usersService.yarat(...);
await this.emailQueue.add("verification", { email: user.email, token }); // navbatga (8.22)
return { message: "..." }; // darrov javob (kutmaydi)
}
}
// Worker 8.22-bob navbatdan oladi email yuboradi (fonda)Og'ir email fonda (5.19: 2.9, 8.22): email yuborish sekin (SMTP) so'rovni bloklamaslik uchun navbatga (BullMQ — 8.22).
@InjectQueue+add. Worker fonda yuboradi. Production'da ko'p email — navbat orqali. 8.22'da chuqur.
2.11. Email use-case'lar (NestJS'da)
Email tasdiqlash — signup'dan keyin 2.4-bob
Parol tiklash — forgot/reset 2.8-bob
Xush kelibsiz — tasdiqlangach
OTP — email OTP (SMS muqobili — 5.18)
Bildirishnoma — buyurtma, hodisa (event-driven — 9)
Davriy — hisobot (cron — 8.22)2.12. Email xavfsizligi (5.19: 2.14 — 14)
SMTP kalitlari .env'da (forRootAsync — 14, 5.8)
Parol tiklash: token crypto + DB hash, qisqa muddat, bir martalik (2.9, 14)
Foydalanuvchi bor/yo'qligini oshkor qilma (parol tiklash — 14)
Tiklangach barcha refresh bekor (5.16: 2.6)
Email validatsiya (DTO — 8.5); rate limiting (spam — 8.16)
Qayta yuborishda cooldown + rate limit (email bombardimon — 2.9a)
Token muddati (expiry) qisqa: tasdiqlash 24s, tiklash 1s (2.5a)
Og'ir email fonda (navbat — 2.10); Mailtrap dev'da (5.19: 2.11)2.13. Guard/token to'liq amaliyot (8.9 bilan)
To'liq auth + email oqimi:
signup email tasdiqlash token email
login (LocalGuard) access + refresh
himoyalangan (JwtGuard) req.user
buyurtma (JwtGuard + EmailVerifiedGuard) tasdiqlangan kerak
forgot/reset email + token
resend-verification (JwtGuard + cooldown + throttle) qayta email (2.9a)
refresh (JwtRefreshGuard) yangi token
admin (JwtGuard + RolesGuard) rol 8.7-bob
Guard'lar + tokenlar + email = to'liq production auth tizimiBu bob 8.9 (auth) + 8.7 (RBAC) + email'ni birlashtiradi: guard'lar (Jwt/Local/Refresh/Roles/EmailVerified), tokenlar (access/refresh/verify/reset), email (tasdiqlash/tiklash). To'liq, production-tayyor auth tizimi. Bu — sizning karyerangizda quradigan asosiy tizim.
2.14. Best practices (14)
MailModule alohida (separation — MailerService DI — 2.1)
Handlebars shablon (HTML — alohida fayl — 2.3)
SMTP kalitlari .env (forRootAsync — 14)
Email tasdiqlash (signup — 2.4); EmailVerifiedGuard (cheklash — 2.7)
Parol tiklash: token hash, oshkor qilma, refresh bekor (2.9, 14)
Og'ir email fonda (navbat — 2.10, 8.22)
Token type ajrat (verify/reset/access — aralashmasin — 2.5)
Email rate limiting (spam — 8.16); Mailtrap dev (5.19)3. Sintaksis — tez ma'lumotnoma
// MailerModule (2.1)
MailerModule.forRootAsync({ useFactory: (c) => ({ transport, defaults, template }) })
// MailService (2.2)
this.mailerService.sendMail({ to, subject, template: "verification", context: { url } });
// Tasdiqlash 2.4-bob: token (JWT/crypto) email verify
// Parol tiklash 2.8-bob: forgot (token+email) reset (token verify+yangi parol)
// EmailVerifiedGuard 2.7-bob: emailTasdiqlangan tekshir
// Fonda 2.10-bob: emailQueue.add("verification", data)
// Token DB (2.5a): resetToken (bcrypt hash) + resetExpires (Date)
// Tekshir: expires < now rad; bcrypt.compare(urlToken, dbHash)
// Resend (2.9a): cooldown (oxirgiTasdiqYuborilgan + 60s) + @Throttle
// Adapter 2.3-bob: HandlebarsAdapter | PugAdapter | EjsAdapter4. Batafsil kod namunalari
Misol 1 — MailModule + MailService (2.1, 2.2)
// mail/mail.module.ts
@Module({
imports: [
MailerModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
transport: {
host: config.get("MAIL_HOST"),
port: config.get("MAIL_PORT") || 587,
secure: false,
auth: { user: config.get("MAIL_USER"), pass: config.get("MAIL_PASS") }, // (14)
},
defaults: { from: `"Mana Do'kon" <${config.get("MAIL_FROM")}>` },
template: {
dir: join(process.cwd(), "templates"),
adapter: new HandlebarsAdapter(),
options: { strict: true },
},
}),
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}
// mail/mail.service.ts
@Injectable()
export class MailService {
constructor(private mailer: MailerService, private config: ConfigService) {}
async tasdiqlashYubor(email: string, token: string) {
const url = `${this.config.get("CLIENT_URL")}/verify-email?token=${token}`;
await this.mailer.sendMail({
to: email, subject: "Email tasdiqlash", template: "verification",
context: { url }, // shablon o'zgaruvchisi (2.3)
});
}
async parolTiklashYubor(email: string, url: string) {
await this.mailer.sendMail({
to: email, subject: "Parolni tiklash", template: "reset-password",
context: { url },
});
}
async xushKelibsiz(email: string, ism: string) {
await this.mailer.sendMail({
to: email, subject: "Xush kelibsiz!", template: "welcome", context: { ism },
});
}
}Misol 2 — Handlebars shablonlar (2.3)
<!-- templates/verification.hbs -->
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #4f46e5;">Mana Do'kon</h1>
<p>Ro'yxatdan o'tganingiz uchun rahmat! Email manzilingizni tasdiqlang:</p>
<a href="{{url}}" style="display:inline-block; background:#4f46e5; color:#fff;
padding:12px 24px; border-radius:6px; text-decoration:none;">Tasdiqlash</a>
<p style="color:#999; font-size:12px;">Havola 24 soat amal qiladi.</p>
</div>
<!-- templates/reset-password.hbs -->
<div style="font-family: Arial; max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #4f46e5;">Parolni tiklash</h1>
<p>Parolni tiklash uchun bosing (1 soat amal qiladi):</p>
<a href="{{url}}" style="background:#4f46e5; color:#fff; padding:12px 24px;
border-radius:6px; text-decoration:none;">Parolni tiklash</a>
<p style="color:#999;">So'ramagan bo'lsangiz, e'tiborsiz qoldiring.</p>
</div>Misol 3 — Email tasdiqlash (AuthService — 2.4, 2.6)
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private mailService: MailService,
private config: ConfigService,
) {}
async signup(dto: SignupDto) {
const mavjud = await this.usersService.emailBilan(dto.email);
if (mavjud) throw new ConflictException("Email band");
const parolHash = await bcrypt.hash(dto.parol, 12); // (5.15)
const user = await this.usersService.yarat({
...dto, parol: parolHash, emailTasdiqlangan: false,
});
// Tasdiqlash tokeni (JWT — 2.5)
const token = await this.jwtService.signAsync(
{ sub: user.id, type: "verify" },
{ secret: this.config.get("VERIFY_SECRET"), expiresIn: "24h" },
);
await this.mailService.tasdiqlashYubor(user.email, token); // email (2.2)
return { message: "Tasdiqlash emaili yuborildi. Emailingizni tekshiring." };
}
async emailTasdiqla(token: string) {
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.config.get("VERIFY_SECRET"),
});
if (payload.type !== "verify") throw new Error("Noto'g'ri token turi");
await this.usersService.emailTasdiqla(payload.sub); // emailTasdiqlangan: true
return { message: "Email muvaffaqiyatli tasdiqlandi" };
} catch {
throw new BadRequestException("Havola yaroqsiz yoki muddati tugagan");
}
}
}Misol 4 — Parol tiklash (AuthService — 2.9)
async parolTiklashSorov(email: string) {
const user = await this.usersService.emailBilan(email);
if (user) { // oshkor qilma (14)
const token = crypto.randomBytes(32).toString("hex"); // (5.3)
await this.usersService.resetTokenSaqla(
user.id,
await bcrypt.hash(token, 10), // DB hash (14)
new Date(Date.now() + 60 * 60 * 1000), // 1 soat
);
const url = `${this.config.get("CLIENT_URL")}/reset-password?token=${token}&id=${user.id}`;
await this.mailService.parolTiklashYubor(email, url);
}
return { message: "Agar email mavjud bo'lsa, tiklash havolasi yuborildi" }; // doim bir xil (14)
}
async parolTiklash(userId: number, token: string, yangiParol: string) {
const user = await this.usersService.resetTokenBilan(userId);
if (
!user?.resetToken || !user.resetExpires || user.resetExpires < new Date() ||
!(await bcrypt.compare(token, user.resetToken)) // hash tekshir (14)
) {
throw new BadRequestException("Havola yaroqsiz yoki muddati tugagan");
}
await this.usersService.parolYangila(userId, await bcrypt.hash(yangiParol, 12)); // (5.15)
await this.usersService.resetTokenTozala(userId); // bir martalik
await this.usersService.refreshBekor(userId); // barcha refresh bekor (5.16: 2.6, 14)
return { message: "Parol tiklandi. Qaytadan kiring." };
}Misol 5 — AuthController (email endpoint'lar — 2.6, 2.8)
@ApiTags("auth")
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Get("verify-email") // havola (2.6)
verifyEmail(@Query("token") token: string) {
return this.authService.emailTasdiqla(token);
}
@Public()
@Throttle({ default: { limit: 3, ttl: 60000 } }) // rate limit (spam — 8.16, 14)
@Post("forgot-password")
forgotPassword(@Body() dto: ForgotPasswordDto) { // { email } (8.5)
return this.authService.parolTiklashSorov(dto.email);
}
@Public()
@Post("reset-password")
resetPassword(@Body() dto: ResetPasswordDto) { // { id, token, yangiParol } (8.5)
return this.authService.parolTiklash(dto.id, dto.token, dto.yangiParol);
}
@UseGuards(JwtAuthGuard)
@Throttle({ default: { limit: 3, ttl: 60000 } }) // IP darajasida cheklov (2.9a, 8.16)
@Post("resend-verification")
resend(@CurrentUser() user) { // cooldown service'da (2.9a)
return this.authService.tasdiqlashQaytaYubor(user.id);
}
}Misol 6 — DTO'lar (8.5)
export class ForgotPasswordDto {
@ApiProperty() @IsEmail() email: string;
}
export class ResetPasswordDto {
@ApiProperty() @IsNumber() id: number;
@ApiProperty() @IsString() token: string;
@ApiProperty()
@IsString() @MinLength(8)
@Matches(/[A-Z]/) @Matches(/\d/) // kuchli parol (5.15)
yangiParol: string;
}Misol 7 — EmailVerified guard (2.7)
@Injectable()
export class EmailVerifiedGuard implements CanActivate {
constructor(private usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const { user } = context.switchToHttp().getRequest();
const dbUser = await this.usersService.bitta(user.id);
if (!dbUser.emailTasdiqlangan) {
throw new ForbiddenException("Iltimos, avval emailingizni tasdiqlang"); // 403
}
return true;
}
}
// Ishlatish — tasdiqlangan email talab qiluvchi amal
@UseGuards(JwtAuthGuard, EmailVerifiedGuard) // tartib (8.6: 2.11)
@Post("orders")
buyurtma(@Body() dto: CreateOrderDto, @CurrentUser() user) {
// faqat tasdiqlangan email'li user buyurtma bera oladi
}Misol 8 — Email fonda (navbat — 2.10, 8.22)
// mail.processor.ts (BullMQ worker — 8.22)
import { Processor, WorkerHost } from "@nestjs/bullmq";
@Processor("email")
export class MailProcessor extends WorkerHost {
constructor(private mailService: MailService) { super(); }
async process(job: Job) { // navbatdan email (8.22)
switch (job.name) {
case "verification":
await this.mailService.tasdiqlashYubor(job.data.email, job.data.token);
break;
case "welcome":
await this.mailService.xushKelibsiz(job.data.email, job.data.ism);
break;
}
}
}
// AuthService — navbatga (so'rovni bloklamasdan — 5.19: 2.9)
async signup(dto: SignupDto) {
const user = await this.usersService.yarat(...);
const token = await this.jwtService.signAsync(...);
await this.emailQueue.add("verification", { email: user.email, token }); // fonda (2.10)
return { message: "Tasdiqlash emaili yuborildi" }; // darrov javob
}Misol 9 — Xush kelibsiz (tasdiqlangach — 2.11)
async emailTasdiqla(token: string) {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.config.get("VERIFY_SECRET"),
});
const user = await this.usersService.emailTasdiqla(payload.sub);
// Tasdiqlangach — xush kelibsiz email (fonda — 2.10)
await this.emailQueue.add("welcome", { email: user.email, ism: user.ism });
return { message: "Email tasdiqlandi" };
}Misol 10 — To'liq auth + email modul (2.13)
Modullar:
AuthModule 8.9-bob — strategiyalar, AuthService, AuthController
MailModule — MailerModule, MailService 2.1-bob
UsersModule 8.3-bob — UsersService
(BullMQ — 8.22 — email navbat)
AuthModule imports: [UsersModule, MailModule, JwtModule, BullModule.registerQueue({ name: "email" })]
Oqim 2.13-bob:
signup user + verify token emailQueue (fonda) tasdiqlash email
verify-email tasdiqlash welcome email (fonda)
login access + refresh (cookie)
forgot/reset reset token + email
himoyalangan JwtGuard (+ EmailVerifiedGuard / RolesGuard)5. To'g'ri va noto'g'ri holatlar
1) SMTP kalitlarini hardcode
kodda (14)
forRootAsync + ConfigService (.env)2) Parol tiklashda foydalanuvchi oshkor qilish
"email topilmadi" (hacker email aniqlaydi — 14, 2.8)
doim "agar email mavjud bo'lsa, yuborildi"3) Reset token DB'da ochiq
token ochiq (DB sizsa — account takeover — 14)
bcrypt hash (DB'da)4) Og'ir email so'rovda
signup'da email kutish (sekin — 5.19: 2.9)
navbatga (fonda — 2.10)5) Token type aralashtirish
access token bilan verify (xavfli)
type ajrat (verify/reset/access — alohida secret — 2.5)6. Keng tarqalgan xatolar va yechimlari
Xato 1 — Email yuborilmaydi
Sababi: SMTP kalit/host noto'g'ri (5.19: Xato 1). Yechimi: .env tekshir; Gmail App Password (5.19: 2.14); verify().
Xato 2 — Shablon topilmadi
Sababi: templates/ dist'ga ko'chirilmagan 2.3-bob. Yechimi: nest-cli.json assets; dir to'g'ri.
Xato 3 — Tasdiqlash havolasi ishlamaydi
Sababi: token muddati/type noto'g'ri 2.5-bob. Yechimi: expiresIn; type tekshir; CLIENT_URL.
Xato 4 — Reset token doim yaroqsiz
Sababi: hash mos emas yoki muddat 2.9-bob. Yechimi: bcrypt.compare; expires tekshir.
Xato 5 — Email so'rovni sekinlashtiradi
Sababi: to'g'ridan yuborish 2.10-bob. Yechimi: navbat (BullMQ — 8.22).
Xato 6 — MailerService inject bo'lmaydi
Sababi: MailModule import yo'q 2.1-bob. Yechimi: MailModule exports + import qiluvchi modulda imports.
Xato 7 — Reset token DB'da ochiq (bcrypt.compare doim false)
Sababi: DB'ga xeshlangan token saqlangan, lekin URL'dagi ochiq token o'rniga xesh solishtirilyapti yoki teskari (2.5a). Yechimi: DB'ga bcrypt.hash(token) saqla; tekshirishda bcrypt.compare(urlToken, dbHash) — ochiq token birinchi argument.
Xato 8 — Qayta yuborish spam'i (email bombardimon)
Sababi: resend'da cooldown yo'q — tugma ko'p bosilib email toshadi (2.9a). Yechimi: DB'da oxirgiTasdiqYuborilgan + 60s cooldown (429) va route'da @Throttle 8.16-bob.
7. Integratsiya — bu mavzu stack'ning qayerida uchraydi
- Email 5.19-bob: Nodemailer — NestJS MailerModule.
- Auth 8.9-bob: email tasdiqlash, parol tiklash.
- Guards 8.6-bob: EmailVerifiedGuard.
- Token 5.16-bob: verify/reset token.
- crypto 5.3-bob: token yaratish.
- DTO 8.5-bob: forgot/reset validatsiya.
- Navbat 8.22-bob: email fonda.
- Config 8.14-bob: SMTP kalitlar.
- Throttler 8.16-bob: email rate limit.
- Xavfsizlik (14): token hash, oshkor qilmaslik.
8. Eng yaxshi amaliyotlar (best practices)
- MailModule alohida (MailerService DI — 2.1); Handlebars shablon 2.3-bob.
- SMTP kalitlari .env (forRootAsync — 14).
- Email tasdiqlash (signup — token + email — 2.4); EmailVerifiedGuard 2.7-bob.
- Parol tiklash: token hash, oshkor qilma, refresh bekor (2.9, 14).
- Og'ir email fonda (navbat — BullMQ — 2.10, 8.22).
- Token type ajrat (verify/reset/access — alohida secret — 2.5).
- Email rate limiting (spam — Throttler — 8.16).
- Mailtrap dev'da (haqiqiy email yuborma — 5.19: 2.11).
- DTO validatsiya (email, kuchli parol — 8.5).
- Xush kelibsiz (tasdiqlangach — 2.11).
9. Amaliy loyiha: "Email Tasdiqlash + Parol Tiklash"
NestJS email va auth amaliyotini mustahkamlash.
Maqsad
Auth'ni 8.9-bob email bilan to'ldirish: email tasdiqlash, parol tiklash, EmailVerifiedGuard, fonda email.
Talablar (requirements)
- MailModule: MailerModule.forRootAsync + MailService (Misol 1, 2.1).
- Handlebars shablonlar: verification, reset, welcome (Misol 2, 2.3).
- Email tasdiqlash: signup token email verify (Misol 3, 2.4).
- Parol tiklash: forgot (token+email) reset (token verify + yangi parol + refresh bekor) (Misol 4, 2.8).
- DTO: forgot/reset (email, kuchli parol — Misol 6, 8.5).
- EmailVerifiedGuard: tasdiqlangan email talab (Misol 7, 2.7).
- Controller: verify/forgot/reset/resend — @Public + rate limit (Misol 5).
- Og'ir email fonda: navbat (Misol 8, 2.10, 8.22).
- Xush kelibsiz: tasdiqlangach (Misol 9).
- Qayta yuborish: resend + cooldown (60s) +
@Throttle(2.9a). - Token maydonlari: entity'da xeshlangan token +
expires(2.5a). - Xavfsizlik: token hash, oshkor qilmaslik, refresh bekor (2.12, 14).
Maslahatlar (hint)
- SMTP .env (forRootAsync — 14, 1-xato).
- Shablon: templates/ dist'ga (nest-cli assets — 2.3, 2-xato).
- Parol tiklash: oshkor qilma + token hash (14, 2-xato, 3-xato).
- Token type ajrat (verify/reset — 2.5, 5-holat).
- Tiklangach refresh bekor (5.16: 2.6).
- Email fonda (navbat — 2.10).
"Tayyor" mezonlari (acceptance criteria)
- MailModule (MailerService DI).
- Handlebars shablonlar.
- Email tasdiqlash (token verify).
- Parol tiklash (forgot reset).
- DTO validatsiya.
- EmailVerifiedGuard.
- Controller (@Public, rate limit).
- Email fonda (navbat).
- Xush kelibsiz.
- Qayta yuborish (cooldown + rate limit).
- Token maydonlari (xesh + expiry entity'da).
- Xavfsizlik (hash, oshkor qilmaslik, refresh bekor).
Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.
10. Xulosa va keyingi bobga ko'prik
Bu bobda auth'ni email bilan to'ldirdik:
- MailerModule (Nodemailer — NestJS — 2.1); MailService (DI — 2.2); Handlebars shablon 2.3-bob.
- Email tasdiqlash (signup token verify — 2.4, 2.6); token turi (JWT/crypto — 2.5); EmailVerifiedGuard 2.7-bob.
- Parol tiklash (forgot reset — token hash, oshkor qilmaslik, refresh bekor — 2.8, 2.9).
- Og'ir email fonda (navbat — 2.10, 8.22); guard/token to'liq amaliyot 2.13-bob; xavfsizlik (14 — 2.12).
Keyingi bob — 8.11-bob: Testing — Jest, unit test va e2e test (chuqur). Auth/email'ni bildik; endi NestJS'ning professional qismini — testlash ni — chuqur o'rganamiz: Jest, unit test (service — DI mock — 8.2: 2.17), e2e test (to'liq oqim — Supertest), test izolyatsiyasi. Test — sifatli, ishonchli kodning kafolati (DI buni oson qiladi — 8.2).
Foydalanilgan rasmiy/ishonchli manbalar
- nodemailer.com — SMTP transport, HTML/shablon email, ilova (attachment)
- @nestjs-modules/mailer — MailerModule, forRootAsync, shablon adapter (Handlebars/Pug/EJS)
- docs.nestjs.com/security/authentication (guard, token, custom decorator)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!