feat: reserve accesses for ca or notification

This commit is contained in:
Fu Diwei 2025-04-27 11:41:09 +08:00
parent 193a19b79c
commit e533f9407f
17 changed files with 166 additions and 71 deletions

View File

@ -1098,7 +1098,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
deployer, err := pWebhook.NewDeployer(&pWebhook.DeployerConfig{ deployer, err := pWebhook.NewDeployer(&pWebhook.DeployerConfig{
WebhookUrl: access.Url, WebhookUrl: access.Url,
WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.TemplateDataForDeployment), WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.DefaultDataForDeployment),
Method: access.Method, Method: access.Method,
Headers: mergedHeaders, Headers: mergedHeaders,
AllowInsecureConnections: access.AllowInsecureConnections, AllowInsecureConnections: access.AllowInsecureConnections,

View File

@ -11,6 +11,7 @@ type Access struct {
Name string `json:"name" db:"name"` Name string `json:"name" db:"name"`
Provider string `json:"provider" db:"provider"` Provider string `json:"provider" db:"provider"`
Config map[string]any `json:"config" db:"config"` Config map[string]any `json:"config" db:"config"`
Reserve string `json:"reserve,omitempty" db:"reserve"`
DeletedAt *time.Time `json:"deleted" db:"deleted"` DeletedAt *time.Time `json:"deleted" db:"deleted"`
} }
@ -265,8 +266,8 @@ type AccessConfigForWebhook struct {
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
HeadersString string `json:"headers,omitempty"` HeadersString string `json:"headers,omitempty"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
TemplateDataForDeployment string `json:"templateDataForDeployment,omitempty"` // TODO: DefaultDataForDeployment string `json:"defaultDataForDeployment,omitempty"`
TemplateDataForNotification string `json:"templateDataForNotification,omitempty"` // TODO: DefaultDataForNotification string `json:"defaultDataForNotification,omitempty"`
} }
type AccessConfigForWestcn struct { type AccessConfigForWestcn struct {

View File

@ -101,7 +101,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{ return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: access.Url, WebhookUrl: access.Url,
WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.TemplateDataForNotification), WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.DefaultDataForNotification),
Method: access.Method, Method: access.Method,
Headers: mergedHeaders, Headers: mergedHeaders,
AllowInsecureConnections: access.AllowInsecureConnections, AllowInsecureConnections: access.AllowInsecureConnections,

View File

@ -80,7 +80,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse webhook url: %w", err) return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" { } else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme: %s", webhookUrl.Scheme) return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme)
} else { } else {
webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName)) webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName))
} }
@ -94,7 +94,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
webhookMethod != http.MethodPut && webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch && webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete { webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method: %s", webhookMethod) return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
} }
// 处理 Webhook 请求标头 // 处理 Webhook 请求标头
@ -114,7 +114,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) && } else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) { strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type: %s", webhookContentType) return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
} }
// 处理 Webhook 请求数据 // 处理 Webhook 请求数据

View File

@ -73,7 +73,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse webhook url: %w", err) return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" { } else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme: %s", webhookUrl.Scheme) return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme)
} }
// 处理 Webhook 请求谓词 // 处理 Webhook 请求谓词
@ -85,7 +85,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
webhookMethod != http.MethodPut && webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch && webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete { webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method: %s", webhookMethod) return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
} }
// 处理 Webhook 请求标头 // 处理 Webhook 请求标头
@ -105,7 +105,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) && } else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) { strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type: %s", webhookContentType) return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
} }
// 处理 Webhook 请求数据 // 处理 Webhook 请求数据

View File

@ -53,6 +53,7 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces
Name: record.GetString("name"), Name: record.GetString("name"),
Provider: record.GetString("provider"), Provider: record.GetString("provider"),
Config: config, Config: config,
Reserve: record.GetString("reserve"),
} }
return access, nil return access, nil
} }

View File

@ -0,0 +1,88 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// update collection `access`
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "text2859962647",
"max": 0,
"min": 0,
"name": "reserve",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
}
// migrate data
{
accesses, err := app.FindAllRecords("access")
if err != nil {
return err
}
for _, access := range accesses {
changed := false
if access.GetString("provider") == "buypass" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "googletrustservices" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "sslcom" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "zerossl" {
access.Set("reserve", "ca")
changed = true
}
if access.GetString("provider") == "webhook" {
config := make(map[string]any)
if err := access.UnmarshalJSONField("config", &config); err != nil {
return err
}
config["method"] = "POST"
config["headers"] = "Content-Type: application/json"
access.Set("config", config)
changed = true
}
if changed {
err = app.Save(access)
if err != nil {
return err
}
}
}
}
return nil
}, func(app core.App) error {
return nil
})
}

View File

@ -14,14 +14,14 @@ export type AccessEditDrawerProps = {
data?: AccessFormProps["initialValues"]; data?: AccessFormProps["initialValues"];
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
range?: AccessFormProps["range"];
scene: AccessFormProps["scene"]; scene: AccessFormProps["scene"];
trigger?: React.ReactNode; trigger?: React.ReactNode;
usage?: AccessFormProps["usage"];
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void; afterSubmit?: (record: AccessModel) => void;
}; };
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => { const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditDrawerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -109,7 +109,7 @@ const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, .
width={720} width={720}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
> >
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} /> <AccessForm ref={formRef} initialValues={data} scene={scene === "add" ? "add" : "edit"} usage={usage} />
</Drawer> </Drawer>
</> </>
); );

View File

@ -14,14 +14,14 @@ export type AccessEditModalProps = {
data?: AccessFormProps["initialValues"]; data?: AccessFormProps["initialValues"];
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
range?: AccessFormProps["range"]; usage?: AccessFormProps["usage"];
scene: AccessFormProps["scene"]; scene: AccessFormProps["scene"];
trigger?: React.ReactNode; trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void; afterSubmit?: (record: AccessModel) => void;
}; };
const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => { const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -105,7 +105,7 @@ const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ..
onCancel={handleCancelClick} onCancel={handleCancelClick}
> >
<div className="pb-2 pt-4"> <div className="pb-2 pt-4">
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} /> <AccessForm ref={formRef} initialValues={data} scene={scene === "add" ? "add" : "edit"} usage={usage} />
</div> </div>
</Modal> </Modal>
</> </>

View File

@ -61,16 +61,16 @@ import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig"; import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>; type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
type AccessFormScenes = "add" | "edit"; type AccessFormScenes = "add" | "edit";
type AccessFormUsages = "both-dns-hosting" | "ca-only" | "notification-only";
export type AccessFormProps = { export type AccessFormProps = {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
disabled?: boolean; disabled?: boolean;
initialValues?: AccessFormFieldValues; initialValues?: AccessFormFieldValues;
range?: AccessFormRanges;
scene: AccessFormScenes; scene: AccessFormScenes;
usage?: AccessFormUsages;
onValuesChange?: (values: AccessFormFieldValues) => void; onValuesChange?: (values: AccessFormFieldValues) => void;
}; };
@ -80,7 +80,7 @@ export type AccessFormInstance = {
validateFields: FormInstance<AccessFormFieldValues>["validateFields"]; validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
}; };
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => { const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, usage, scene, onValuesChange }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const formSchema = z.object({ const formSchema = z.object({
@ -91,13 +91,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
.trim(), .trim(),
provider: z.nativeEnum(ACCESS_PROVIDERS, { provider: z.nativeEnum(ACCESS_PROVIDERS, {
message: message:
range === "ca-only" usage === "ca-only"
? t("access.form.certificate_authority.placeholder") ? t("access.form.certificate_authority.placeholder")
: range === "notify-only" : usage === "notification-only"
? t("access.form.notification_channel.placeholder") ? t("access.form.notification_channel.placeholder")
: t("access.form.provider.placeholder"), : t("access.form.provider.placeholder"),
}), }),
config: z.any(), config: z.any(),
reserve: z.string().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({ const { form: formInst, formProps } = useAntdForm({
@ -105,33 +106,33 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
}); });
const providerLabel = useMemo(() => { const providerLabel = useMemo(() => {
switch (range) { switch (usage) {
case "ca-only": case "ca-only":
return t("access.form.certificate_authority.label"); return t("access.form.certificate_authority.label");
case "notify-only": case "notification-only":
return t("access.form.notification_channel.label"); return t("access.form.notification_channel.label");
} }
return t("access.form.provider.label"); return t("access.form.provider.label");
}, [range]); }, [usage]);
const providerPlaceholder = useMemo(() => { const providerPlaceholder = useMemo(() => {
switch (range) { switch (usage) {
case "ca-only": case "ca-only":
return t("access.form.certificate_authority.placeholder"); return t("access.form.certificate_authority.placeholder");
case "notify-only": case "notification-only":
return t("access.form.notification_channel.placeholder"); return t("access.form.notification_channel.placeholder");
} }
return t("access.form.provider.placeholder"); return t("access.form.provider.placeholder");
}, [range]); }, [usage]);
const providerTooltip = useMemo(() => { const providerTooltip = useMemo(() => {
switch (range) { switch (usage) {
case "both-dns-hosting": case "both-dns-hosting":
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>; return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
} }
return undefined; return undefined;
}, [range]); }, [usage]);
const fieldProvider = Form.useWatch("provider", formInst); const fieldProvider = Form.useWatch("provider", formInst);
@ -269,6 +270,7 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
getFieldsValue: () => { getFieldsValue: () => {
const values = formInst.getFieldsValue(true); const values = formInst.getFieldsValue(true);
values.config = nestedFormInst.getFieldsValue(); values.config = nestedFormInst.getFieldsValue();
values.reserve = usage === "ca-only" ? "ca" : usage === "notification-only" ? "notification" : undefined;
return values; return values;
}, },
resetFields: (fields) => { resetFields: (fields) => {
@ -297,20 +299,20 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}> <Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
<AccessProviderSelect <AccessProviderSelect
filter={(record) => { filter={(record) => {
if (range == null) return true; if (usage == null) return true;
switch (range) { switch (usage) {
case "both-dns-hosting": case "both-dns-hosting":
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING); return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
case "ca-only": case "ca-only":
return record.usages.includes(ACCESS_USAGES.CA); return record.usages.includes(ACCESS_USAGES.CA);
case "notify-only": case "notification-only":
return record.usages.includes(ACCESS_USAGES.NOTIFICATION); return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
} }
}} }}
disabled={scene !== "add"} disabled={scene !== "add"}
placeholder={providerPlaceholder} placeholder={providerPlaceholder}
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)} showOptionTags={usage == null || (usage === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showSearch={!disabled} showSearch={!disabled}
/> />
</Form.Item> </Form.Item>

View File

@ -352,7 +352,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</div> </div>
<div className="text-right"> <div className="text-right">
<AccessEditModal <AccessEditModal
range="both-dns-hosting"
scene="add" scene="add"
trigger={ trigger={
<Button size="small" type="link"> <Button size="small" type="link">
@ -360,6 +359,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<PlusOutlinedIcon className="text-xs" /> <PlusOutlinedIcon className="text-xs" />
</Button> </Button>
} }
usage="both-dns-hosting"
afterSubmit={(record) => { afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) { if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
@ -374,6 +374,8 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<Form.Item name="providerAccessId" rules={[formRule]}> <Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect <AccessSelect
filter={(record) => { filter={(record) => {
if (record.reserve) return false;
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.DNS); return !!provider?.usages?.includes(ACCESS_USAGES.DNS);
}} }}
@ -429,7 +431,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<div className="text-right"> <div className="text-right">
<AccessEditModal <AccessEditModal
data={{ provider: caProvidersMap.get(fieldCAProvider!)?.provider }} data={{ provider: caProvidersMap.get(fieldCAProvider!)?.provider }}
range="ca-only"
scene="add" scene="add"
trigger={ trigger={
<Button size="small" type="link"> <Button size="small" type="link">
@ -437,6 +438,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<PlusOutlinedIcon className="text-xs" /> <PlusOutlinedIcon className="text-xs" />
</Button> </Button>
} }
usage="ca-only"
afterSubmit={(record) => { afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.CA)) { if (provider?.usages?.includes(ACCESS_USAGES.CA)) {
@ -450,9 +452,8 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<Form.Item name="caProviderAccessId" rules={[formRule]}> <Form.Item name="caProviderAccessId" rules={[formRule]}>
<AccessSelect <AccessSelect
filter={(record) => { filter={(record) => {
if (fieldCAProvider) { if (!!record.reserve && record.reserve !== "ca") return false;
return caProvidersMap.get(fieldCAProvider)?.provider === record.provider; if (fieldCAProvider) return caProvidersMap.get(fieldCAProvider)?.provider === record.provider;
}
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.CA); return !!provider?.usages?.includes(ACCESS_USAGES.CA);

View File

@ -409,7 +409,6 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<div className="text-right"> <div className="text-right">
<AccessEditModal <AccessEditModal
data={{ provider: deploymentProvidersMap.get(fieldProvider!)?.provider }} data={{ provider: deploymentProvidersMap.get(fieldProvider!)?.provider }}
range="both-dns-hosting"
scene="add" scene="add"
trigger={ trigger={
<Button size="small" type="link"> <Button size="small" type="link">
@ -417,6 +416,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<PlusOutlinedIcon className="text-xs" /> <PlusOutlinedIcon className="text-xs" />
</Button> </Button>
} }
usage="both-dns-hosting"
afterSubmit={(record) => { afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) { if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) {
@ -430,9 +430,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form.Item name="providerAccessId" rules={[formRule]}> <Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect <AccessSelect
filter={(record) => { filter={(record) => {
if (fieldProvider) { if (record.reserve) return false;
return deploymentProvidersMap.get(fieldProvider)?.provider === record.provider; if (fieldProvider) return deploymentProvidersMap.get(fieldProvider)?.provider === record.provider;
}
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING); return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);

View File

@ -228,7 +228,6 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</div> </div>
<div className="text-right"> <div className="text-right">
<AccessEditModal <AccessEditModal
range="notify-only"
scene="add" scene="add"
trigger={ trigger={
<Button size="small" type="link"> <Button size="small" type="link">
@ -236,6 +235,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
<PlusOutlinedIcon className="text-xs" /> <PlusOutlinedIcon className="text-xs" />
</Button> </Button>
} }
usage="notification-only"
afterSubmit={(record) => { afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION)) { if (provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION)) {
@ -250,6 +250,8 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
<Form.Item name="providerAccessId" rules={[formRule]}> <Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect <AccessSelect
filter={(record) => { filter={(record) => {
if (!!record.reserve && record.reserve !== "notification") return false;
const provider = accessProvidersMap.get(record.provider); const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION); return !!provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION);
}} }}

View File

@ -56,6 +56,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForWestcn | AccessConfigForWestcn
| AccessConfigForZeroSSL | AccessConfigForZeroSSL
); );
reserve?: "ca" | "notification";
} }
// #region AccessConfig // #region AccessConfig
@ -310,8 +311,8 @@ export type AccessConfigForWebhook = {
method: string; method: string;
headers?: string; headers?: string;
allowInsecureConnections?: boolean; allowInsecureConnections?: boolean;
templateDataForDeployment?: string; defaultDataForDeployment?: string;
templateDataForNotification?: string; defaultDataForNotification?: string;
}; };
export type AccessConfigForWestcn = { export type AccessConfigForWestcn = {

View File

@ -18,9 +18,9 @@
"access.props.provider.usage.ca": "Certificate authority", "access.props.provider.usage.ca": "Certificate authority",
"access.props.provider.usage.notification": "Notification channel", "access.props.provider.usage.notification": "Notification channel",
"access.props.provider.builtin": "Built-in", "access.props.provider.builtin": "Built-in",
"access.props.range.both_dns_hosting": "Provider", "access.props.usage.both_dns_hosting": "Provider",
"access.props.range.ca_only": "Certificate authority", "access.props.usage.ca_only": "Certificate authority",
"access.props.range.notify_only": "Notification channel", "access.props.usage.notification_only": "Notification channel",
"access.props.created_at": "Created at", "access.props.created_at": "Created at",
"access.props.updated_at": "Updated at", "access.props.updated_at": "Updated at",

View File

@ -18,9 +18,9 @@
"access.props.provider.usage.ca": "证书颁发机构", "access.props.provider.usage.ca": "证书颁发机构",
"access.props.provider.usage.notification": "通知渠道", "access.props.provider.usage.notification": "通知渠道",
"access.props.provider.builtin": "内置", "access.props.provider.builtin": "内置",
"access.props.range.both_dns_hosting": "提供商", "access.props.usage.both_dns_hosting": "提供商",
"access.props.range.ca_only": "证书颁发机构", "access.props.usage.ca_only": "证书颁发机构",
"access.props.range.notify_only": "通知渠道", "access.props.usage.notification_only": "通知渠道",
"access.props.created_at": "创建时间", "access.props.created_at": "创建时间",
"access.props.updated_at": "更新时间", "access.props.updated_at": "更新时间",

View File

@ -21,7 +21,7 @@ import { useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access"; import { useAccessesStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
type AccessRanges = AccessEditDrawerProps["range"]; type AccessUsageProp = AccessEditDrawerProps["usage"];
const AccessList = () => { const AccessList = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -87,7 +87,7 @@ const AccessList = () => {
<Space.Compact> <Space.Compact>
<AccessEditDrawer <AccessEditDrawer
data={record} data={record}
range={filters["range"] as AccessRanges} usage={filters["usage"] as AccessUsageProp}
scene="edit" scene="edit"
trigger={ trigger={
<Tooltip title={t("access.action.edit")}> <Tooltip title={t("access.action.edit")}>
@ -98,7 +98,7 @@ const AccessList = () => {
<AccessEditDrawer <AccessEditDrawer
data={{ ...record, id: undefined, name: `${record.name}-copy` }} data={{ ...record, id: undefined, name: `${record.name}-copy` }}
range={filters["range"] as AccessRanges} usage={filters["usage"] as AccessUsageProp}
scene="add" scene="add"
trigger={ trigger={
<Tooltip title={t("access.action.duplicate")}> <Tooltip title={t("access.action.duplicate")}>
@ -126,7 +126,7 @@ const AccessList = () => {
const [filters, setFilters] = useState<Record<string, unknown>>(() => { const [filters, setFilters] = useState<Record<string, unknown>>(() => {
return { return {
range: "both-dns-hosting" satisfies AccessRanges, usage: "both-dns-hosting" satisfies AccessUsageProp,
keyword: searchParams.get("keyword"), keyword: searchParams.get("keyword"),
}; };
}); });
@ -160,13 +160,13 @@ const AccessList = () => {
}) })
.filter((e) => { .filter((e) => {
const provider = accessProvidersMap.get(e.provider); const provider = accessProvidersMap.get(e.provider);
switch (filters["range"] as AccessRanges) { switch (filters["usage"] as AccessUsageProp) {
case "both-dns-hosting": case "both-dns-hosting":
return provider?.usages?.includes(ACCESS_USAGES.DNS) || provider?.usages?.includes(ACCESS_USAGES.HOSTING); return !e.reserve && (provider?.usages?.includes(ACCESS_USAGES.DNS) || provider?.usages?.includes(ACCESS_USAGES.HOSTING));
case "ca-only": case "ca-only":
return provider?.usages?.includes(ACCESS_USAGES.CA); return e.reserve === "ca" && provider?.usages?.includes(ACCESS_USAGES.CA);
case "notify-only": case "notification-only":
return provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION); return e.reserve === "notification" && provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION);
} }
}); });
return Promise.resolve({ return Promise.resolve({
@ -184,7 +184,7 @@ const AccessList = () => {
); );
const handleTabChange = (key: string) => { const handleTabChange = (key: string) => {
setFilters((prev) => ({ ...prev, range: key })); setFilters((prev) => ({ ...prev, usage: key }));
setPage(1); setPage(1);
}; };
@ -226,7 +226,7 @@ const AccessList = () => {
extra={[ extra={[
<AccessEditDrawer <AccessEditDrawer
key="create" key="create"
range={filters["range"] as AccessRanges} usage={filters["usage"] as AccessUsageProp}
scene="add" scene="add"
trigger={ trigger={
<Button type="primary" icon={<PlusOutlinedIcon />}> <Button type="primary" icon={<PlusOutlinedIcon />}>
@ -247,18 +247,18 @@ const AccessList = () => {
tabList={[ tabList={[
{ {
key: "both-dns-hosting", key: "both-dns-hosting",
label: t("access.props.range.both_dns_hosting"), label: t("access.props.usage.both_dns_hosting"),
}, },
{ {
key: "ca-only", key: "ca-only",
label: t("access.props.range.ca_only"), label: t("access.props.usage.ca_only"),
}, },
{ {
key: "notify-only", key: "notification-only",
label: t("access.props.range.notify_only"), label: t("access.props.usage.notification_only"),
}, },
]} ]}
activeTabKey={filters["range"] as string} activeTabKey={filters["usage"] as string}
onTabChange={(key) => handleTabChange(key)} onTabChange={(key) => handleTabChange(key)}
/> />