Email yuborish va User Activation

~25 daq · NestJS · Qo'shimcha xizmatlar

Muammodan boshlaylik

Foydalanuvchi ro'yxatdan o'tdi, lekin email manzili to'g'rimi? Kimdir boshqaning emailini kiritgan bo'lishi mumkin. Yoki tasodifan xato yozgan.

Ko'pchilik ilovalar ro'yxatdan o'tishdan keyin email tasdiqlashni talab qiladi:

  1. Foydalanuvchi email kiritadi
  2. Unga maxsus link yuboriladi
  3. U linkni bosadi — hisob faollashadi

Bu ham xavfsizlik, ham spam oldini olish.


Nodemailer nima?

Nodemailer — Node.js da email yuborish uchun eng mashhur kutubxona. Gmail, Outlook, yoki o'z SMTP serveringiz bilan ishlaydi.

npm install nodemailer
npm install -D @types/nodemailer

Email Module yaratish

// email/email.service.ts
import { Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';

@Injectable()
export class EmailService {
  private transporter: nodemailer.Transporter;

  constructor() {
    this.transporter = nodemailer.createTransport({
      host: process.env.SMTP_HOST,
      port: Number(process.env.SMTP_PORT),
      auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASS,
      },
    });
  }

  async send(to: string, subject: string, html: string) {
    await this.transporter.sendMail({
      from: `"MyApp" <${process.env.SMTP_USER}>`,
      to,
      subject,
      html,
    });
  }
}

Gmail bilan sozlash (development uchun)

# .env
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=yourname@gmail.com
SMTP_PASS=your_app_password  # Google App Password (ikki bosqichli tekshiruv yoqilgan bo'lsa)

⚠️ Gmail parolini emas, App Password ishlating. Google Account → Security → App Passwords dan oling.

Production da: Resend, SendGrid, Mailgun kabi maxsus xizmatlar ishlatiladi. Arzonroq va ishonchli.


HTML shablonlar

Oddiy matn emas, chiroyli email:

// email/templates/welcome.template.ts
export const welcomeTemplate = (name: string, activationLink: string) => `
<!DOCTYPE html>
<html>
  <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
    <h2>Salom, ${name}!</h2>
    <p>Ro'yxatdan o'tganingiz uchun rahmat.</p>
    <p>Hisobingizni faollashtirish uchun quyidagi tugmani bosing:</p>
    <a href="${activationLink}"
       style="background: #4F46E5; color: white; padding: 12px 24px;
              text-decoration: none; border-radius: 6px; display: inline-block;">
      Hisobni faollashtirish
    </a>
    <p>Link 24 soat amal qiladi.</p>
  </body>
</html>
`;

User Activation — to'liq oqim

1. Ro'yxatdan o'tishda token yaratish

// users/users.service.ts
import { randomBytes } from 'crypto';

@Injectable()
export class UsersService {
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService,
  ) {}

  async register(dto: RegisterDto) {
    // Token yaratish
    const activationToken = randomBytes(32).toString('hex');
    const tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 soat

    // Foydalanuvchi yaratish (hali faol emas)
    const user = await this.userRepository.save({
      ...dto,
      password: await bcrypt.hash(dto.password, 10),
      isActive: false,               // ← faol emas
      activationToken,               // ← token saqlangan
      activationTokenExpiry: tokenExpiry,
    });

    // Email yuborish
    const link = `${process.env.APP_URL}/auth/activate/${activationToken}`;
    await this.emailService.send(
      user.email,
      'Hisobingizni faollashtiring',
      welcomeTemplate(user.name, link),
    );

    return { message: 'Emailingizni tekshiring' };
  }
}

2. Token orqali faollashtirish

async activate(token: string) {
  const user = await this.userRepository.findOne({
    where: { activationToken: token },
  });

  if (!user) {
    throw new BadRequestException('Token noto\'g\'ri');
  }

  if (user.activationTokenExpiry < new Date()) {
    throw new BadRequestException('Token muddati o\'tgan');
  }

  await this.userRepository.update(user.id, {
    isActive: true,
    activationToken: null,       // ← bir marta ishlatildi, o'chirildi
    activationTokenExpiry: null,
  });

  return { message: 'Hisob faollashtirildi' };
}

3. Controller

@Controller('auth')
export class AuthController {
  @Get('activate/:token')
  activate(@Param('token') token: string) {
    return this.usersService.activate(token);
  }

  @Post('resend-activation')
  resendActivation(@Body('email') email: string) {
    return this.usersService.resendActivation(email);
  }
}

Parolni tiklash (Reset Password)

Xuddi shu pattern, boshqa maqsad uchun:

async forgotPassword(email: string) {
  const user = await this.userRepository.findOne({ where: { email } });

  if (!user) return; // ← email topilmasa ham xato bermang (xavfsizlik)

  const token = randomBytes(32).toString('hex');
  await this.userRepository.update(user.id, {
    resetToken: token,
    resetTokenExpiry: new Date(Date.now() + 60 * 60 * 1000), // 1 soat
  });

  const link = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
  await this.emailService.send(
    email,
    'Parolni tiklash',
    resetPasswordTemplate(link),
  );
}

Tez-tez uchraydigan xatolar

1. Email ketmayapti, xato yo'q Sababi: SMTP sozlamalari noto'g'ri, lekin catch qilinmayapti. Yechim: try/catch qo'shing va logging ishlating.


2. Gmail "Less secure app" xatosi Sababi: Gmail oddiy parol bilan ulanishni bloklaydi. Yechim: Google App Password ishlating (ikki bosqichli tekshiruv yoqiq bo'lishi kerak).


3. Spam papkasiga tushyapti Sababi: SPF, DKIM, DMARC yozuvlari sozlanmagan. Yechim: Production da SendGrid/Resend kabi xizmatlar ishlating — ular domain tekshiruvini osonlashtiradi.


Xulosa

Eslab qoling:

  • Nodemailer — email yuborish uchun
  • Activation token — randomBytes(32) bilan yaratiladi
  • Token bir martalik, vaqt cheklangan bo'lishi kerak
  • Faollashtirilgandan keyin token o'chiriladi
  • Production da Gmail emas — SendGrid, Resend, Mailgun ishlating

Keyingi dars: 49-dars: Telegram Bot — asoslar