基于Node.js+Express实现类SendGrid的子账户创建与成员邮件邀请逻辑
Got it, let's walk through building this sub-account creation and team invite system with Node.js and Express—mirroring SendGrid's workflow. I'll break this down into actionable steps with code examples you can adapt:
First, let's map the core workflow to align with SendGrid's pattern:
- A primary account owner creates a sub-account with specific permissions
- The system generates a time-limited invite token, sends it via email to the team member
- The recipient clicks the link, sets their password, and activates the sub-account
- The sub-account can only perform actions matching its assigned permissions
We'll use these key dependencies:
expressfor the servermongoose(or your preferred ORM) for database modelingnodemailer+ a transactional email service (like SendGrid itself) for invitesjsonwebtokenfor secure invite tokensbcryptjsfor password hashingdotenvfor environment variables
2.1 Set Up Dependencies & Database Models
First, install the required packages:
npm install express mongoose nodemailer jsonwebtoken bcryptjs dotenv
Next, define your database models. We'll need two core models: a primary User account and a SubAccount linked to it.
// models/User.js (Primary Account) const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, password: { type: String, required: true }, subAccounts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'SubAccount' }] }); module.exports = mongoose.model('User', userSchema); // models/SubAccount.js const subAccountSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, password: { type: String }, // Empty until invite is accepted owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, permissions: [{ type: String, enum: ['send_emails', 'view_stats', 'manage_templates'], required: true }], status: { type: String, enum: ['pending', 'active'], default: 'pending' }, inviteToken: { type: String } }); module.exports = mongoose.model('SubAccount', subAccountSchema);
2.2 Create Sub-Account & Send Invite Email
Build an endpoint for primary account owners to create sub-accounts and trigger invite emails. We'll generate a JWT as the invite token (expires in 7 days) and send it via a transactional email service.
// routes/subAccounts.js const express = require('express'); const router = express.Router(); const jwt = require('jsonwebtoken'); const nodemailer = require('nodemailer'); const SubAccount = require('../models/SubAccount'); const User = require('../models/User'); require('dotenv').config(); // Configure Nodemailer with SendGrid (replace with your credentials) const transporter = nodemailer.createTransport({ service: 'SendGrid', auth: { user: process.env.SENDGRID_USERNAME, pass: process.env.SENDGRID_API_KEY } }); // Create sub-account and send invite router.post('/create', async (req, res) => { try { const { ownerId, email, permissions } = req.body; // Check if sub-account email already exists const existingSubAccount = await SubAccount.findOne({ email }); if (existingSubAccount) { return res.status(400).json({ message: 'This email is already linked to a sub-account' }); } // Generate time-limited invite token const inviteToken = jwt.sign( { email, ownerId }, process.env.JWT_SECRET, { expiresIn: '7d' } ); // Create pending sub-account record const subAccount = new SubAccount({ email, owner: ownerId, permissions, inviteToken }); await subAccount.save(); // Link sub-account to primary user await User.findByIdAndUpdate(ownerId, { $push: { subAccounts: subAccount._id } }); // Send invite email const mailOptions = { from: 'your-app@example.com', to: email, subject: 'Invitation to join our team', html: ` <p>You've been invited to join a team on our platform!</p> <p>Click the link below to set up your account:</p> <a href="${process.env.FRONTEND_URL}/accept-invite?token=${inviteToken}">Accept Invitation</a> <p>This link expires in 7 days.</p> ` }; await transporter.sendMail(mailOptions); res.status(201).json({ message: 'Sub-account created successfully, invite sent', subAccount: { id: subAccount._id, email: subAccount.email, permissions: subAccount.permissions } }); } catch (err) { res.status(500).json({ message: 'Failed to create sub-account', error: err.message }); } });
2.3 Handle Invite Acceptance
Build an endpoint to let recipients set their password and activate their sub-account. We'll verify the invite token, hash the password, and update the sub-account status.
// routes/subAccounts.js (add this to the existing file) const bcrypt = require('bcryptjs'); // Accept invite and set password router.post('/accept-invite', async (req, res) => { try { const { token, password } = req.body; // Verify invite token const decoded = jwt.verify(token, process.env.JWT_SECRET); const { email, ownerId } = decoded; // Find the pending sub-account const subAccount = await SubAccount.findOne({ email, owner: ownerId, status: 'pending', inviteToken: token }); if (!subAccount) { return res.status(400).json({ message: 'Invalid or expired invite token' }); } // Hash the new password const hashedPassword = await bcrypt.hash(password, 10); // Activate the sub-account subAccount.password = hashedPassword; subAccount.status = 'active'; subAccount.inviteToken = undefined; // Clear token after use await subAccount.save(); res.status(200).json({ message: 'Invite accepted! Your account is now active.' }); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(400).json({ message: 'Invite token has expired' }); } res.status(500).json({ message: 'Failed to accept invite', error: err.message }); } });
2.4 Permission Middleware
Add middleware to enforce permission checks for sub-account actions, ensuring they only access allowed features.
// middleware/permissionMiddleware.js const SubAccount = require('../models/SubAccount'); const hasPermission = (requiredPermission) => { return async (req, res, next) => { try { // Assuming sub-account ID is attached to req.user after authentication const subAccount = await SubAccount.findById(req.user.id); if (!subAccount.permissions.includes(requiredPermission)) { return res.status(403).json({ message: 'Insufficient permissions' }); } next(); } catch (err) { res.status(500).json({ message: 'Failed to check permissions', error: err.message }); } }; }; module.exports = hasPermission;
Use the middleware in your feature routes:
// routes/email.js const express = require('express'); const router = express.Router(); const hasPermission = require('../middleware/permissionMiddleware'); router.post('/send', hasPermission('send_emails'), async (req, res) => { // Logic to send emails (e.g., using SendGrid API) res.status(200).json({ message: 'Email sent successfully' }); });
- Security: Use HTTPS, store JWT secrets and API keys in environment variables, sanitize all inputs to prevent NoSQL injection, and use bcrypt with a high salt round (12+).
- Email Deliverability: Stick to transactional email services (SendGrid, Mailgun) instead of plain SMTP to avoid landing in spam folders.
- Authentication: Implement JWT or session-based auth for both primary and sub-accounts (sub-accounts should have their own auth tokens).
- Audit Logs: Track sub-account actions (e.g., emails sent, changes made) for accountability.
- Error Handling: Add granular error messages for common issues (e.g., duplicate invites, expired tokens) to improve user experience.
内容的提问来源于stack exchange,提问作者Vishal Trivedi

