WisarWisar
Dasturlash kitobi/8-QISM — NestJS23 daqiqa

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

bash
npm install @nestjs-modules/mailer nodemailer handlebars
ts
// 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). Alohida MailModule (separation — 8.1). MailService (DI — 2.2).

2.2. MailService (DI bilan)

ts
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
    });
  }
}

MailServiceMailerService (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)

handlebars
<!-- 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). MailerModule dir da topadi. Build'da templates/ ni dist/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:

ts
// 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 aylantirish

Adapter 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, inlineCssEnabled bilan <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/.ejs kengaytmasi va sintaksis farq qiladi.

2.4. Email tasdiqlash oqimi (token bilan)

text
  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)

ts
// 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:

ts
// 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;
}
ts
// 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:

  1. Muddat (expiry) — token expiresAt bilan saqlanadi; tekshirishda resetExpires < new Date() bo'lsa — rad et. Tasdiqlash odatda 24 soat, parol tiklash 1 soat (qisqaroq — xavfliroq amal).
  2. 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.
  3. 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 — expiresIn ichida 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

ts
// 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)

ts
// 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 kerak

EmailVerifiedGuard 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)

text
  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

ts
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.

ts
// 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:

  1. Cooldown (foydalanuvchi darajasida) — DB'da oxirgiTasdiqYuborilgan vaqtini 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).
  2. Rate limit (IP/route darajasida) — controller'da @Throttle 8.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 @Throttle bilan — Misol 5).

2.10. Og'ir email fonda (navbat — 8.22 kirish)

ts
// 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)

text
  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)

text
   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)

text
  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 tizimi

Bu 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)

text
   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

ts
// 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 | EjsAdapter

4. Batafsil kod namunalari

Misol 1 — MailModule + MailService (2.1, 2.2)

ts
// 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)

handlebars
<!-- 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)

ts
@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)

ts
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)

ts
@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)

ts
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)

ts
@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)

ts
// 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)

ts
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)

text
  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

text
 kodda (14)
 forRootAsync + ConfigService (.env)

2) Parol tiklashda foydalanuvchi oshkor qilish

text
 "email topilmadi" (hacker email aniqlaydi — 14, 2.8)
 doim "agar email mavjud bo'lsa, yuborildi"

3) Reset token DB'da ochiq

text
 token ochiq (DB sizsa — account takeover — 14)
 bcrypt hash (DB'da)

4) Og'ir email so'rovda

text
 signup'da email kutish (sekin — 5.19: 2.9)
 navbatga (fonda — 2.10)

5) Token type aralashtirish

text
 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)

  1. MailModule: MailerModule.forRootAsync + MailService (Misol 1, 2.1).
  2. Handlebars shablonlar: verification, reset, welcome (Misol 2, 2.3).
  3. Email tasdiqlash: signup token email verify (Misol 3, 2.4).
  4. Parol tiklash: forgot (token+email) reset (token verify + yangi parol + refresh bekor) (Misol 4, 2.8).
  5. DTO: forgot/reset (email, kuchli parol — Misol 6, 8.5).
  6. EmailVerifiedGuard: tasdiqlangan email talab (Misol 7, 2.7).
  7. Controller: verify/forgot/reset/resend — @Public + rate limit (Misol 5).
  8. Og'ir email fonda: navbat (Misol 8, 2.10, 8.22).
  9. Xush kelibsiz: tasdiqlangach (Misol 9).
  10. Qayta yuborish: resend + cooldown (60s) + @Throttle (2.9a).
  11. Token maydonlari: entity'da xeshlangan token + expires (2.5a).
  12. 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!
8.10-bob: Email (Nodemailer), guard va tokenlar amaliyoti — Wisar