diff --git a/.env.example b/.env.example index 52abfc8..379eff4 100644 --- a/.env.example +++ b/.env.example @@ -28,11 +28,10 @@ RESEND_API_KEY= # ----------------------------------------------------------------------------- # Cloudflare # ----------------------------------------------------------------------------- -CLOUDFLARE_ZONE_ID= +NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=wr.do,uv.do +CLOUDFLARE_ZONE=[{"zone_id":"abc123", "zone_name": "wr.do"},{"zone_id":"abc456", "zone_name": "uv.do"}] CLOUDFLARE_API_KEY= CLOUDFLARE_EMAIL= -# Cloudflare zone name, example: wr.do -CLOUDFLARE_ZONE_NAME= # Open Signup NEXT_PUBLIC_OPEN_SIGNUP=1 diff --git a/app/(protected)/admin/records/page.tsx b/app/(protected)/admin/records/page.tsx index 0afc775..7f08023 100644 --- a/app/(protected)/admin/records/page.tsx +++ b/app/(protected)/admin/records/page.tsx @@ -25,7 +25,12 @@ export default async function DashboardPage() { linkText="DNS records." /> diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 072c90f..18b4af0 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -119,7 +119,7 @@ async function UserUrlsListSection({ async function UserRecordsListSection({ user, }: { - user: { id: string; name: string; apiKey: string }; + user: { id: string; name: string; apiKey: string; email: string }; }) { return ( @@ -208,6 +209,7 @@ export default async function DashboardPage() { id: user.id, name: user.name || "", apiKey: user.apiKey || "", + email: user.email || "", }} /> diff --git a/app/(protected)/dashboard/records/page.tsx b/app/(protected)/dashboard/records/page.tsx index 5d2db99..ace0c24 100644 --- a/app/(protected)/dashboard/records/page.tsx +++ b/app/(protected)/dashboard/records/page.tsx @@ -25,7 +25,12 @@ export default async function DashboardPage() { linkText="DNS records." /> diff --git a/app/(protected)/dashboard/records/record-list.tsx b/app/(protected)/dashboard/records/record-list.tsx index 635f153..d2a7e7c 100644 --- a/app/(protected)/dashboard/records/record-list.tsx +++ b/app/(protected)/dashboard/records/record-list.tsx @@ -43,7 +43,7 @@ import { LinkPreviewer } from "@/components/shared/link-previewer"; import { PaginationWrapper } from "@/components/shared/pagination"; export interface RecordListProps { - user: Pick; + user: Pick; action: string; } @@ -344,7 +344,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) { setShowModal={setShowForm} > = TeamPlanQuota[target_user.team!].RC_NewRecords) { + return Response.json("Your records have reached the free limit.", { + status: 409, + }); + } + + const record = { + ...records[0], + id: generateSecret(16), + }; + + let record_name = ["A", "CNAME"].includes(record.type) + ? record.name + : `${record.name}.${record.zone_name}`; + + let matchedZone; + + for (const zone of zones) { + if (record.zone_name === zone.zone_name) { + matchedZone = zone; + break; + } + } + + if (!matchedZone) { + return Response.json( + `No matching zone found for domain: ${record_name}`, + { + status: 400, + statusText: "Invalid domain", + }, + ); + } + + if (reservedDomains.includes(record_name)) { + return Response.json("Domain name is reserved", { + status: 403, + }); + } + + const user_record = await getUserRecordByTypeNameContent( + target_user.id, + record.type, + record_name, + record.content, + 1, + ); + if (user_record && user_record.length > 0) { + return Response.json("Record already exists", { + status: 403, + }); + } + + const data = await createDNSRecord( + matchedZone.zone_id, + CLOUDFLARE_API_KEY, + CLOUDFLARE_EMAIL, + record, + ); + + if (!data.success || !data.result?.id) { + // console.log("[data]", data); + return Response.json(data.messages, { + status: 501, + }); + } else { + const res = await createUserRecord(target_user.id, { + record_id: data.result.id, + zone_id: matchedZone.zone_id, + zone_name: matchedZone.zone_name, + name: data.result.name, + type: data.result.type, + content: data.result.content, + proxied: data.result.proxied, + proxiable: data.result.proxiable, + ttl: data.result.ttl, + comment: data.result.comment ?? "", + tags: data.result.tags?.join("") ?? "", + created_on: data.result.created_on, + modified_on: data.result.modified_on, + active: 0, + }); + + if (res.status !== "success") { + return Response.json(res.status, { + status: 502, + }); + } + return Response.json(res.data); + } + } catch (error) { + console.error("[错误]", error); + return Response.json(error, { + status: error?.status || 500, + }); + } +} diff --git a/app/api/record/admin/delete/route.ts b/app/api/record/admin/delete/route.ts index a91dd90..7e2ff9c 100644 --- a/app/api/record/admin/delete/route.ts +++ b/app/api/record/admin/delete/route.ts @@ -3,6 +3,7 @@ import { deleteDNSRecord } from "@/lib/cloudflare"; import { deleteUserRecord } from "@/lib/dto/cloudflare-dns-record"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; +import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -11,44 +12,57 @@ export async function POST(req: Request) { if (user.role !== "ADMIN") { return Response.json("Unauthorized", { status: 401, + statusText: "Admin access required", }); } const { record_id, zone_id, userId, active } = await req.json(); - if (!record_id || !userId) { - return Response.json("RecordId and userId are required", { + if (!record_id || !userId || !zone_id) { + return Response.json("record_id, userId, and zone_id are required", { status: 400, + statusText: "Invalid request body", }); } - const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - if (!CLOUDFLARE_ZONE_ID || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json("API key、zone iD and email are required", { + const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; + const zones = parseZones(CLOUDFLARE_ZONE || "[]"); + + if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + return Response.json( + "API key, zone configuration, and email are required", + { + status: 400, + statusText: "Missing required configuration", + }, + ); + } + + const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + if (!matchedZone) { + return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { status: 400, + statusText: "Invalid zone_id", }); } - // Delete cf dns record first. - const res = await deleteDNSRecord( - CLOUDFLARE_ZONE_ID, + // force delete + await deleteUserRecord(userId, record_id, zone_id, active); + await deleteDNSRecord( + matchedZone.zone_id, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, record_id, ); - if (res && res.result?.id) { - // Then delete user record. - await deleteUserRecord(userId, record_id, zone_id, active); - return Response.json("success", { - status: 200, - }); - } - return Response.json("Not Implemented", { - status: 501, + + return Response.json("success", { + status: 200, + statusText: "success", }); } catch (error) { - console.error(error); - return Response.json(error?.statusText || error, { - status: error.status || 500, + console.error("[Error]", error); + return Response.json(error.message || "Server error", { + status: error?.status || 500, + statusText: error?.statusText || "Server error", }); } } diff --git a/app/api/record/admin/route.ts b/app/api/record/admin/route.ts index 5d0f503..cd6042e 100644 --- a/app/api/record/admin/route.ts +++ b/app/api/record/admin/route.ts @@ -1,6 +1,6 @@ import { env } from "@/env.mjs"; import { getUserRecords } from "@/lib/dto/cloudflare-dns-record"; -import { checkUserStatus } from "@/lib/dto/user"; +import { checkUserStatus, getUserByEmail } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; export async function GET(req: Request) { diff --git a/app/api/record/admin/update/route.ts b/app/api/record/admin/update/route.ts index dd8b1cf..e8dc5c8 100644 --- a/app/api/record/admin/update/route.ts +++ b/app/api/record/admin/update/route.ts @@ -3,6 +3,7 @@ import { updateDNSRecord } from "@/lib/cloudflare"; import { updateUserRecord } from "@/lib/dto/cloudflare-dns-record"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; +import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -11,70 +12,104 @@ export async function POST(req: Request) { if (user.role !== "ADMIN") { return Response.json("Unauthorized", { status: 401, + statusText: "Admin access required", }); } - const { - CLOUDFLARE_ZONE_ID, - CLOUDFLARE_ZONE_NAME, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, - } = env; - if ( - !CLOUDFLARE_ZONE_ID || - !CLOUDFLARE_ZONE_NAME || - !CLOUDFLARE_API_KEY || - !CLOUDFLARE_EMAIL - ) { - return Response.json("API key、zone iD and email are required", { - status: 400, - }); + const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; + const zones = parseZones(CLOUDFLARE_ZONE || "[]"); + + if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + return Response.json( + "API key, zone configuration, and email are required", + { + status: 400, + statusText: "Missing required configuration", + }, + ); } const { record, recordId, userId } = await req.json(); - if (!recordId || !userId) { - return Response.json("RecordId and userId are required", { + if (!record || !recordId || !userId) { + return Response.json("record, recordId, and userId are required", { status: 400, + statusText: "Invalid request body", }); } + let record_name = ["A", "CNAME"].includes(record.type) + ? record.name + : `${record.name}.${record.zone_name}`; + + let matchedZone; + + for (const zone of zones) { + if (record.zone_name === zone.zone_name) { + matchedZone = zone; + break; + } + } + + if (!matchedZone) { + return Response.json( + `No matching zone found for domain: ${record_name}`, + { + status: 400, + statusText: "Invalid domain", + }, + ); + } + const data = await updateDNSRecord( - CLOUDFLARE_ZONE_ID, + matchedZone.zone_id, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, recordId, - record, + { ...record, name: record_name }, ); + if (!data.success || !data.result?.id) { - return Response.json(data.errors, { - status: 501, - }); - } else { - const res = await updateUserRecord(userId, { - record_id: data.result.id, - zone_id: CLOUDFLARE_ZONE_ID, - zone_name: CLOUDFLARE_ZONE_NAME, - name: data.result.name, - type: data.result.type, - content: data.result.content, - proxied: data.result.proxied, - proxiable: data.result.proxiable, - ttl: data.result.ttl, - comment: data.result.comment ?? "", - tags: data.result.tags?.join("") ?? "", - modified_on: data.result.modified_on, - active: 1, - }); - if (res.status !== "success") { - return Response.json(res.status, { - status: 502, - }); - } - return Response.json(res.data); + return Response.json( + data.errors?.[0]?.message || "Failed to update DNS record", + { + status: 501, + statusText: "Cloudflare API error", + }, + ); } + + const res = await updateUserRecord(userId, { + record_id: data.result.id, + zone_id: matchedZone.zone_id, + zone_name: matchedZone.zone_name, + name: data.result.name, + type: data.result.type, + content: data.result.content, + proxied: data.result.proxied, + proxiable: data.result.proxiable, + ttl: data.result.ttl, + comment: data.result.comment ?? "", + tags: data.result.tags?.join("") ?? "", + modified_on: data.result.modified_on, + active: 1, + }); + + if (res.status !== "success") { + return Response.json(res.status, { + status: 502, + statusText: "Failed to update user record", + }); + } + + return Response.json(res.data, { + status: 200, + statusText: "success", + }); } catch (error) { - return Response.json(error?.statusText || error, { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: error?.status || 500, + statusText: error?.statusText || "Server error", }); } } diff --git a/app/api/record/delete/route.ts b/app/api/record/delete/route.ts index f2c7085..4d6d3de 100644 --- a/app/api/record/delete/route.ts +++ b/app/api/record/delete/route.ts @@ -3,6 +3,7 @@ import { deleteDNSRecord } from "@/lib/cloudflare"; import { deleteUserRecord } from "@/lib/dto/cloudflare-dns-record"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; +import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -10,36 +11,50 @@ export async function POST(req: Request) { if (user instanceof Response) return user; const { record_id, zone_id, active } = await req.json(); - const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - if (!CLOUDFLARE_ZONE_ID || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json("API key、zone iD and email are required", { + const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; + const zones = parseZones(CLOUDFLARE_ZONE || "[]"); + + if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + return Response.json( + "API key, zone configuration, and email are required", + { + status: 400, + statusText: "API key, zone configuration, and email are required", + }, + ); + } + + const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + if (!matchedZone) { + return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { status: 400, + statusText: "Invalid zone_id", }); } - // Delete cf dns record first. const res = await deleteDNSRecord( - CLOUDFLARE_ZONE_ID, + matchedZone.zone_id, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, record_id, ); + if (res && res.result?.id) { - // Then delete user record. await deleteUserRecord(user.id, record_id, zone_id, active); return Response.json("success", { status: 200, statusText: "success", }); } + return Response.json({ status: 501, - statusText: "Not Implemented", + statusText: "Failed to delete DNS record", }); } catch (error) { - console.error(error); - return Response.json(error?.statusText || error, { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: error.status || 500, statusText: error.statusText || "Server error", }); diff --git a/app/api/record/update/route.ts b/app/api/record/update/route.ts index d8a5da9..612a9dc 100644 --- a/app/api/record/update/route.ts +++ b/app/api/record/update/route.ts @@ -7,105 +7,155 @@ import { import { checkUserStatus } from "@/lib/dto/user"; import { reservedDomains } from "@/lib/enums"; import { getCurrentUser } from "@/lib/session"; +import { parseZones } from "@/lib/utils"; -// update record +// Update DNS record export async function POST(req: Request) { try { const user = checkUserStatus(await getCurrentUser()); if (user instanceof Response) return user; - const { - CLOUDFLARE_ZONE_ID, - CLOUDFLARE_ZONE_NAME, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, - } = env; - if ( - !CLOUDFLARE_ZONE_ID || - !CLOUDFLARE_ZONE_NAME || - !CLOUDFLARE_API_KEY || - !CLOUDFLARE_EMAIL - ) { - return Response.json("API key andzone id are required.", { status: 401 }); + const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; + const zones = parseZones(CLOUDFLARE_ZONE || "[]"); + + if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + return Response.json( + "API key, zone configuration, and email are required", + { status: 401, statusText: "Missing required configuration" }, + ); } const { record, recordId } = await req.json(); + if (!record || !recordId) { + return Response.json("Record and recordId are required", { + status: 400, + statusText: "Invalid request body", + }); + } - const record_name = record.name.endsWith(".wr.do") + let record_name = ["A", "CNAME"].includes(record.type) ? record.name - : record.name + ".wr.do"; + : `${record.name}.${record.zone_name}`; + + let matchedZone; + + for (const zone of zones) { + if (record.zone_name === zone.zone_name) { + matchedZone = zone; + break; + } + } + + if (!matchedZone) { + return Response.json( + `No matching zone found for domain: ${record_name}`, + { + status: 400, + statusText: "Invalid domain", + }, + ); + } + if (reservedDomains.includes(record_name)) { return Response.json("Domain name is reserved", { status: 403, + statusText: "Reserved domain", }); } const data = await updateDNSRecord( - CLOUDFLARE_ZONE_ID, + matchedZone.zone_id, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, recordId, - record, + { ...record, name: record_name }, ); - console.log("updateDNSRecord", data); + console.log("[updateDNSRecord]", data); if (!data.success || !data.result?.id) { - return Response.json(data.errors, { - status: 501, - }); - } else { - const res = await updateUserRecord(user.id, { - record_id: data.result.id, - zone_id: CLOUDFLARE_ZONE_ID, - zone_name: CLOUDFLARE_ZONE_NAME, - name: data.result.name, - type: data.result.type, - content: data.result.content, - proxied: data.result.proxied, - proxiable: data.result.proxiable, - ttl: data.result.ttl, - comment: data.result.comment ?? "", - tags: data.result.tags?.join("") ?? "", - modified_on: data.result.modified_on, - active: 1, - }); - if (res.status !== "success") { - return Response.json(res.status, { - status: 502, - }); - } - return Response.json(res.data); + return Response.json( + data.errors?.[0]?.message || "Failed to update DNS record", + { status: 501, statusText: "Cloudflare API error" }, + ); } + + const res = await updateUserRecord(user.id, { + record_id: data.result.id, + zone_id: matchedZone.zone_id, // Use matched zone_id + zone_name: matchedZone.zone_name, // Use matched zone_name + name: data.result.name, + type: data.result.type, + content: data.result.content, + proxied: data.result.proxied, + proxiable: data.result.proxiable, + ttl: data.result.ttl, + comment: data.result.comment ?? "", + tags: data.result.tags?.join("") ?? "", + modified_on: data.result.modified_on, + active: 1, + }); + + if (res.status !== "success") { + return Response.json(res.status, { + status: 502, + statusText: "Failed to update user record", + }); + } + + return Response.json(res.data); } catch (error) { - console.log(error); - return Response.json(error?.statusText || error, { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: error?.status || 500, + statusText: error?.statusText || "Server error", }); } } -// update record state +// Update record state export async function PUT(req: Request) { try { const user = checkUserStatus(await getCurrentUser()); if (user instanceof Response) return user; - const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - if (!CLOUDFLARE_ZONE_ID || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json("API key and zone id are required.", { - status: 401, - }); + const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; + const zones = parseZones(CLOUDFLARE_ZONE || "[]"); + + if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + return Response.json( + "API key, zone configuration, and email are required", + { status: 401, statusText: "Missing required configuration" }, + ); } const { zone_id, record_id, target, active } = await req.json(); + if (!zone_id || !record_id || !target) { + return Response.json("zone_id, record_id, and target are required", { + status: 400, + statusText: "Invalid request body", + }); + } + + const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + if (!matchedZone) { + return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { + status: 400, + statusText: "Invalid zone_id", + }); + } let isTargetAccessible = false; try { - const target_res = await fetch(`https://${target}`); + const target_res = await fetch(`https://${target}`, { + method: "HEAD", + signal: AbortSignal.timeout(10000), + }); isTargetAccessible = target_res.status === 200; } catch (fetchError) { isTargetAccessible = false; - // console.log(`Failed to access target: ${fetchError}`); + console.log( + `[Fetch Error] Failed to access target ${target}: ${fetchError}`, + ); } const res = await updateUserRecordState( @@ -116,13 +166,24 @@ export async function PUT(req: Request) { ); if (!res) { - return Response.json("An error occurred.", { status: 502 }); + return Response.json("Failed to update record state", { + status: 502, + statusText: "Database error", + }); } + return Response.json( isTargetAccessible ? "Target is accessible!" : "Target is unaccessible!", + { status: 200 }, ); } catch (error) { - console.error(error); - return Response.json(`An error occurred. ${error}`, { status: 500 }); + console.error("[Error]", error); + return Response.json( + `An error occurred: ${error.message || "Unknown error"}`, + { + status: 500, + statusText: "Server error", + }, + ); } } diff --git a/app/emails/layout.tsx b/app/emails/layout.tsx index c492624..5dbe7aa 100644 --- a/app/emails/layout.tsx +++ b/app/emails/layout.tsx @@ -32,8 +32,8 @@ export default async function Dashboard({ children }: ProtectedLayoutProps) {
-
- +
+
diff --git a/components/forms/record-form.tsx b/components/forms/record-form.tsx index bd28218..0f26bd4 100644 --- a/components/forms/record-form.tsx +++ b/components/forms/record-form.tsx @@ -6,6 +6,7 @@ import { User } from "@prisma/client"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { siteConfig } from "@/config/site"; import { CreateDNSRecord, RecordType } from "@/lib/cloudflare"; import { UserRecordFormData } from "@/lib/dto/cloudflare-dns-record"; import { RECORD_TYPE_ENUMS, TTL_ENUMS } from "@/lib/enums"; @@ -30,7 +31,7 @@ export type FormData = CreateDNSRecord; export type FormType = "add" | "edit"; export interface RecordFormProps { - user: Pick; + user: Pick; isShowForm: boolean; setShowForm: Dispatch>; type: FormType; @@ -53,24 +54,25 @@ export function RecordForm({ const [currentRecordType, setCurrentRecordType] = useState( initData?.type || "CNAME", ); + const [currentZoneName, setCurrentZoneName] = useState( + initData?.zone_name || "wr.do", + ); + const [email, setEmail] = useState(user.email); const { handleSubmit, register, formState: { errors }, - getValues, setValue, } = useForm({ resolver: zodResolver(createRecordSchema), defaultValues: { + zone_name: initData?.zone_name || "wr.do", type: initData?.type || "CNAME", ttl: initData?.ttl || 1, proxied: initData?.proxied || false, - comment: initData?.comment || "", - name: - (initData?.name.endsWith(".wr.do") - ? initData?.name.slice(0, -6) - : initData?.name) || "", + comment: "Created by wr.do", + name: initData?.name ? initData.name.split(".")[0] : "", content: initData?.content || "", }, }); @@ -89,6 +91,7 @@ export function RecordForm({ method: "POST", body: JSON.stringify({ records: [data], + email, }), }); @@ -161,7 +164,62 @@ export function RecordForm({ {type === "add" ? "Create" : "Edit"} record
+ {action.indexOf("admin") > -1 && ( +
+ +
+ + setEmail(e.target.value)} + /> +
+
+ {errors?.content ? ( +

+ {errors.content.message} +

+ ) : ( +

+ Required. Enter user email +

+ )} +
+
+
+ )} +
+ + +

+ Required. Select a domain. +

+

Required.

+
+ +
@@ -211,38 +272,13 @@ export function RecordForm({

) : (

- Required. Use @ for root. + Required. E.g. www.

)}
-
- -
- - -

- Optional. Time To Live. -

-
- + {/*
diff --git a/config/site.ts b/config/site.ts index 84c68cd..7d7d5e3 100644 --- a/config/site.ts +++ b/config/site.ts @@ -6,6 +6,7 @@ const open_signup = env.NEXT_PUBLIC_OPEN_SIGNUP; const short_domains = env.NEXT_PUBLIC_SHORT_DOMAINS || ""; const email_domains = env.NEXT_PUBLIC_EMAIL_DOMAINS || ""; const email_r2_domain = env.NEXT_PUBLIC_EMAIL_R2_DOMAIN || ""; +const record_domains = env.NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME || ""; export const siteConfig: SiteConfig = { name: "WR.DO", @@ -24,6 +25,7 @@ export const siteConfig: SiteConfig = { openSignup: open_signup === "1" ? true : false, shortDomains: short_domains.split(","), emailDomains: email_domains.split(","), + recordDomains: record_domains.split(","), emailR2Domain: email_r2_domain, }; diff --git a/content/docs/developer/cloudflare.mdx b/content/docs/developer/cloudflare.mdx index 80bceb6..d947c6a 100644 --- a/content/docs/developer/cloudflare.mdx +++ b/content/docs/developer/cloudflare.mdx @@ -8,23 +8,51 @@ Before you start, you must have a Cloudflare account and be hosted on Cloudflare In this section, you can update these variables: ```js title=".env" -CLOUDFLARE_ZONE_ID=abcdef1234567890 +CLOUDFLARE_ZONE=[{"zone_id":"abc465","zone_name":"example.com"},{"zone_id":"abc465","zone_name":"example2.com"}] CLOUDFLARE_API_KEY=1234567890abcdef1234567890abcdef CLOUDFLARE_EMAIL=user@example.com -CLOUDFLARE_ZONE_NAME=wr.do +NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=example.com,example2.com ``` -### Variable Descriptions -- **CLOUDFLARE_ZONE_ID**: This is the unique identifier for your Cloudflare zone. You can find it in the Cloudflare dashboard under the Overview section of your domain. +## Variable Descriptions -> Follow [this way](https://dash.cloudflare.com/Your_Acount_Id/wr.do), and scroll down to `Zone ID`. - -- **CLOUDFLARE_API_KEY**: This is the API key that you use to authenticate requests to the Cloudflare API. You can generate or find your API key in the Cloudflare dashboard under the `profile` -> `api-tokens` section. +### CLOUDFLARE_ZONE -> Follow [https://dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens), and scroll down to `API Token`, the `Global API Key` should be used. - -- **CLOUDFLARE_EMAIL**: This is the email address associated with your Cloudflare account. It is used for authentication alongside the API key. +- Description: A JSON array of objects, each containing a zone_id and zone_name for your Cloudflare zones. The zone_id is the unique identifier for a domain, and the zone_name is the domain name (e.g., example.com). +- Where to find: In the Cloudflare dashboard, go to your domain’s Overview section and locate the Zone ID. +- Example: `[{"zone_id":"abc465","zone_name":"example.com"},{"zone_id":"def789","zone_name":"example2.com"}]` +- Note: Ensure the zone_name values match the domains listed in NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME. -- **CLOUDFLARE_ZONE_NAME**: This is the name of your Cloudflare zone. It is used to specify the zone in the Cloudflare API requests. +> Instructions: Navigate to Cloudflare Dashboard, select your account, and find the Zone ID under the Overview tab of your domain. +### CLOUDFLARE_API_KEY + +- Description: The API key used to authenticate requests to the Cloudflare API. +- Where to find: In the Cloudflare dashboard, go to Profile > API Tokens and locate your Global API Key. +- Example: 1234567890abcdef1234567890abcdef +- Security Note: Keep this key confidential and never expose it in client-side code. + +> Instructions: Visit https://dash.cloudflare.com/profile/api-tokens, and find the Global API Key under the API Tokens section. + +### CLOUDFLARE_EMAIL + +- Description: The email address associated with your Cloudflare account, used for API authentication alongside the API key. +- Example: `user@example.com` + +### NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME +- Description: A comma-separated list of domain names (e.g., `example.com,example2.com`) used for frontend display. These must correspond to the zone_name values in CLOUDFLARE_ZONE. +- Example: example.com,example2.com +- Note: Since this variable is prefixed with NEXT_PUBLIC_, it is exposed to the frontend. Ensure it only contains domain names and no sensitive information. + +### Important Notes + +- Correspondence: The zone_name in `CLOUDFLARE_ZONE` must match the domains listed in NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME. For example, if CLOUDFLARE_ZONE includes `{"zone_id":"abc465","zone_name":"example.com"}`, then `NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME` should include example.com. +- Security: Never expose `CLOUDFLARE_API_KEY` or `CLOUDFLARE_ZONE` in frontend code, as they contain sensitive information. Only `NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME` is safe for frontend use. +- Validation: Ensure the zone_id and zone_name in `CLOUDFLARE_ZONE` are correct, as incorrect values will cause API requests to fail. + +### Troubleshooting + +- API Key Issues: If API requests fail, verify your `CLOUDFLARE_API_KEY` and `CLOUDFLARE_EMAIL` are correct and have the necessary permissions. +- Zone Mismatch: If the frontend displays incorrect domains, ensure `NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME` matches the zone_name values in `CLOUDFLARE_ZONE`. +- Finding Zone ID: If you can’t locate your Zone ID, check the Overview tab of your domain in the Cloudflare dashboard. \ No newline at end of file diff --git a/content/docs/developer/installation.mdx b/content/docs/developer/installation.mdx index d4d43fc..f874900 100644 --- a/content/docs/developer/installation.mdx +++ b/content/docs/developer/installation.mdx @@ -48,7 +48,8 @@ Copy/paste the `.env.example` in the `.env` file: | GITHUB_ID | `123465` | The ID of the GitHub OAuth client. | | GITHUB_SECRET | `123465` | The secret of the GitHub OAuth client. | | RESEND_API_KEY | `123465` | The API key for Resend. | -| CLOUDFLARE_ZONE_ID | `123465` | The zone ID for Cloudflare. | +| CLOUDFLARE_ZONE | `[{"zone_id":"abc465","zone_name":"example.com"}]` | The zone info for Cloudflare. | +| NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME | `example.com,example2.com` | The zone name for Cloudflare. | | CLOUDFLARE_API_KEY | `123465` | The API key for Cloudflare. | | CLOUDFLARE_EMAIL | `123465` | The email for Cloudflare. | | NEXT_PUBLIC_OPEN_SIGNUP | `1` | Open signup. | @@ -61,7 +62,7 @@ Copy/paste the `.env.example` in the `.env` file: - How to get `GOOGLE_CLIENT_ID`、`GITHUB_ID`, see [Authentification](/docs/developer/authentification). - How to get `RESEND_API_KEY`, see [Email](/docs/developer/email). -- How to get `CLOUDFLARE_ZONE_ID`、`CLOUDFLARE_API_KEY`、`CLOUDFLARE_EMAIL`, see [Cloudflare Configs](/docs/developer/cloudflare). +- How to get `CLOUDFLARE_ZONE`、`CLOUDFLARE_API_KEY`、`CLOUDFLARE_EMAIL`、`NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME`, see [Cloudflare Configs](/docs/developer/cloudflare). - How to get `DATABASE_URL`, see [Database](/docs/developer/database). - How to active email worker, see [Email Worker](/docs/developer/cloudflare-email-worker). diff --git a/content/docs/developer/quick-start.mdx b/content/docs/developer/quick-start.mdx index 338200e..715b395 100644 --- a/content/docs/developer/quick-start.mdx +++ b/content/docs/developer/quick-start.mdx @@ -140,9 +140,9 @@ RESEND_API_KEY = re_your_resend_api_key; Before you start, you must have a Cloudflare account and be hosted on Cloudflare. -### Add the CLOUDFLARE_ZONE_ID Environment Variable +### Add the CLOUDFLARE_ZONE Environment Variable -This is the unique identifier for your Cloudflare zone. You can find it in the Cloudflare dashboard under the Overview section of your domain. +A JSON array of objects, each containing a zone_id and zone_name for your Cloudflare zones. The zone_id is the unique identifier for a domain, and the zone_name is the domain name (e.g., example.com). > Follow [this way](https://dash.cloudflare.com/Your_Acount_Id/wr.do), and scroll down to `Zone ID`. @@ -156,15 +156,15 @@ This is the unique identifier for your Cloudflare zone. You can find it in the C This is the email address associated with your Cloudflare account. It is used for authentication alongside the API key. -### Add the CLOUDFLARE_ZONE_NAME Environment Variable +### Add the NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME Environment Variable -This is the name of your Cloudflare zone. It is used to specify the zone in the Cloudflare API requests. +A comma-separated list of domain names (e.g., `example.com,example2.com`) used for frontend display. These must correspond to the zone_name values in CLOUDFLARE_ZONE. In this section, you can update these variables: ```js title=".env" -CLOUDFLARE_ZONE_ID=abcdef1234567890 -CLOUDFLARE_ZONE_NAME=wr.do +CLOUDFLARE_ZONE=[{"zone_id":"abc465","zone_name":"example.com"},{"zone_id":"abc465","zone_name":"example2.com"}] +NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=example.com,example2.com CLOUDFLARE_API_KEY=1234567890abcdef1234567890abcdef CLOUDFLARE_EMAIL=user@example.com ``` @@ -228,7 +228,7 @@ Via [http://localhost:3000](http://localhost:3000) ## Q & A -### 1. Worker Error - Too many redirects +### Worker Error - Too many redirects Via: @@ -236,4 +236,23 @@ Via: https://dash.cloudflare.com/[account_id]/[zone_name]/ssl-tls/configuration ``` -Change the `SSL/TLS Encryption` Mode to `Full` in the Cloudflare dashboard. \ No newline at end of file +Change the `SSL/TLS Encryption` Mode to `Full` in the Cloudflare dashboard. + +### How can I access the admin panel after first deployment? + +You need to first register an account and log in, +and modify the `role` field of this account to `ADMIN` in the `users` table of the database. +Then, refresh the website and access http://localhost:3000/admin. + + +Although it may not be convenient to do so, this is currently the fastest way to become an administrator. +In future versions, we will implement the function of automatically setting up administrators as soon as possible. + + +### How can I change the team plan quota? + +Via team.ts: + +```bash +https://github.com/oiov/wr.do/tree/main/config/team.ts +``` diff --git a/env.mjs b/env.mjs index 2b423fd..605b671 100644 --- a/env.mjs +++ b/env.mjs @@ -15,8 +15,7 @@ export const env = createEnv({ LinuxDo_CLIENT_SECRET: z.string().optional(), DATABASE_URL: z.string().min(1), RESEND_API_KEY: z.string().optional(), - CLOUDFLARE_ZONE_ID: z.string().min(1), - CLOUDFLARE_ZONE_NAME: z.string().min(1), + CLOUDFLARE_ZONE: z.string().min(1), CLOUDFLARE_API_KEY: z.string().min(1), CLOUDFLARE_EMAIL: z.string().min(1), SCREENSHOTONE_BASE_URL: z.string().optional(), @@ -28,6 +27,7 @@ export const env = createEnv({ NEXT_PUBLIC_SHORT_DOMAINS: z.string().min(1).default(""), NEXT_PUBLIC_EMAIL_DOMAINS: z.string().min(1).default(""), NEXT_PUBLIC_EMAIL_R2_DOMAIN: z.string().min(1), + NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME: z.string().min(1), }, runtimeEnv: { NEXTAUTH_URL: process.env.NEXTAUTH_URL, @@ -43,8 +43,9 @@ export const env = createEnv({ NEXT_PUBLIC_SHORT_DOMAINS: process.env.NEXT_PUBLIC_SHORT_DOMAINS, NEXT_PUBLIC_EMAIL_DOMAINS: process.env.NEXT_PUBLIC_EMAIL_DOMAINS, NEXT_PUBLIC_EMAIL_R2_DOMAIN: process.env.NEXT_PUBLIC_EMAIL_R2_DOMAIN, - CLOUDFLARE_ZONE_ID: process.env.CLOUDFLARE_ZONE_ID, - CLOUDFLARE_ZONE_NAME: process.env.CLOUDFLARE_ZONE_NAME, + NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME: + process.env.NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME, + CLOUDFLARE_ZONE: process.env.CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY: process.env.CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL: process.env.CLOUDFLARE_EMAIL, SCREENSHOTONE_BASE_URL: process.env.SCREENSHOTONE_BASE_URL, diff --git a/lib/cloudflare.ts b/lib/cloudflare.ts index 3119fcd..cdd81a7 100644 --- a/lib/cloudflare.ts +++ b/lib/cloudflare.ts @@ -2,6 +2,7 @@ export const CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4"; export interface CreateDNSRecord { id: string; + zone_name: string; type: string; name: string; content: string; diff --git a/lib/dto/user.ts b/lib/dto/user.ts index a744171..8004a1f 100644 --- a/lib/dto/user.ts +++ b/lib/dto/user.ts @@ -12,8 +12,11 @@ export const getUserByEmail = async (email: string) => { email: email, }, select: { + id: true, name: true, emailVerified: true, + active: true, + team: true, }, }); diff --git a/lib/utils.ts b/lib/utils.ts index 9b2c1ab..752eb01 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -388,3 +388,25 @@ export function extractHost(url: string): string { const match = url.match(regex); return match ? match[1] : ""; } + +// 解析 CLOUDFLARE_ZONE 环境变量并返回结构化的域名配置 +export function parseZones(raw: string) { + let zones; + + try { + zones = JSON.parse(raw); + } catch (error) { + return []; + } + + if (!Array.isArray(zones)) { + return []; + } + + const parsedZones = zones.map((zone) => { + const { zone_id, zone_name } = zone; + return { zone_id, zone_name }; + }); + + return parsedZones; +} diff --git a/lib/validations/record.ts b/lib/validations/record.ts index 55116f9..3ff9202 100644 --- a/lib/validations/record.ts +++ b/lib/validations/record.ts @@ -1,6 +1,7 @@ import * as z from "zod"; export const createRecordSchema = z.object({ + zone_name: z.string().min(1).max(32), type: z .string() .regex(/^[a-zA-Z0-9]+$/, "Invalid characters") diff --git a/public/sw.js.map b/public/sw.js.map index b80b3bb..79b87a0 100644 --- a/public/sw.js.map +++ b/public/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/230e0a373e595c1da7a8cc5631d219e7/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAa,EAEZ,CAAA;EAQDC,CAAI,CAAA,CAAA,CAAA,CAACC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAA;AAElBC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAyB,EAAE,CAAA;AAI3BC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAA6B,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAIC,oBAA+B,CAAC,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAO,EAAE,CAAC,CAAA;GAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAe,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA;AAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAIF,QAAQ,CAAIA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAQ,CAACG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,gBAAgB,CAAE,CAAA,CAAA;AAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,OAAO,CAAIC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAQ,CAACJ,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAACK,IAAI,CAAE,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAM,EAAE,CAAG,CAAA,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAU,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA;YAAEC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAER,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAACQ,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA;AAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA;EAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAOR,QAAQ,CAAA;EAAC,CAAA,CAAA,CAAA,CAAA,CAAA;KAAG,CAAA;AAAE,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAA;AACxWL,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAA6B,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAIc,mBAA8B,CAAC,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;EAAEZ,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAO,EAAE,CAAA,CAAA;EAAG,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA;;"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/f265de56c16e5523852e53c538138141/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index e75ee52..a9b500d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -19,6 +19,7 @@ export type SiteConfig = { openSignup: boolean; shortDomains: string[]; emailDomains: string[]; + recordDomains: string[]; emailR2Domain: string; };