iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0

前言

前兩天整理了 Google 第三方登入,今天要寫的是註冊登入同樣也常用到的功能 - 信箱驗證。同樣也會用到 Google 的服務。

為什麼需要信箱驗證?

  1. 確認信箱是真的
    防止使用者隨便亂填 email,確保這個信箱是真實存在且使用者可以收信的。
  2. 防止惡意註冊
    如果沒有驗證機制,有人可能會用機器人大量註冊假帳號,癱瘓你的系統。
  3. 增加安全性
    確保是本人操作,而不是別人用你的 email 去註冊。
  4. 建立溝通管道
    確認 email 後,未來就可以安心寄送重要通知、密碼重設信件等等。

信箱驗證流程

讓我們先理解整個驗證流程是怎麼運作的:

1. 使用者註冊
   ↓
2. 系統生成驗證碼 (Token)
   ↓
3. 寄送驗證信到使用者信箱
   ↓
4. 使用者點擊驗證連結
   ↓
5. 後端驗證 Token 是否正確
   ↓
6. 更新使用者為「已驗證」狀態
   ↓
7. 完成!可以正常使用所有功能

Google 應用程式密碼設定

想要用程式寄信,不能直接用你的 Google 帳號密碼,這樣太不安全,要申請一個專門給應用程式用的密碼。

步驟 1:開啟兩步驟驗證

重要! 一定要先開啟兩步驟驗證,才能使用應用程式密碼功能。

  1. 前往 Google 帳戶
  2. 左側選單點選「安全性」
  3. 找到「兩步驟驗證」並開啟

步驟 2:建立應用程式密碼

  1. 在 Google 帳戶頁面搜尋「應用程式密碼」

image

  1. 輸入應用程式名稱(例如:「我的網站寄信功能」)

image

  1. 點擊「建立」後,Google 會給你一組 16 位數的密碼

image

⚠️ 注意! 這組密碼只會顯示一次,請立刻複製起來!

步驟 3:設定環境變數

.env 檔案中加入以下設定:

# Email 設定
EMAILER_USER=你的gmail帳號@gmail.com
EMAILER_PASSWORD=chup qcbi sjcz vbfd  # 剛剛複製的應用程式密碼

安裝必要套件

我們需要用 nodemailer 來寄信:

npm install nodemailer
npm install --save-dev @types/nodemailer

資料庫設定

首先要在 User 資料表中加上驗證相關的欄位。

更新 User Entity

// src/models/user.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Column()
  name: string;

  // 新增:信箱驗證相關欄位
  @Column({ default: false })
  isEmailVerified: boolean;

  @Column({ nullable: true })
  emailVerificationToken: string;

  @Column({ nullable: true })
  emailVerificationExpires: Date;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

欄位說明:

  • isEmailVerified: 標記信箱是否已驗證
  • emailVerificationToken: 驗證用的 Token
  • emailVerificationExpires: Token 過期時間

建立郵件服務

建立 Email Service

// src/services/email.service.ts
// src/services/email.service.ts
import nodemailer from 'nodemailer';
import dotenv from 'dotenv';

dotenv.config();

class EmailService {
  private transporter;

  constructor() {
    this.transporter = nodemailer.createTransport({
      service: 'gmail',
      auth: {
        user: process.env.EMAILER_USER,
        pass: process.env.EMAILER_PASSWORD,
      },
    });
  }

  /**
   * 寄送驗證信
   */
  async sendVerificationEmail(
    email: string,
    name: string,
    verificationToken: string
  ): Promise<void> {
    const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;

    const mailOptions = {
      from: process.env.EMAILER_USER,
      to: email,
      subject: '請驗證您的信箱',
      html: `
        <p>哈囉 ${name},</p>
        <p>請點擊以下連結驗證您的信箱:</p>
        <p><a href="${verificationUrl}">${verificationUrl}</a></p>
        <p>此連結將在 24 小時後失效。</p>
      `,
    };

    try {
      await this.transporter.sendMail(mailOptions);
      console.log('驗證信已寄送至:', email);
    } catch (error) {
      console.error('寄信失敗:', error);
      throw new Error('寄送驗證信失敗');
    }
  }

  /**
   * 寄送密碼重設信
   */
  async sendPasswordResetEmail(
    email: string,
    name: string,
    resetToken: string
  ): Promise<void> {
    const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;

    const mailOptions = {
      from: process.env.EMAILER_USER,
      to: email,
      subject: '重設您的密碼',
      html: `
        <p>哈囉 ${name},</p>
        <p>請點擊以下連結重設您的密碼:</p>
        <p><a href="${resetUrl}">${resetUrl}</a></p>
        <p>此連結將在 1 小時後失效。</p>
      `,
    };

    try {
      await this.transporter.sendMail(mailOptions);
      console.log('密碼重設信已寄送至:', email);
    } catch (error) {
      console.error('寄信失敗:', error);
      throw new Error('寄送密碼重設信失敗');
    }
  }
}

export default new EmailService();

更新環境變數

別忘了在 .env 加上前端網址:

# Email 設定
EMAILER_USER=你的gmail帳號@gmail.com
EMAILER_PASSWORD=你的應用程式密碼

# 前端網址
FRONTEND_URL=http://localhost:3001

測試功能

成功的話會看到:

image

參考資源


上一篇
Day23 - Passport.js
下一篇
Day25 - supabase
系列文
欸欸!! 這是我的學習筆記26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言