When adding a domain, you can also add a custom email address.

This commit is contained in:
yoan 2024-09-13 07:36:26 +08:00
parent 139a6980ac
commit 7550aec904
12 changed files with 1147 additions and 289 deletions

View File

@ -23,6 +23,8 @@ const (
configTypeGodaddy = "godaddy"
)
const defaultEmail = "536464346@qq.com"
type Certificate struct {
CertUrl string `json:"certUrl"`
CertStableUrl string `json:"certStableUrl"`
@ -60,8 +62,12 @@ type Applicant interface {
func Get(record *models.Record) (Applicant, error) {
access := record.ExpandedOne("access")
email := record.GetString("email")
if email == "" {
email = defaultEmail
}
option := &ApplyOption{
Email: "536464346@qq.com",
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
}

View File

@ -0,0 +1,584 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-09-12 23:13:12.119Z",
"name": "domains",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "iuaerpl2",
"name": "domain",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
"name": "crontab",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "alc8e9ow",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "topsc9bj",
"name": "certUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vixgq072",
"name": "certStableUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "g3a3sza5",
"name": "privateKey",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gr6iouny",
"name": "certificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tk6vnrmn",
"name": "issuerCertificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "sjo6ibse",
"name": "csr",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x03n1bkj",
"name": "expiredAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
},
{
"system": false,
"id": "xy7yk0mb",
"name": "targetAccess",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "6jqeyggw",
"name": "enabled",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "hdsjcchf",
"name": "deployed",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "aiya3rev",
"name": "rightnow",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "ixznmhzc",
"name": "lastDeployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "ghtlkn5j",
"name": "lastDeployment",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "0a1o4e6sstp694f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-09-12 23:08:52.810Z",
"name": "access",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "geeur58v",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "iql7jpwx",
"name": "config",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy"
]
}
},
{
"system": false,
"id": "lr33hiwg",
"name": "deleted",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-09-12 23:08:52.810Z",
"name": "deployments",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "farvlzk7",
"name": "domain",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "z3p974ainxjqlvs",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "jx5f69i3",
"name": "log",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "qbxdtg9q",
"name": "phase",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"check",
"apply",
"deploy"
]
}
},
{
"system": false,
"id": "rglrp1hz",
"name": "phaseSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "lt1g1blu",
"name": "deployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-12 23:08:52.811Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
},
{
"id": "dy6ccjb60spfy6p",
"created": "2024-09-12 23:12:21.677Z",
"updated": "2024-09-12 23:19:09.110Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tcmdsdf",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f9wyhypi",
"name": "content",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

284
ui/dist/assets/index-CSdX68Da.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
ui/dist/index.html vendored
View File

@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certimate - Your Trusted SSL Automation Partner</title>
<script type="module" crossorigin src="/assets/index-DTugtq1k.js"></script>
<script type="module" crossorigin src="/assets/index-CSdX68Da.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cg0yCJnh.css">
</head>
<body class="bg-background">

View File

@ -0,0 +1,141 @@
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { useConfig } from "@/providers/config";
import { update } from "@/repository/settings";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
type EmailsEditProps = {
className?: string;
trigger: React.ReactNode;
};
const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
const {
config: { emails },
setEmails,
} = useConfig();
const [open, setOpen] = useState(false);
const formSchema = z.object({
email: z.string().email(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if (emails.content.emails.includes(data.email)) {
form.setError("email", {
message: "邮箱已存在",
});
return;
}
// 保存到 config
const newEmails = [...emails.content.emails, data.email];
try {
const resp = await update({
...emails,
name: "emails",
content: {
emails: newEmails,
},
});
// 更新本地状态
setEmails(resp);
// 关闭弹窗
form.reset();
form.clearErrors();
setOpen(false);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(
([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
}
);
}
};
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild className={cn(className)}>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="container py-3">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
};
export default EmailsEdit;

View File

@ -3,6 +3,7 @@ import { Deployment, Pahse } from "./deployment";
export type Domain = {
id: string;
domain: string;
email?: string;
crontab: string;
access: string;
targetAccess: string;

View File

@ -0,0 +1,9 @@
export type Setting = {
id?: string;
name?: string;
content: EmailsSetting;
};
type EmailsSetting = {
emails: string[];
};

View File

@ -34,10 +34,11 @@ import { useLocation, useNavigate } from "react-router-dom";
import { Plus } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access";
import EmailsEdit from "@/components/certimate/EmailsEdit";
const Edit = () => {
const {
config: { accesses },
config: { accesses, emails },
} = useConfig();
const [domain, setDomain] = useState<Domain>();
@ -62,6 +63,7 @@ const Edit = () => {
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名",
}),
email: z.string().email().optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
}),
@ -78,6 +80,7 @@ const Edit = () => {
defaultValues: {
id: "",
domain: "",
email: "",
access: "",
targetAccess: "",
targetType: "",
@ -89,6 +92,7 @@ const Edit = () => {
form.reset({
id: domain.id,
domain: domain.domain,
email: domain.email,
access: domain.access,
targetAccess: domain.targetAccess,
targetType: domain.targetType,
@ -119,6 +123,7 @@ const Edit = () => {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
targetAccess: data.targetAccess,
targetType: data.targetType,
@ -180,6 +185,51 @@ const Edit = () => {
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex w-full justify-between">
<div>Email</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择邮箱" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{emails.content.emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="access"

View File

@ -10,9 +10,12 @@ import {
useReducer,
} from "react";
import { configReducer } from "./reducer";
import { getEmails } from "@/repository/settings";
import { Setting } from "@/domain/settings";
export type ConfigData = {
accesses: Access[];
emails: Setting;
};
export type ConfigContext = {
@ -20,6 +23,7 @@ export type ConfigContext = {
deleteAccess: (id: string) => void;
addAccess: (access: Access) => void;
updateAccess: (access: Access) => void;
setEmails: (email: Setting) => void;
};
const Context = createContext({} as ConfigContext);
@ -31,7 +35,10 @@ interface ContainerProps {
}
export const ConfigProvider = ({ children }: ContainerProps) => {
const [config, dispatchConfig] = useReducer(configReducer, { accesses: [] });
const [config, dispatchConfig] = useReducer(configReducer, {
accesses: [],
emails: { content: { emails: [] } },
});
useEffect(() => {
const featchData = async () => {
@ -41,6 +48,18 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
featchData();
}, []);
useEffect(() => {
const featchEmails = async () => {
const emails = await getEmails();
dispatchConfig({ type: "SET_EMAILS", payload: emails });
};
featchEmails();
}, []);
const setEmails = useCallback((emails: Setting) => {
dispatchConfig({ type: "SET_EMAILS", payload: emails });
}, []);
const deleteAccess = useCallback((id: string) => {
dispatchConfig({ type: "DELETE_ACCESS", payload: id });
}, []);
@ -58,9 +77,11 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
value={{
config: {
accesses: config.accesses,
emails: config.emails,
},
deleteAccess,
addAccess,
setEmails,
updateAccess,
}}
>

View File

@ -1,11 +1,14 @@
import { Access } from "@/domain/access";
import { ConfigData } from ".";
import { Setting } from "@/domain/settings";
type Action =
| { type: "ADD_ACCESS"; payload: Access }
| { type: "DELETE_ACCESS"; payload: string }
| { type: "UPDATE_ACCESS"; payload: Access }
| { type: "SET_ACCESSES"; payload: Access[] };
| { type: "SET_ACCESSES"; payload: Access[] }
| { type: "SET_EMAILS"; payload: Setting }
| { type: "ADD_EMAIL"; payload: string };
export const configReducer = (
state: ConfigData,
@ -40,6 +43,23 @@ export const configReducer = (
),
};
}
case "SET_EMAILS": {
return {
...state,
emails: action.payload,
};
}
case "ADD_EMAIL": {
return {
...state,
emails: {
...state.emails,
content: {
emails: [...state.emails.content.emails, action.payload],
},
},
};
}
default:
return state;
}

View File

@ -0,0 +1,26 @@
import { Setting } from "@/domain/settings";
import { getPb } from "./api";
export const getEmails = async () => {
try {
const resp = await getPb()
.collection("settings")
.getFirstListItem<Setting>("name='emails'");
return resp;
} catch (e) {
return {
content: { emails: [] },
};
}
};
export const update = async (setting: Setting) => {
const pb = getPb();
let resp: Setting;
if (setting.id) {
resp = await pb.collection("settings").update(setting.id, setting);
} else {
resp = await pb.collection("settings").create(setting);
}
return resp;
};