我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。
首先,什么是多租户SaaS应用程序?
多租户SaaS应用程序可让您从单个代码库中为多个客户提供服务。但是,为此,您需要管理安全和特定于租户的访问,手动完成时可能会具有挑战性。这就是为什么我决定使用许可证,这是一种简化此过程的现代授权工具的原因。
在本文中,我将向您展示如何使用许可证简化SaaS应用程序的授权,并逐步构建具有私人隔离和基于角色的访问控制(RBAC)的演示应用程序(RBAC),并使用Next.js和AppWrite。
什么是下一步。js和appwrite,为什么我们需要它们?
next.js
Next.js是一个基于React的框架,可提供服务器端渲染(SSR),静态站点生成(SSG),API路由和性能优化。
对于这个项目,我使用了Next.js,因为:
- 它允许预先渲染页面,从而改善性能和SEO。
- 它的内置路由使管理页面过渡和动态内容易于管理。
- 它可以轻松地与诸如AppWrite和Clum.io的后端服务集成以进行身份验证和授权。
AppWrite
AppWrite是一个后端AS-A-Service(BAAS)平台,可提供用户身份验证,数据库,存储和无服务器功能。使用诸如AppWrite之类的服务消除了从头开始构建后端的需求,因此您可以在访问后端功能的同时专注于前端开发。
对于这个项目,我使用了AppWrite:
- 处理用户注册,登录和会话管理。
- 提供一个结构化的NOSQL数据库来存储特定于租户的数据。
使用Next.js和AppWrite一起,可以使我创建一个可扩展的高性能多租户SaaS应用程序,同时保持开发过程有效。
多租户SAAS授权简介
多租户SaaS应用程序是使用该应用程序的单个软件实例为多个用户或称为租户的用户组的软件。
这意味着在多租户SaaS体系结构中,多个客户(租户)共享相同的应用程序基础架构或使用相同的应用程序,但要维护数据隔离。
一个实践的例子是Trello等项目管理工具。
- 它是一个单个基础架构,可在共享服务器上运行,并为其所有用户具有相同的代码库。
- 使用Trello(例如A和B公司B)的每个公司都是租户。
- 它隔离数据:
- 公司A的员工只能看到他们的项目,任务和董事会。
- B公司的员工无法访问或查看公司A的数据,反之亦然。
这样可以确保在共享资源时,每个租户的数据和活动都是私人且安全的。
在多租户应用程序中,即使在租户内,有些用户也可以更高地访问某些信息,而某些成员将仅限于某些资源。
在此类应用中的授权必须:
- 确保用户无法访问其他租户的数据或资源。这称为隔离租户。
- 确保租户内的用户只能通过提供颗粒状访问控制来访问其角色允许的资源。
- 处理更多的用户,租户和角色,而不会减慢或退化性能。
租户隔离和颗粒状访问控制的重要性
租户隔离通过确保每个客户的信息保持私密来确保数据的安全。虽然粒状访问控制可确保组织内的用户仅获得所需的权限。
在SaaS应用程序中实施授权可能是复杂而棘手的,但是当您拥有像许可证这样的授权工具时,它不必是。
什么是许可,其好处是什么?
许可证是一种易于使用的授权工具,用于管理任何应用程序中的访问,包括多租户应用程序。在您的应用程序中使用许可证。您可以轻松地定义和分配角色,并在应用程序中使用特定的权限进行访问控制。除了在应用程序中创建角色外,您还可以根据用户或资源属性添加条件和规则,以指定每个用户可以做什么和不能做什么。
现在,您知道了您需要了解的有关许可证及其福利的大部分内容,让我们进入主要交易 - 与Next.js一起建立SaaS申请并集成授权许可证。
为了展示许可的力量,我们将建立一个多租户Edtech SaaS平台。
构建Edtech SaaS平台涉及几个挑战,包括用户身份验证,基于角色的访问控制(RBAC)和多租户。我们将使用Next.js进行前端,AppWrite用于身份验证和数据库管理,并允许授权精细。
技术堆栈概述
系统体系结构
该应用程序遵循后端优先的方法:
- 后端(node.js express)
- 处理API请求和业务逻辑。
- 使用AppWrite进行身份验证和数据库管理。
- 工具允许授权,定义角色和权限。
- 确保在数据访问之前验证每个请求。
- 前端(Next.js)
- 连接到后端以安全地获取数据。
- 使用基于角色的UI渲染,这意味着用户只能看到他们授权访问的内容。
- 根据权限限制操作(例如创建作业)。
通过在API级别执行授权,我们确保用户无法绕过限制,即使他们操纵前端。
在本指南的末尾,您将拥有一个功能齐全的多租户Edtech SaaS应用程序,其中:
- 管理员可以添加和查看学生。
- 教师可以添加和查看学生,并创建作业。
- 学生只能查看他们指定的课程。
本文提供了我如何实施许可来处理构建该项目的授权的分步分类,因此请跟随并构建您的项目。
获得许可证的后端实施
要执行基于角色的访问控制(RBAC)和租户隔离,我们需要:
- 设置许可证并定义角色,租户和政策。
- 在后端(Node.js Express)集成许可证。
- 使用中间件在允许请求之前检查权限。
让我们逐步吧。
1。设置许可证
在编写任何代码之前,您需要
- 根据许可创建帐户。

您将出现入职,但是一旦输入组织名称,就可以跳过设置。
- 创建资源和动作
导航到策略部分,您将创建一个可以在该资源上执行的资源和操作。

创建资源后,应该看起来像这样:

- 创造角色
创建资源后,使用“角色”选项卡导航到角色页面。您会发现某些角色已自动分配。

删除这些角色并创建新角色。每个角色都将具有与用户可以做什么和不能做什么相关的特定规则。首先创建管理员角色,因为它将作为RBAC条件的构建块。单击顶部的添加角色按钮并创建角色。

创建角色后,应该看起来像这样:

伟大的!
现在您已经创建了资源和角色,现在可以在策略编辑器中配置权限。
- 在策略编辑器中配置权限
返回策略编辑器,这就是角色现在的样子,每个资源定义了每个资源以及您可以选择的操作。现在,您可以在资源上执行所选操作的角色权限。

完成每个角色的操作后,请单击页面右下方的“保存更改”按钮。
- 复制API键
最后,要使用许可证的云PDP,您将需要当前环境的API键。对于此项目,您将使用开发环境密钥。继续进行设置,然后单击API键,向下滚动到环境API键,单击“显示键”,然后复制它。

设置许可仪表板后,您现在可以继续前进。
2。安装依赖项
首先,您需要在计算机上安装node.js。确保系统上安装了node.js后,请按照以下步骤:
- 首先使用以下命令创建一个新项目:
Mkdir后端 CD Backendnpm Init -Y
- 然后,安装以下软件包:
NPM安装Express Dotenv允许CORS APPWWRITE AXIOS JSONWEBTOKEN
- Express中的配置许可证。在您的.env文件中,存储您的API密钥:
pull_api_key =您的permit-key-you popied-earlier
3.设置AppWrite
- 转到AppWrite并通过输入项目名称并选择区域来创建一个新项目。记下您的项目ID和API端点;这就是您将作为.env文件中的值输入的内容。您的env文件应该看起来像这样:
pull_api_key =您的permit-key-you popied-earlier appwrite_endpoint = https://cloud.appwrite.io/v1 appwrite_project_id = your-project-id
- 现在,继续使用数据库以创建您的数据库,然后复制数据库ID将其粘贴到Env文件中。

您的env文件现在应该看起来像这样:
pull_api_key =您的permit-key-you popied-earlier appwrite_endpoint = https://cloud.appwrite.io/v1 appwrite_project_id = your-project-id appwrite_database_id =您的数据库-ID
现在,在AppWrite数据库中创建以下属性:
- 配置文件集合

- 学生收集

- 分配收集

目前,您的env文件应该是什么样子:
pull_api_key =您的permit-key-you popied-earlier pliper_project_id = copy-from-dashboard 允许_env_id =复制式划线板 appwrite_endpoint = https://cloud.appwrite.io/v1 appwrite_project_id = your-project-id appwrite_database_id =您的数据库-ID appwrite_profile_collection_id = your-id appwrite_assignments_collection_id = your-id appwrite_students_collection_id = your-id jwt_secret =生成 - by-by-running // openssl rand -base64 16 端口= 8080
4。创建文件结构和文件
现在,在文件的根部创建一个SRC文件夹。然后在根文件夹中生成tsconfig.json文件,然后将以下代码粘贴到其中:
<span>{ </span><span>“ compileroptions”:{ </span><span>“目标”:“ ES6”, </span><span>“模块”:“ commonjs”, </span><span>“ Outdir”:“ ./ dist”, </span><span>“ eSmoduleInterop”:true, </span><span>“ forceconsistentcasinginfilenames”:是的, </span><span>“严格”:是的, </span><span>“ Skiplibcheck”:是的, </span><span>“ resolvejsonmodule”:是的, </span><span>“ baseurl”:“ ./”, </span><span>“路径”:{ </span><span>“@/*”:[“ src/*”] </span><span>} </span><span>},, </span><span>“包括”:[“ SRC/**/*”], </span><span>“排除”:[“ node_modules”,“ dist”] </span><span>}</span>
此tsconfig.json将打字稿编译器配置为Target ES6,使用commonjs模块,然后输出文件为./dist。它强制执行严格的类型检查,启用JSON模块分辨率,为SRC设置路径别名,并排除Node_Modules和Dist dist。
在SRC文件夹内部,创建以下文件夹:API,配置,控制器,中间件,模型和UTILS。
- UTILS文件夹
- 现在,在UTILS文件夹项目中创建一个新的许可证文件,以使用以下代码初始化许可证:
<span>从“许可证”导入{许可证}; </span><span>从'../config/environment'导入{puls_api_key}; </span><span>//此行初始化SDK并连接您的node.js应用 </span><span>//到上一步中设置的许可证PDP容器。 </span><span>const许可证=新许可证({ </span><span>//您的API键 </span> token <span>:plix_api_key,//将您的API键存储在.env中 </span><span>//在生产中,您可能需要更改此URL以适合您的部署 </span> PDP <span>:'https://cloudpdp.api.permit.io',//默认许可证 </span><span>//如果您希望SDK发射日志,请输入以下内容: </span> 日志<span>: { </span> 等级<span>:“调试”, </span><span>},, </span><span>//如果您获得超时 /网络错误,SDK会返回false </span><span>//如果您想提出错误,然后让您处理此错误,请输入以下内容: </span><span>// throneRror:是的, </span><span>}); </span> <span>出口默认许可证;</span>
该文件初始化node.js的许可证SDK,使用存储在环境中的API键将其连接到许可证PDP容器。它配置了调试日志记录并设置SDK以默默处理错误,除非明确配置将其投掷。
- 接下来,创建一个称为errorHandler.ts的文件,然后粘贴以下代码:
<span>//实用程序功能(例如,错误处理) </span><span>从'express'导入{请求,响应,nextfunction}; </span> <span>导出const errirhandler =(err:any,req:request,res:ress:reverse,next:next:next function)=> { </span><span>console.error('错误:',err.message || err); </span> res <span>.status(err.Status || 500).json({ </span> 错误<span>:err.message || “内部服务器错误”, </span><span>}); </span><span>};</span>
该文件定义了一个明确的错误处理中间件,该中间件记录错误并发送带有错误消息和状态代码的JSON响应。如果未提供特定状态,则默认为500状态代码。
- 模型文件夹
- 创建一个名为profile.ts的文件并粘贴以下代码:
<span>导出接口配置文件{ </span> 名称<span>:字符串; </span> 电子邮件<span>:字符串; </span> 角色<span>:'admin'| “老师” | '学生'; </span> 用户ID <span>:字符串; </span><span>}</span>
该文件定义了具有属性,电子邮件,角色和用户的属性的打字稿配置文件接口,其中角色仅限于特定值:管理员,老师或学生。
- 创建sizhtment.ts文件并粘贴以下代码:
<span>导入{数据库,id}来自'../config/appwrite'; </span><span>import {database_id,sistionments_collection_id}来自'../config/environment'; </span> <span>导出界面sizgmentData { </span> 标题<span>:字符串; </span> 主题<span>:字符串; </span> className <span>:string; </span> 老师<span>:弦; </span> duedate <span>:string; </span> 创建者邮件<span>:字符串; </span><span>} </span> <span>//创建一个新作业 </span><span>导出异步函数createSignmentIndB(数据:sigsmentmentData){ </span><span>返回等待数据库。 </span><span>database_id, </span><span>sizgments_collection_id, </span><span>id.unique(), </span> 数据 <span>); </span><span>} </span> <span>//获取所有作业 </span><span>导出异步函数fetchAssignmentsFromDB(){ </span><span>const响应=等待database.listDocuments(database_id,sissigments_collection_id); </span><span>返回响应。 </span><span>}</span>
该文件提供了与AppWrite数据库进行交互的功能,以管理作业。它定义了一个sizhtmentData接口,并包含创建新分配并从数据库中获取所有分配的功能。
- 创建一个学生。TS文件并粘贴以下代码:
<span>导入{数据库,ID,权限,角色,查询}来自'../config/appwrite'; </span><span>导入{database_id,students_collection_id}来自'../config/environment'; </span> <span>导出界面StudentData { </span> firstName <span>:string; </span> lastName <span>:string; </span> 性别<span>:“女孩” | '男孩'| '男孩'| '女孩'; </span> className <span>:string; </span> 年龄<span>:数字; </span> 创建者邮件<span>:字符串; </span><span>} </span> <span>//创建一个新学生 </span><span>导出异步函数CreateStudentIndB(数据:studentData){ </span><span>返回等待数据库。 </span><span>database_id, </span><span>学生_collection_id, </span><span>id.unique(), </span> 数据<span>, </span><span>[ </span> 许可<span>。阅读(cole.any()),//公共阅读许可 </span><span>这是给出的 </span><span>); </span><span>} </span> <span>//获取所有学生 </span><span>导出异步函数fetchStudentsFromDB(){ </span><span>const响应=等待database.listDocuments(database_id,students_collection_id); </span><span>返回响应。 </span><span>}</span>
该文件提供了在AppWrite数据库中管理学生数据的功能。它定义了一个StudentData界面,并包含功能,以创建具有公共阅读权限的新学生,并从数据库中获取所有学生。
- 中间件文件夹
- 创建auth.ts文件并粘贴以下代码:
<span>从'express'导入{请求,响应,nextfunction}; </span><span>从“ jsonwebtoken”导入JWT; </span> <span>//将请求类型扩展到包含“用户” </span><span>接口AuthentIcatedRequest扩展了请求{ </span> 用户<span>?:{ </span> id <span>:string; </span> 角色<span>:字符串; </span><span>}; </span><span>} </span> <span>const authmiddleware =(req:authenticatedRequest,res:reverse,next:next function):void => { </span><span>const token = req.headers.authorization?.split('')[1]; </span> <span>如果(!token){ </span> res <span>.status(401).json({error:'未经授权。no doken提供'}); </span><span>返回 </span><span>} </span> <span>尝试 { </span><span>const解码= jwt.verify(token,process.env.jwt_secret!)as {id:string;角色:字符串}; </span> req <span>.user =解码; </span><span>下一个(); </span><span>} catch(错误){ </span> res <span>.status(403).json({error:'无效令牌'}); </span><span>返回 </span><span>} </span><span>}; </span> <span>导出默认authmiddleware;</span>
该文件定义了用于基于JWT的身份验证的快速中间件。它检查了请求标头中的有效令牌,使用秘密键验证它,并将解码的用户信息(ID和角色)附加到请求对象。如果令牌丢失或无效,则返回适当的错误响应。
- 创建许可证ts并粘贴以下代码:
<span>从“ ../ Utils/permit”进口许可证; </span> <span>导出const charchusertopermitStudents = async(电子邮件:字符串,操作:字符串,资源:string):Promise <boolean> => { </boolean></span><span>尝试 { </span><span>const允许=等待许可证。检查(电子邮件,操作,资源); </span><span>console.log(“允许”,允许); </span><span>退货允许; </span><span>} catch(错误){ </span><span>console.error( <span>`错误同步用户<span>$ {email}</span> to bervile.io:`</span> ,错误); </span><span>返回false; </span><span>} </span><span>}; </span> <span>导出const charchusertopermitAssignment = async(电子邮件:字符串,操作:字符串,资源:string):Promise <boolean> => { </boolean></span><span>尝试 { </span><span>const允许=等待许可证。检查(电子邮件,操作,资源); </span><span>console.log(“允许”,允许); </span><span>退货允许; </span><span>} catch(错误){ </span><span>console.error( <span>`错误同步用户<span>$ {email}</span> to bervile.io:`</span> ,错误); </span><span>返回false; </span><span>} </span><span>};</span>
该文件定义了实用程序功能,checkusertopermitstudents和checkusertoperpertersignment,以在许可证中检查用户权限是否有特定的操作和资源。这两个功能都优雅地处理错误,记录问题并在权限检查失败时返回false。它们用于在应用程序中执行授权。
- 控制器文件夹
- 创建auth.ts文件并粘贴以下代码:
<span>导入{account,id}来自'../config/appwrite'; </span><span>从'express'导入{请求,响应}; </span><span>从“ jsonwebtoken”导入JWT; </span> <span>const jwt_secret = process.env.jwt_secret as String; //确保将其设置在.env文件中 </span> <span>//注册控制器 </span><span>导出const Ingip = async(req:request,res:response)=> { </span><span>const {电子邮件,密码,名称} = req.body; </span> <span>如果(!电子邮件||!密码||!name){ </span><span>返回res.status(400).json({错误:'名称,电子邮件和密码是必需的。'}); </span><span>} </span> <span>尝试 { </span><span>const user =等待帐户。 </span><span>//生成JWT </span><span>const token = jwt.sign({email},jwt_secret,{expiresin:'8h'}); </span> res <span>.cookie('token',令牌,{ </span> httponly <span>:是的, </span> Samesite <span>:“严格”, </span> 安全<span>:是的, </span><span>}); </span> res <span>.status(201).json({成功:true,用户,token}); </span><span>} catch(错误:任何){ </span><span>Console.Error('注册错误:',错误); </span> res <span>.status(500).json({成功:false,消息:error.message}); </span><span>} </span><span>}; </span> <span>//登录控制器 </span><span>导出const login = async(req:request,res:response)=> { </span><span>const {email,passwass} = req.body; </span> <span>如果(!电子邮件||!密码){ </span><span>返回res.status(400).json({错误:需要电子邮件和密码。'}); </span><span>} </span> <span>尝试 { </span><span>const session =等待帐户。CreateeMailPasswordsession(电子邮件,密码); </span> <span>//生成无角色的JWT </span><span>const token = jwt.sign( </span><span>{userId:session.userid,email},//不包括任何角色 </span><span>jwt_secret, </span><span>{expiresin:'8H'} </span><span>); </span> res <span>.cookie('token',令牌,{ </span> httponly <span>:是的, </span> Samesite <span>:“严格”, </span> 安全<span>:是的, </span><span>}); </span> res <span>.status(200).json({成功:true,token,session}); </span><span>} catch(错误:任何){ </span><span>Console.Error('登录错误:',错误); </span> res <span>.status(401).json({成功:false,消息:error.message}); </span><span>} </span><span>}; </span> <span>//注销控制器 </span><span>导出const logout = async(req:request,res:response)=> { </span><span>尝试 { </span><span>等待帐户。deletesessession('当前会话ID'); </span> res <span>.clearcookie('token'); </span> res <span>.status(200).json({成功:true,消息:'成功登录'}); </span><span>} catch(错误:任何){ </span><span>Console.Error('登录错误:',错误); </span> res <span>.status(500).json({成功:false,消息:error.message}); </span><span>} </span><span>};</span>
该文件定义了用于注册,登录和注销的身份验证控制器,与AppWrite集成了用于用户管理的AppWrite和用于会话处理的JWT。注册和登录控制器验证输入,创建用户会话并生成JWT,而注销控制器清除了会话和令牌。所有控制器都处理错误并返回适当的响应。
- 创建sizhtment.ts文件并粘贴以下代码:
<span>从'express'导入{请求,响应}; </span><span>导入{createSignmentIndb,sissigmentData,fetchAssignmentsfromdb} from'../models/assignment'; </span><span>从'../middleware/permit'import {checkusertopermitAssignment}; </span> <span>//创建一个新作业 </span><span>导出异步函数createSignment(req:request ,res:reverse:wendesp):Promise <void> { </void></span><span>尝试 { </span><span>const {标题,主题,老师,className,duedate,createMail}:sigsionmentData = req.body; </span> <span>const Ispermittit =等待checkusertopermitAssignment(createRemail,“ create”,“ sistments”); </span><span>如果(! </span> res <span>.status(403).json({error:'未授权'}); </span><span>返回; </span><span>} </span> <span>const newAssignment =等待createSignmentIndb({{ </span> 标题<span>, </span> 主题<span>, </span> 老师<span>, </span> className <span>, </span> 到期日<span>, </span> 创造力 <span>}); </span> <span>console.log(创建新的分配:',new Assignment); </span> res <span>.Status(201).json(newAssignment); </span><span>} catch(错误){ </span><span>Console.Error('错误创建分配:',错误); </span> res <span>.status(500).json({error :( erry as任何时候).message}); </span><span>} </span><span>} </span> <span>//获取所有作业 </span><span>导出异步函数fetchAssignments(req:request,res:reverse):Promise <void> { </void></span><span>尝试 { </span><span>const {email} = req.params; </span> <span>const iSpernittit =等待checkusertopermitAssignment(电子邮件,读取”,“ sigsments”); </span><span>如果(! </span> res <span>.status(403).json({消息:'未授权'}); </span><span>返回; </span><span>} </span> <span>const sistments =等待fetchassignmentsfromdb(); </span> res <span>.Status(200).json(作业); </span><span>} catch(错误){ </span> res <span>.status(500).json({error :( erry as任何时候).message}); </span><span>} </span><span>}</span>
该文件定义了用于创建和获取分配的控制器,以与数据库集成并允许进行授权检查。 CreateSignment Controller验证输入,检查权限并创建一个新的分配,而FetchAssignments Controller在验证访问后会检索所有任务。两个控制器都处理错误并返回适当的响应。
- 创建一个学生。TS文件并粘贴以下代码:
<span>进口 { </span> CreateStudentIndb <span>, </span> <span>从 </span> StudentData <span>}来自'../ models/student'; </span><span>从'express'导入{请求,响应}; </span><span>从'../middleware/permit'导入{checkusertopermitstudents}; </span> <span>导出异步函数createStudent(req:request,res:reverse):Promise <void> { </void></span><span>尝试 { </span><span>const {firstName,lastName,性别,className,age,createMail}:studentData = req.body; </span> <span>如果(!['girl','boy']。包括(性别)){ </span> res <span>.status(400).json({error:'无效性别类型'}); </span><span>返回; </span><span>} </span> <span>const iSpernits =等待checkusertopermitstudents(creatsoremail,“创建”,“学生”); </span><span>如果(! </span> res <span>.status(403).json({消息:'未授权'}); </span><span>返回; </span><span>} </span> <span>const newstudent =等待createStudentIndb({ </span> 名<span>, </span> 姓<span>, </span> 性别<span>, </span> className <span>, </span> 年龄<span>, </span> 创造力 <span>}); </span> res <span>.Status(201).JSON(NEWSTUDENT); </span><span>} catch(错误){ </span> res <span>.status(500).json({error :( erry as任何时候).message}); </span><span>} </span><span>} </span> <span>//获取所有学生 </span><span>导出异步函数提取器(req:request,res:reverse):Promise <void> { </void></span><span>尝试 { </span><span>const {email} = req.params; </span> <span>const iSpernits =等待checkusertopermitstudents(电子邮件,“读”,“学生”); </span><span>如果(! </span> res <span>.status(403).json({消息:'未授权'}); </span><span>返回; </span><span>} </span> <span>const学生=等待fetchstudents fromdb(); </span> Res <span>.Status(200).Json(学生); </span><span>} catch(错误){ </span> res <span>.status(500).json({error :( erry as任何时候).message}); </span><span>} </span><span>}</span>
该文件定义用于创建和获取学生的控制器,与数据库集成并允许进行授权检查。 CreateStudent Controller验证输入,检查权限并创建新学生,而获取研究员控制器在验证访问后会检索所有学生。两个控制器都处理错误并返回适当的响应。
- 创建一个profile.ts文件并粘贴以下代码:
<span>导入{profile}来自'@/models/profile'; </span><span>从“ Axios”导入Axios; </span><span>从'../config/appwrite'import {数据库,id,query}; </span><span>导入{请求,响应,next功能,requestHandler}来自'express'; </span><span>从'../config/environment'导入{puls_api_key}; </span> <span>const profileId = process.env.appwrite_profile_collection_id as String; //确保这是.env </span><span>const databaseId = process.env.appwrite_database_id as String; //确保这是.env </span><span>const projectId = process.env.permit_project_id as String </span><span>const Environment = process.env.permit_env_id作为字符串 </span> <span>const plase_api_url = <span>`https://api.permit.io/v2/facts/ <span>$ {projectID}</span> / <span>$ {emoventionId}</span> /users`</span> ; </span><span>const plass_auth_header = { </span> 授权<span>: <span>`bearer <span>$ {plum_api_key}</span> `</span> , </span><span>“ content-type”:“ application/json”, </span><span>}; </span> <span>//创建个人资料控制器 </span><span>导出const constrecrofile:requestHandler = async(req:request,res:respeart of stempter:reverse,next:next function):Promise <void> => { </void></span><span>const {firstName,lastName,email,cool,userId} = req.body; </span><span>console.log(req.body); </span> <span>if(!email ||!角色||!userId){ </span> res <span>.status(400).json({错误:'firstName,lastname,email,cool和userId是必需的。'}); </span><span>返回; </span><span>} </span> <span>//验证角色 </span><span>const允许的:profile ['remo'] [] = ['admin','老师','student']; </span><span>if(!washeRoles.cimludes(cole)){ </span> res <span>.status(400).json({错误:'无效角色。 </span><span>返回; </span><span>} </span> <span>尝试 { </span><span>const newuser =等待数据库。 </span> 数据库<span>, </span> profileid <span>, </span><span>id.unique(), </span><span>{firstName,lastName,电子邮件,角色,用户iD} </span><span>); </span><span>//步骤2:同步用户允许oio </span><span>const允许载荷= { </span> 钥匙<span>:电子邮件, </span> 电子邮件<span>, </span> first_name <span>:firstName, </span> last_name <span>:lastname, </span> 角色_ASSIGNMENTS <span>:[{{角色,租户:“默认”}], </span><span>}; </span> <span>让允许回答; </span><span>尝试 { </span><span>const响应=等待axios.post(允许_api_url,periverpayload,{标题:pers_auth_header}); </span> periverResponse <span>= wendesp.data; </span><span>console.log(“用户同步到允许。 </span><span>} catch(periverError){ </span><span>if(axios.isaxioserror(periverError)){ </span><span>Console.Error(“无法同步用户允许。 </span><span>} 别的 { </span><span>Console.Error(“未能同步用户允许。 </span><span>} </span> puroperresponse <span>= {error:“无法与许可证同步”}; </span><span>} </span> <span>//步骤3:返回两个回复 </span> res <span>.status(201).json({ </span> 消息<span>:“成功创建的用户配置文件”, </span> 用户<span>:Newuser, </span> 许可证<span>:许可证, </span><span>}); </span><span>返回; </span><span>} catch(错误:任何){ </span> res <span>.status(500).json({成功:false,消息:error.message}); </span><span>返回; </span><span>} </span><span>}; </span> <span>//通过电子邮件获取个人资料 </span><span>导出const const getProfileByeMail = async(req:request,res:ress:reverse,next:next function):Promise <void> => { </void></span><span>const {email} = req.params; </span> <span>如果(!email){ </span> res <span>.status(400).json({错误:'电子邮件是必需的。'}); </span><span>返回; </span><span>} </span> <span>尝试 { </span><span>const profile =等待数据库。listDocuments( </span> 数据库<span>, </span> profileid <span>, </span><span>[query.equal(“电子邮件”,电子邮件)] </span><span>); </span> <span>if(profile.documents.length === 0){ </span> res <span>.status(404).json({error:'profile找不到'}); </span><span>返回; </span><span>} </span> res <span>.status(200).json({成功:true,profile:profile.documents [0]}); </span><span>} catch(错误:任何){ </span><span>Console.Error('错误获取配置文件:',错误); </span> res <span>.status(500).json({成功:false,消息:error.message}); </span><span>} </span><span>};</span>
该文件定义了用于创建和获取用户配置文件的控制器,与AppWrite集成以进行数据库操作,并允许角色同步。 CreateProfile控制器验证输入,创建配置文件并同步用户允许,而GetProfileByemail Controller通过电子邮件检索配置文件。两个控制器都处理错误并返回适当的响应。
- 配置文件夹
- 创建AppWrite.ts文件并粘贴以下代码:
<span>导入{客户端,帐户,数据库,存储,ID,权限,角色,查询}来自'appWrite'; </span><span>导入{appwrite_endpoint,appwrite_project_id,appwrite_api_key}来自'./environment'; </span> <span>//初始化appwrite客户端 </span><span>const客户端=新客户端() </span><span>.setEndpoint(AppWrite_Endpoint)// AppWrite Endpoint </span><span>.setProject(appwrite_project_id); // AppWrite项目ID </span> <span>//添加API密钥(如果可用)(用于服务器端操作) </span><span>if(appwrite_api_key){ </span><span>(client as not).config.key = appwrite_api_key; //解决方案以设置API密钥 </span><span>} </span> <span>//初始化appwrite服务 </span><span>const帐户=新帐户(客户端); </span><span>const数据库=新数据库(客户端); </span><span>const Storage =新存储(客户端); </span> <span>//导出AppWrite客户端和服务 </span><span>导出{客户端,帐户,数据库,存储,ID,权限,角色,查询};</span>
此文件初始化并使用项目端点,ID和可选的API密钥配置AppWrite客户端。它还设置并导出AppWrite服务(例如帐户,数据库和存储),以及ID,权限,角色和查询等实用程序常数。
- 创建环境.ts文件并粘贴以下代码:
<span>从“ dotenv”导入dotenv; </span>dotenv <span>.config(); //来自.env的加载环境变量 </span> <span>导出const appwrite_endpoint = process.env.appwrite_endpoint || ''; </span><span>导出const plus_api_key = process.env.permit_api_key || ''; </span><span>导出const const_project_id = process.env.permit_project_id || ''; </span><span>导出const plass_env_id = process.env.permit_env_id || ''; </span><span>导出const appwrite_project_id = process.env.appwrite_project_id || ''; </span><span>导出const database_id = process.env.appwrite_database_id || ''; </span><span>导出const susident_collection_id = process.env.appwrite_students_collection_id || ''; </span><span>导出const signtments_collection_id = process.env.appwrite_assignments_collection_id || ''; </span> <span>导出const profile_collection_id = process.env.appwrite_profile_collection_id || '';</span>
该文件从.env文件加载环境变量,并将其作为用于应用程序中的常数,例如AppWrite和允许配置,数据库ID和Collection ID。如果未设置环境变量,则将默认值提供为后备。
- API文件夹
- 创建student.ts并粘贴以下代码:
<span>从“ Express”导入Express; </span><span>从'../controllers/student'导入{createStudent,fetchstudents}; </span><span>从“ ../ middleware/auth”导入authmiddleware; </span> <span>const router = express.router(); </span> <span>//定义与学生相关的终点 </span>路由器<span>.post('/students',authmiddleware,createStudent); //创建一个新学生 </span>Router <span>.get('/student/:email',authmiddleware,fetchStudents); //获取所有学生 </span><span>导出默认路由器; //导出路由器实例</span>
该文件设置了一个带有用于管理学生数据的端点的明确路由器。它包括用于创建新学生和获取学生的路线,均受身份验证中间件(authmiddleware)保护。然后将路由器导出以用于应用程序。
- 创建auth.ts文件并粘贴以下代码:
<span>// src/routes/authRoutes.ts </span><span>从“ Express”导入Express; </span><span>从'../controllers/auth'; </span> <span>const router = express.router(); </span> <span>//定义与验证相关的终点 </span>路由器<span>.post('//impost',(req,res,next)=> {//注册路由 </span><span>Indip(req,res)。然后(()=> { </span><span>下一个(); </span><span>})。catch((err)=> { </span><span>下一个(err); </span><span>}); </span><span>}); </span>路由器<span>.post('/login',(req,res,sext)=> {//登录路由 </span><span>登录(req,res)。然后(()=> { </span><span>下一个(); </span><span>})。catch((err)=> { </span><span>下一个(err); </span><span>}); </span><span>}); </span>路由器<span>.post('/logout',logout); //注销路线 </span><span>导出默认路由器; //导出路由器实例</span>
该文件设置了一个带有端点的快速路由器,用于与身份验证相关的操作,包括用户注册,登录和注销。注册和登录路由可以使用错误处理的异步操作,而注销路由很简单。路由器已导出以用于应用程序。
- 创建sizhtment.ts文件并粘贴以下代码:
<span>从“ Express”导入Express </span><span>从“ ../controllers/assignment”导入{createSignment,fetchAssignments} </span><span>从“ ../middleware/auth”导入authmiddleware </span> <span>const router = express.router() </span> 路由器<span>.post(“/create”,authmiddleware,createSignment) </span>路由器<span>.get(“/:电子邮件”,authmiddleware,fetchAssignments) </span><span>导出默认路由器</span>
该文件设置了一个带有用于管理作业的端点的快线路由器。它包括用于创建分配和获取作业的路由,均受身份验证中间件(authmiddleware)保护。路由器已导出以用于应用程序。
- 创建profile.ts文件并粘贴以下代码:
<span>从“ Express”导入Express; </span><span>从'../controllers/profile'import {createProfile,getProfileByemail}; </span><span>从“ ../ middleware/auth”导入authmiddleware; </span> <span>const router = express.router(); </span> <span>//创建个人资料的路线 </span>路由器<span>.post('/profile',authmiddleware,createProfile); </span> <span>//通过电子邮件获取个人资料的路线 </span>路由器<span>.get('/profile/:email',authmiddleware,getProfileByeMail); </span><span>导出默认路由器;</span>
This file sets up an Express router with endpoints for managing user profiles. It includes routes for creating a profile and fetching a profile by email, both protected by an authentication middleware (authMiddleware). The router is exported for use in the application.
- Create index.ts file and paste the following code:
<span>import express, { Request, Response } from 'express'; </span><span>import dotenv from 'dotenv'; </span><span>import cors from 'cors'; // CORS middleware </span><span>import authRoutes from './auth'; // Import auth routes </span><span>import profileRoutes from './profile'; </span><span>import studentRoutes from './student'; </span><span>import assignmentRoutes from './assignment'; </span><span>import { errorHandler } from '../utils/errorHandler'; // Custom error handler middleware </span> dotenv <span>.config(); // Load environment variables from .env file </span> <span>const app = express(); </span><span>const PORT = process.env.PORT || 8080; </span> <span>// 中间件 </span>app <span>.use(cors()); // Handle CORS </span>app <span>.use(express.json()); /// Parse incoming JSON requests </span> <span>// Routes </span>app <span>.use('/api/auth', authRoutes); // Authentication routes </span>app <span>.use('/api', profileRoutes); // Profile routes mounted </span>app <span>.use('/api', studentRoutes); // Student routes mounted </span>app <span>.use('/api/assignments', assignmentRoutes); // Assignment routes mounted </span> <span>// Global Error Handling Middleware </span>app <span>.use(errorHandler); // Handle errors globally </span> <span>// Default Route </span>app <span>.get('/', (req: Request, res: Response) => { </span> res <span>.send('Appwrite Express API'); </span><span>}); </span> <span>// Start Server </span>app <span>.listen(PORT, () => { </span><span>console.log( <span>`Server is running on port <span>${PORT}</span> `</span> ); </span><span>}); </span><span>export default app;</span>
This file sets up an Express server, configuring middleware like CORS and JSON parsing, and mounts routes for authentication, profiles, students, and assignments. It includes a global error handler and a default route to confirm the server is running. The server listens on a specified port, logs its status, and exports the app instance for further use.
- Finally, to run this project, change a part of package.json and install the following packages below so when you run npm run dev, it works.
- Install packages:
npm install concurrently ts-node nodemon --save-dev
- By updating the scripts in the package.json, when you start the server, the typescript files are compiled to JavaScript in a new folder that is automatically created called dist
"scripts": { "dev": "concurrently \"tsc --watch\" \"nodemon -q --watch src --ext ts --exec ts-node src/api/index.ts\"", "build": "tsc", "start": "node ./dist/api/index.js" },,
Now run npm run dev to start your server. When you see this message, it means that you have successfully implemented the backend.

Congratulations, your backend is ready for requests.
Now that our backend is set up, move on to frontend integration, where you'll:
- Secure API requests from Next.js
- Dynamically show/hide UI elements based on user permissions.
Reason for creating an extensive backend service using Appwrite
Appwrite is often described as a backend-as-a-service (BaaS) solution, meaning it provides ready-made backend functionality like authentication, database management, and storage without requiring developers to build a traditional backend.
However, for this project, I needed more flexibility and control over how data was processed, secured, and structured, which led me to create an extensive custom backend using Node.js and Express while still leveraging Appwrite's services.
Instead of relying solely on Appwrite's built-in API calls from the frontend, I designed a Node.js backend that acted as an intermediary between the frontend and Appwrite. This allowed me to:
- Implement fine-grained access control with Permit.io before forwarding requests to Appwrite.
- Structure API endpoints for multi-tenancy to ensure tenant-specific data isolation.
- Create custom business logic, such as processing role-based actions before committing them to the Appwrite database.
- Maintain a centralized API layer, making it easier to enforce security policies, log activities, and scale the application.
Appwrite provided the core authentication and database functionality of this application, but this additional backend layer enhanced security, flexibility, and maintainability, to ensure strict access control before any action reached Appwrite.
结论
That's it for part one of this article series. In part 2, we'll handle the frontend integration by setting up API calls with authorization, initializing and installing necessary dependencies, writing out the component file codes, and handling state management & routes.
以上是使用Next.js(后端集成)构建多租户SaaS应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

JavaScript在现实世界中的应用包括服务器端编程、移动应用开发和物联网控制:1.通过Node.js实现服务器端编程,适用于高并发请求处理。2.通过ReactNative进行移动应用开发,支持跨平台部署。3.通过Johnny-Five库用于物联网设备控制,适用于硬件交互。

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。 1.Python以简洁语法和丰富库生态着称,适用于数据分析和Web开发。 2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

SecLists
SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具

适用于 Eclipse 的 SAP NetWeaver 服务器适配器
将Eclipse与SAP NetWeaver应用服务器集成。

DVWA
Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)