秘诀示例:自定义政策
¥Examples cookbook: Custom policies
此页面是后端定制示例手册的一部分。请确保你已阅读其 introduction。
¥This page is part of the back end customization examples cookbook. Please ensure you've read its introduction.
FoodAdvisor 开箱即用,不使用任何可以控制对内容类型端点的访问的自定义策略或路由中间件。
¥Out of the box, FoodAdvisor does not use any custom policies or route middlewares that could control access to content type endpoints.
在 Strapi 中,可以使用策略或路由中间件来控制对内容类型端点的访问:
¥In Strapi, controlling access to a content-type endpoint can be done either with a policy or route middleware:
-
策略是只读的,允许请求传递或返回错误,
¥policies are read-only and allow a request to pass or return an error,
-
而路由中间件可以执行额外的逻辑。
¥while route middlewares can perform additional logic.
在我们的示例中,我们使用一个策略。
¥In our example, let's use a policy.
创建自定义策略
¥Creating a custom policy
💭 上下文:
¥💭 Context:
假设我们想要定制 FoodAdvisor 的后端,以防止餐馆老板在前端网站上使用 先前创建的表单 为其业务创建虚假评论。
¥Let's say we would like to customize the backend of FoodAdvisor to prevent restaurant owners from creating fake reviews for their businesses using a form previously created on the front-end website.
🎯 目标:
¥🎯 Goals:
-
为策略创建一个新文件夹以仅应用于 "评论" 集合类型。
¥Create a new folder for policies to apply only to the "Reviews" collection type.
-
创建新的策略文件。
¥Create a new policy file.
-
当到达
/reviews
端点时,使用实体服务 API 中的findMany()
方法获取有关餐厅所有者的信息。¥Use the
findMany()
method from the Entity Service API to get information about the owner of a restaurant when the/reviews
endpoint is reached. -
如果经过身份验证的用户是餐厅的所有者,则返回错误,或者在其他情况下让请求通过。
¥Return an error if the authenticated user is the restaurant's owner, or let the request pass in other cases.
🧑💻 代码示例:
¥🧑💻 Code example:
在 FoodAdvisor 项目的 /api
文件夹中,新建一个 src/api/review/policies/is-owner-review.js
文件,代码如下:
¥In the /api
folder of the FoodAdvisor project, create a new src/api/review/policies/is-owner-review.js
file with the following code:
module.exports = async (policyContext, config, { strapi }) => {
const { body } = policyContext.request;
const { user } = policyContext.state;
// Return an error if there is no authenticated user with the request
if (!user) {
return false;
}
/**
* Queries the Restaurants collection type
* using the Entity Service API
* to retrieve information about the restaurant's owner.
*/
const [restaurant] = await strapi.entityService.findMany(
'api::restaurant.restaurant',
{
filters: {
slug: body.restaurant,
},
populate: ['owner'],
}
);
if (!restaurant) {
return false;
}
/**
* If the user submitting the request is the restaurant's owner,
* we don't allow the review creation.
*/
if (user.id === restaurant.owner.id) {
return false;
}
return true;
};
应在路由配置中声明策略或路由中间件以实际控制访问。阅读有关 参考文档 中路由的更多信息或查看 路由秘诀 中的示例。
¥Policies or route middlewares should be declared in the configuration of a route to actually control access. Read more about routes in the reference documentation or see an example in the routes cookbook.
通过策略发送自定义错误
¥Sending custom errors through policies
💭 上下文:
¥💭 Context:
当策略拒绝访问路由时,FoodAdvisor 会立即发送默认错误。假设我们想要自定义当 之前创建的自定义策略 不允许创建评论时发送的错误。
¥Out of the box, FoodAdvisor sends a default error when a policy refuses access to a route. Let's say we want to customize the error sent when the previously created custom policy does not allow creating a review.
🎯 目标:
¥🎯 Goal:
配置自定义策略以引发自定义错误而不是默认错误。
¥Configure the custom policy to throw a custom error instead of the default error.
其他信息可在 错误处理 文档中找到。
¥Additional information can be found in the Error handling documentation.
🧑💻 代码示例:
¥🧑💻 Code example:
在 FoodAdvisor 项目的 /api
文件夹中,如下更新 之前创建的 is-owner-review
自定义策略(高亮的行是唯一修改的行):
¥In the /api
folder of the FoodAdvisor project, update the previously created is-owner-review
custom policy as follows (highlighted lines are the only modified lines):
const { errors } = require('@strapi/utils');
const { PolicyError } = errors;
module.exports = async (policyContext, config, { strapi }) => {
const { body } = policyContext.request;
const { user } = policyContext.state;
// Return an error if there is no authenticated user with the request
if (!user) {
return false;
}
/**
* Queries the Restaurants collection type
* using the Entity Service API
* to retrieve information about the restaurant's owner.
*/
const filteredRestaurants = await strapi.entityService.findMany(
'api::restaurant.restaurant',
{
filters: {
slug: body.restaurant,
},
populate: ['owner'],
}
);
const restaurant = filteredRestaurants[0];
if (!restaurant) {
return false;
}
/**
* If the user submitting the request is the restaurant's owner,
* we don't allow the review creation.
*/
if (user.id === restaurant.owner.id) {
/**
* Throws a custom policy error
* instead of just returning false
* (which would result into a generic Policy Error).
*/
throw new PolicyError('The owner of the restaurant cannot submit reviews', {
errCode: 'RESTAURANT_OWNER_REVIEW', // can be useful for identifying different errors on the front end
});
}
return true;
};
Responses sent with default policy error vs. custom policy error:
- Default error response
- Custom error response
当策略拒绝访问路由并引发默认错误时,尝试通过 REST API 查询内容类型时将发送以下响应:
¥When a policy refuses access to a route and a default error is thrown, the following response will be sent when trying to query the content-type through the REST API:
{
"data": null,
"error": {
"status": 403,
"name": "PolicyError",
"message": "Policy Failed",
"details": {}
}
}
当策略拒绝访问路由并且自定义策略抛出上面代码示例中定义的自定义错误时,尝试通过 REST API 查询内容类型时将发送以下响应:
¥When a policy refuses access to a route and the custom policy throws the custom error defined in the code example above, the following response will be sent when trying to query the content-type through the REST API:
{
"data": null,
"error": {
"status": 403,
"name": "PolicyError",
"message": "The owner of the restaurant cannot submit reviews",
"details": {
"policy": "is-owner-review",
"errCode": "RESTAURANT_OWNER_REVIEW"
}
}
}
在前端使用自定义错误
¥Using custom errors on the front end
💭 上下文:
¥💭 Context:
开箱即用,FoodAdvisor 提供的 Next.js 支持的前端网站在访问内容时不会在前端网站上显示错误或成功消息。例如,当不可能添加带有 先前创建的表格 的新评论时,网站不会通知用户。
¥Out of the box, the Next.js-powered front-end website provided with FoodAdvisor does not display errors or success messages on the front-end website when accessing content. For instance, the website will not inform the user when adding a new review with a previously created form is not possible.
假设我们想要自定义 FoodAdvisor 的前端来捕获 之前创建的自定义策略 抛出的自定义错误,并将其显示给使用 反应热吐司通知 的用户。作为奖励,成功创建评论后将显示另一个 Toast 通知。
¥Let's say we want to customize the front end of FoodAdvisor to catch the custom error thrown by a previously created custom policy and display it to the user with a React Hot Toast notification. As a bonus, another toast notification will be displayed when a review is successfully created.
🎯 目标:
¥🎯 Goals:
-
在前端网站上捕获错误并将其显示在通知中。
¥Catch the error on the front-end website and display it within a notification.
-
如果政策允许创建新评论,请发送另一条通知。
¥Send another notification in case the policy allows the creation of a new review.
🧑💻 代码示例:
¥🧑💻 Code example:
在 FoodAdvisor 项目的 /client
文件夹中,你可以按如下方式更新 先前创建的 new-review
组件(修改的行高亮):
¥In the /client
folder of the FoodAdvisor project, you could update the previously created new-review
component as follows (modified lines are highlighted):
Example front-end code to display toast notifications for custom errors or successful review creation:
import { Button, Input, Textarea } from '@nextui-org/react';
import { useFormik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { getStrapiURL } from '../../../../../utils';
/**
* A notification will be displayed on the front-end using React Hot Toast
* (See https://github.com/timolins/react-hot-toast).
* React Hot Toast should be added to your project's dependencies;
* Use yarn or npm to install it and it will be added to your package.json file.
*/
import toast from 'react-hot-toast';
class UnauthorizedError extends Error {
constructor(message) {
super(message);
}
}
const NewReview = () => {
const router = useRouter();
const { handleSubmit, handleChange, values } = useFormik({
initialValues: {
note: '',
content: '',
},
onSubmit: async (values) => {
/**
* The previously added code is wrapped in a try/catch block.
*/
try {
const res = await fetch(getStrapiURL('/reviews'), {
method: 'POST',
body: JSON.stringify({
restaurant: router.query.slug,
...values,
}),
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
},
});
const { data, error } = await res.json();
/**
* If the Strapi backend server returns an error,
* we use the custom error message to throw a custom error.
* If the request is a success, we display a success message.
* In both cases, a toast notification is displayed on the front-end.
*/
if (error) {
throw new UnauthorizedError(error.message);
}
toast.success('Review created!');
return data;
} catch (err) {
toast.error(err.message);
console.error(err);
}
},
});
return (
<div className="my-6">
<h1 className="font-bold text-2xl mb-3">Write your review</h1>
<form onSubmit={handleSubmit} className="flex flex-col gap-y-4">
<Input
onChange={handleChange}
name="note"
type="number"
min={1}
max={5}
label="Stars"
/>
<Textarea
name="content"
onChange={handleChange}
placeholder="What do you think about this restaurant?"
/>
<Button
type="submit"
className="bg-primary text-white rounded-md self-start"
>
Send
</Button>
</form>
</div>
);
};
export default NewReview;
详细了解如何配置 定制路由 以使用自定义策略,以及如何使用这些自定义路由来调整基于 Strapi 的应用。
¥Learn more about how to configure custom routes to use your custom policies, and how these custom routes can be used to tweak a Strapi-based application.