You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MERN栈网站团队成员邀请后加入工作区的实现方案咨询

MERN栈实现团队成员加入组织工作区的思路

一、后端核心实现(Node.js/Express + MongoDB)

1. 完善数据模型

首先得扩展现有模型,新增邀请记录和组织成员关联:

  • Invitation模型:存储邀请邮件、关联组织、唯一验证token、状态和过期时间
    // models/Invitation.js
    const mongoose = require('mongoose');
    
    const invitationSchema = new mongoose.Schema({
      email: { type: String, required: true, lowercase: true },
      organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true },
      token: { type: String, required: true, unique: true },
      status: { type: String, enum: ['pending', 'accepted', 'expired'], default: 'pending' },
      createdAt: { type: Date, default: Date.now, expires: '7d' } // 邀请7天过期
    });
    
    module.exports = mongoose.model('Invitation', invitationSchema);
    
  • 更新Organization模型:添加members数组,记录成员ID和角色(这里直接给成员设full-access表示完整权限)
    // models/Organization.js
    const mongoose = require('mongoose');
    
    const organizationSchema = new mongoose.Schema({
      name: String,
      ownerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
      members: [
        {
          userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
          role: { type: String, enum: ['owner', 'full-access'], default: 'full-access' }
        }
      ],
      // 你的其他组织字段
    });
    
    module.exports = mongoose.model('Organization', organizationSchema);
    

2. 发送邀请时生成记录

在你现有的发送邀请接口里,补充生成邀请记录的逻辑,用crypto生成唯一token:

// routes/organization.js
const crypto = require('crypto');
const Invitation = require('../models/Invitation');
const Organization = require('../models/Organization');
const nodemailer = require('nodemailer');

router.post('/send-invite', async (req, res) => {
  const { email, organizationId } = req.body;
  const currentUser = req.user; // 假设已通过身份验证拿到当前用户

  // 校验当前用户是否是组织所有者
  const org = await Organization.findById(organizationId);
  if (org.ownerId.toString() !== currentUser._id.toString()) {
    return res.status(403).json({ message: '只有组织所有者能发送邀请' });
  }

  // 生成唯一验证token
  const token = crypto.randomBytes(32).toString('hex');

  // 存储邀请记录
  await Invitation.create({ email, organizationId, token });

  // 发送邮件(你已实现,这里补充带token的邀请链接)
  const transporter = nodemailer.createTransport({ /* 你的邮件配置 */ });
  await transporter.sendMail({
    to: email,
    subject: '邀请你加入组织工作区',
    html: `<p>点击下方链接加入:<a href="${process.env.FRONTEND_URL}/accept-invite/${token}">确认加入</a></p>`
  });

  res.json({ message: '邀请已发送' });
});

3. 处理邀请接受请求

前端点击邀请链接后,后端验证token并完成成员添加:

// routes/invitation.js
router.get('/accept/:token', async (req, res) => {
  const { token } = req.params;

  // 查找有效邀请(未过期、未接受)
  const invitation = await Invitation.findOne({ token, status: 'pending' });
  if (!invitation) {
    return res.status(400).json({ message: '邀请无效或已过期' });
  }

  // 查找对应用户
  let user = await User.findOne({ email: invitation.email });
  // 如果用户未注册,跳转到注册页并携带token,注册后自动处理邀请
  if (!user) {
    return res.redirect(`${process.env.FRONTEND_URL}/register?inviteToken=${token}`);
  }

  // 检查用户是否已在组织中
  const org = await Organization.findById(invitation.organizationId);
  const isAlreadyMember = org.members.some(mem => mem.userId.toString() === user._id.toString());
  if (isAlreadyMember) {
    return res.status(400).json({ message: '你已经是该组织成员了' });
  }

  // 添加用户到组织,赋予完整权限
  org.members.push({ userId: user._id, role: 'full-access' });
  await org.save();

  // 更新邀请状态为已接受
  invitation.status = 'accepted';
  await invitation.save();

  // 跳转到组织工作区
  res.redirect(`${process.env.FRONTEND_URL}/workspace/${invitation.organizationId}`);
});

4. 注册时自动处理邀请

如果用户是通过邀请链接注册的,注册完成后自动加入组织:

// routes/auth.js
router.post('/register', async (req, res) => {
  const { username, email, password, inviteToken } = req.body;

  // 常规注册逻辑(创建用户、加密密码等)
  const user = await User.create({ username, email, password });

  // 如果有邀请token,自动处理加入
  if (inviteToken) {
    const invitation = await Invitation.findOne({ token: inviteToken, status: 'pending', email });
    if (invitation) {
      const org = await Organization.findById(invitation.organizationId);
      org.members.push({ userId: user._id, role: 'full-access' });
      await org.save();
      invitation.status = 'accepted';
      await invitation.save();
    }
  }

  // 返回用户信息和JWT token
  res.json({ user, token: generateJwtToken(user) });
});

二、前端逻辑(React)

1. 邀请接受页面

创建AcceptInvite组件,处理token验证和跳转:

import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';

const AcceptInvite = () => {
  const { token } = useParams();
  const navigate = useNavigate();
  const [message, setMessage] = useState('');

  useEffect(() => {
    const handleInvite = async () => {
      try {
        const res = await fetch(`/api/invitation/accept/${token}`);
        if (res.ok) {
          const data = await res.json();
          navigate(`/workspace/${data.organizationId}`);
        } else {
          const errData = await res.json();
          setMessage(errData.message);
        }
      } catch (err) {
        setMessage('邀请处理失败,请稍后重试');
      }
    };
    handleInvite();
  }, [token, navigate]);

  return (
    <div className="invite-page">
      <h2>正在处理你的邀请...</h2>
      {message && <p className="error">{message}</p>}
    </div>
  );
};

export default AcceptInvite;

2. 注册页处理邀请参数

从URL中提取inviteToken,注册时一起提交:

import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

const Register = () => {
  const location = useLocation();
  const navigate = useNavigate();
  const [inviteToken, setInviteToken] = useState('');
  const [formData, setFormData] = useState({ username: '', email: '', password: '' });

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const token = params.get('inviteToken');
    if (token) setInviteToken(token);
  }, [location]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const res = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...formData, inviteToken })
      });
      if (res.ok) {
        navigate('/workspace');
      }
    } catch (err) {
      console.error('注册失败', err);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="register-form">
      <input
        type="text"
        placeholder="用户名"
        value={formData.username}
        onChange={(e) => setFormData({...formData, username: e.target.value})}
        required
      />
      <input
        type="email"
        placeholder="邮箱"
        value={formData.email}
        onChange={(e) => setFormData({...formData, email: e.target.value})}
        required
      />
      <input
        type="password"
        placeholder="密码"
        value={formData.password}
        onChange={(e) => setFormData({...formData, password: e.target.value})}
        required
      />
      <button type="submit">注册并加入组织</button>
    </form>
  );
};

export default Register;

3. 工作区权限校验

进入工作区前,验证用户是否是组织成员:

import { useEffect, useState } from 'react';
import { useParams, Navigate } from 'react-router-dom';

const Workspace = () => {
  const { orgId } = useParams();
  const [isAuthorized, setIsAuthorized] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const checkAccess = async () => {
      try {
        const token = localStorage.getItem('token');
        const res = await fetch(`/api/organization/${orgId}/check-member`, {
          headers: { 'Authorization': `Bearer ${token}` }
        });
        const data = await res.json();
        setIsAuthorized(data.isMember);
      } catch (err) {
        setIsAuthorized(false);
      } finally {
        setLoading(false);
      }
    };
    checkAccess();
  }, [orgId]);

  if (loading) return <div className="loading">加载中...</div>;
  if (!isAuthorized) return <Navigate to="/unauthorized" />;

  return (
    <div className="workspace-container">
      {/* 你的工作区内容 */}
    </div>
  );
};

export default Workspace;

三、后端权限中间件

给工作区相关接口加权限校验,确保只有组织成员能访问:

// middleware/organizationAuth.js
const Organization = require('../models/Organization');

exports.isOrganizationMember = async (req, res, next) => {
  const { orgId } = req.params;
  const org = await Organization.findById(orgId);
  const isMember = org.members.some(mem => mem.userId.toString() === req.user._id.toString());
  
  if (!isMember) {
    return res.status(403).json({ message: '无权限访问此工作区' });
  }
  next();
};

// 使用示例(需要先经过用户身份验证中间件)
router.get('/workspace/:orgId/data', authMiddleware, exports.isOrganizationMember, async (req, res) => {
  // 返回工作区数据
});

内容的提问来源于stack exchange,提问作者r121

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 04:01:06