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
相关产品推荐
相关产品推荐

