@naviyra/cloud-panel 0.0.3-beta2 → 0.0.3-beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/agent/src/cloudflare.mjs +1 -0
- package/apps/agent/src/files.mjs +162 -0
- package/apps/agent/src/host-tools.mjs +518 -0
- package/apps/agent/src/native.mjs +157 -32
- package/apps/agent/src/runtime.mjs +70 -37
- package/apps/agent/src/server.mjs +61 -6
- package/apps/panel/app/api/[...path]/route.js +94 -13
- package/apps/panel/app/globals.css +262 -3
- package/apps/panel/app/layout.jsx +2 -1
- package/apps/panel/app/login/page.jsx +3 -8
- package/apps/panel/app/page.jsx +17 -7
- package/apps/panel/app/ui/cloudflare.jsx +72 -47
- package/apps/panel/app/ui/confirm.jsx +109 -0
- package/apps/panel/app/ui/dashboard.jsx +87 -72
- package/apps/panel/app/ui/databases.jsx +168 -59
- package/apps/panel/app/ui/project-tools.jsx +604 -0
- package/apps/panel/app/ui/toast.jsx +90 -0
- package/apps/panel/lib/agent.mjs +13 -2
- package/apps/panel/lib/databases.mjs +120 -0
- package/apps/panel/lib/request-origin.mjs +29 -29
- package/package.json +1 -1
- package/packages/shared/index.mjs +49 -0
- package/scripts/panel-control.mjs +214 -214
- package/scripts/service-supervisor.mjs +164 -164
- package/scripts/windows-boot.mjs +259 -259
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
Check,
|
|
5
5
|
Cloud,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
Trash2,
|
|
11
11
|
} from "lucide-react";
|
|
12
12
|
import { EmailSkeleton } from "./skeleton";
|
|
13
|
+
import { useConfirm } from "./confirm";
|
|
14
|
+
import { useToast } from "./toast";
|
|
13
15
|
|
|
14
16
|
async function api(url, options = {}) {
|
|
15
17
|
const response = await fetch(`/api/${url}`, {
|
|
@@ -29,9 +31,15 @@ function when(value) {
|
|
|
29
31
|
return value ? new Date(value).toLocaleString() : "—";
|
|
30
32
|
}
|
|
31
33
|
export function CloudflareConnect({ initial, onReady, canWrite }) {
|
|
34
|
+
const toast = useToast();
|
|
35
|
+
const seen = useRef("");
|
|
32
36
|
const [progress, setProgress] = useState(initial?.login || { state: "idle" });
|
|
33
|
-
const [error, setError] = useState(initial?.error || "");
|
|
34
37
|
const [busy, setBusy] = useState(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!initial?.error || initial.error === seen.current) return;
|
|
40
|
+
seen.current = initial.error;
|
|
41
|
+
toast.failed(initial.error);
|
|
42
|
+
}, [initial?.error, toast]);
|
|
35
43
|
useEffect(() => {
|
|
36
44
|
if (progress.state !== "running") return undefined;
|
|
37
45
|
const timer = setInterval(async () => {
|
|
@@ -42,16 +50,22 @@ export function CloudflareConnect({ initial, onReady, canWrite }) {
|
|
|
42
50
|
]);
|
|
43
51
|
setProgress(current);
|
|
44
52
|
if (status.registered) onReady(status);
|
|
45
|
-
else if (current.state === "error"
|
|
53
|
+
else if (current.state === "error" && current.error !== seen.current) {
|
|
54
|
+
seen.current = current.error;
|
|
55
|
+
toast.failed(current.error);
|
|
56
|
+
}
|
|
46
57
|
} catch (e) {
|
|
47
|
-
|
|
58
|
+
if (seen.current !== e.message) {
|
|
59
|
+
seen.current = e.message;
|
|
60
|
+
toast.failed(e.message);
|
|
61
|
+
}
|
|
48
62
|
}
|
|
49
63
|
}, 1000);
|
|
50
64
|
return () => clearInterval(timer);
|
|
51
|
-
}, [progress.state, onReady]);
|
|
65
|
+
}, [progress.state, onReady, toast]);
|
|
52
66
|
async function connect() {
|
|
53
67
|
setBusy(true);
|
|
54
|
-
|
|
68
|
+
seen.current = "";
|
|
55
69
|
try {
|
|
56
70
|
const started = await api("cloudflare/login", { method: "POST" });
|
|
57
71
|
setProgress(started);
|
|
@@ -59,8 +73,9 @@ export function CloudflareConnect({ initial, onReady, canWrite }) {
|
|
|
59
73
|
onReady(await api("cloudflare/status"));
|
|
60
74
|
return;
|
|
61
75
|
}
|
|
76
|
+
toast.progress("Waiting for Cloudflare approval.");
|
|
62
77
|
} catch (e) {
|
|
63
|
-
|
|
78
|
+
toast.failed(e.message);
|
|
64
79
|
} finally {
|
|
65
80
|
setBusy(false);
|
|
66
81
|
}
|
|
@@ -96,7 +111,6 @@ export function CloudflareConnect({ initial, onReady, canWrite }) {
|
|
|
96
111
|
</li>
|
|
97
112
|
))}
|
|
98
113
|
</ol>
|
|
99
|
-
{error && <div className="error">{error}</div>}
|
|
100
114
|
{progress.log && <pre className="connect-log">{progress.log}</pre>}
|
|
101
115
|
{progress.url && (
|
|
102
116
|
<a className="primary connect-link" href={progress.url} target="_blank" rel="noreferrer">
|
|
@@ -119,22 +133,20 @@ export function CloudflareConnect({ initial, onReady, canWrite }) {
|
|
|
119
133
|
);
|
|
120
134
|
}
|
|
121
135
|
export function DnsScreen({ canWrite }) {
|
|
136
|
+
const toast = useToast();
|
|
122
137
|
const [data, setData] = useState(null);
|
|
123
138
|
const [detail, setDetail] = useState(null);
|
|
124
139
|
const [draft, setDraft] = useState(null);
|
|
125
|
-
const [error, setError] = useState("");
|
|
126
|
-
const [notice, setNotice] = useState("");
|
|
127
140
|
const [query, setQuery] = useState("");
|
|
128
141
|
const [type, setType] = useState("all");
|
|
129
142
|
const [loading, setLoading] = useState(true);
|
|
130
143
|
const [saving, setSaving] = useState(false);
|
|
131
144
|
async function load() {
|
|
132
145
|
setLoading(true);
|
|
133
|
-
setError("");
|
|
134
146
|
try {
|
|
135
147
|
setData(await api("cloudflare/dns"));
|
|
136
148
|
} catch (e) {
|
|
137
|
-
|
|
149
|
+
toast.failed(e.message);
|
|
138
150
|
} finally {
|
|
139
151
|
setLoading(false);
|
|
140
152
|
}
|
|
@@ -153,23 +165,19 @@ export function DnsScreen({ canWrite }) {
|
|
|
153
165
|
priority: record.priority ?? 10,
|
|
154
166
|
comment: record.comment || "",
|
|
155
167
|
});
|
|
156
|
-
setNotice("");
|
|
157
|
-
setError("");
|
|
158
168
|
}
|
|
159
169
|
async function open(id) {
|
|
160
170
|
setDraft(null);
|
|
161
|
-
setError("");
|
|
162
171
|
try {
|
|
163
172
|
setDetail(await api(`cloudflare/dns/${id}`));
|
|
164
173
|
} catch (e) {
|
|
165
|
-
|
|
174
|
+
toast.failed(e.message);
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
async function saveEdit(event) {
|
|
169
178
|
event.preventDefault();
|
|
170
179
|
if (!detail) return;
|
|
171
180
|
setSaving(true);
|
|
172
|
-
setError("");
|
|
173
181
|
try {
|
|
174
182
|
const saved = await api(`cloudflare/dns/${detail.id}`, {
|
|
175
183
|
method: "PUT",
|
|
@@ -187,9 +195,9 @@ export function DnsScreen({ canWrite }) {
|
|
|
187
195
|
}
|
|
188
196
|
: current,
|
|
189
197
|
);
|
|
190
|
-
|
|
198
|
+
toast.success("DNS record updated.");
|
|
191
199
|
} catch (e) {
|
|
192
|
-
|
|
200
|
+
toast.failed(e.message);
|
|
193
201
|
} finally {
|
|
194
202
|
setSaving(false);
|
|
195
203
|
}
|
|
@@ -216,8 +224,6 @@ export function DnsScreen({ canWrite }) {
|
|
|
216
224
|
Refresh
|
|
217
225
|
</button>
|
|
218
226
|
</div>
|
|
219
|
-
{error && <div className="error">{error}</div>}
|
|
220
|
-
{notice && <div className="notice">{notice}</div>}
|
|
221
227
|
<section className="panel">
|
|
222
228
|
{loading && data && (
|
|
223
229
|
<div
|
|
@@ -484,9 +490,11 @@ export function DnsScreen({ canWrite }) {
|
|
|
484
490
|
);
|
|
485
491
|
}
|
|
486
492
|
export function EmailScreen({ canWrite }) {
|
|
493
|
+
const confirm = useConfirm();
|
|
494
|
+
const toast = useToast();
|
|
495
|
+
const seen = useRef("");
|
|
496
|
+
const addressError = useRef("");
|
|
487
497
|
const [data, setData] = useState(null);
|
|
488
|
-
const [error, setError] = useState("");
|
|
489
|
-
const [notice, setNotice] = useState("");
|
|
490
498
|
const [busy, setBusy] = useState(false);
|
|
491
499
|
const [loading, setLoading] = useState(true);
|
|
492
500
|
const [authorized, setAuthorized] = useState(false);
|
|
@@ -494,11 +502,10 @@ export function EmailScreen({ canWrite }) {
|
|
|
494
502
|
const [matcher, setMatcher] = useState("literal");
|
|
495
503
|
async function load() {
|
|
496
504
|
setLoading(true);
|
|
497
|
-
setError("");
|
|
498
505
|
try {
|
|
499
506
|
setData(await api("cloudflare/email"));
|
|
500
507
|
} catch (e) {
|
|
501
|
-
|
|
508
|
+
toast.failed(e.message);
|
|
502
509
|
} finally {
|
|
503
510
|
setLoading(false);
|
|
504
511
|
}
|
|
@@ -514,7 +521,7 @@ export function EmailScreen({ canWrite }) {
|
|
|
514
521
|
})
|
|
515
522
|
.catch((e) => {
|
|
516
523
|
if (!stop) {
|
|
517
|
-
|
|
524
|
+
toast.failed(e.message);
|
|
518
525
|
setLoading(false);
|
|
519
526
|
}
|
|
520
527
|
});
|
|
@@ -522,6 +529,12 @@ export function EmailScreen({ canWrite }) {
|
|
|
522
529
|
stop = true;
|
|
523
530
|
};
|
|
524
531
|
}, []);
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
const message = data?.address_error || "";
|
|
534
|
+
if (message === addressError.current) return;
|
|
535
|
+
addressError.current = message;
|
|
536
|
+
if (message) toast.failed(message);
|
|
537
|
+
}, [data?.address_error, toast]);
|
|
525
538
|
useEffect(() => {
|
|
526
539
|
if (apiProgress.state !== "running") return undefined;
|
|
527
540
|
const timer = setInterval(async () => {
|
|
@@ -530,41 +543,45 @@ export function EmailScreen({ canWrite }) {
|
|
|
530
543
|
setApiProgress(current);
|
|
531
544
|
if (current.state === "done" || current.skipped) {
|
|
532
545
|
setAuthorized(true);
|
|
533
|
-
|
|
546
|
+
toast.success("Cloudflare API authorized.");
|
|
534
547
|
await load();
|
|
535
|
-
} else if (current.state === "error"
|
|
548
|
+
} else if (current.state === "error" && current.error !== seen.current) {
|
|
549
|
+
seen.current = current.error || "";
|
|
550
|
+
if (current.error) toast.failed(current.error);
|
|
551
|
+
}
|
|
536
552
|
} catch (e) {
|
|
537
|
-
|
|
553
|
+
if (seen.current !== e.message) {
|
|
554
|
+
seen.current = e.message;
|
|
555
|
+
toast.failed(e.message);
|
|
556
|
+
}
|
|
538
557
|
}
|
|
539
558
|
}, 1000);
|
|
540
559
|
return () => clearInterval(timer);
|
|
541
560
|
}, [apiProgress.state]);
|
|
542
561
|
async function authorize() {
|
|
543
562
|
setBusy(true);
|
|
544
|
-
|
|
563
|
+
seen.current = "";
|
|
545
564
|
try {
|
|
546
565
|
const started = await api("cloudflare/api-login", { method: "POST" });
|
|
547
566
|
setApiProgress(started);
|
|
548
567
|
if (started.skipped || started.registered) {
|
|
549
568
|
setAuthorized(true);
|
|
550
|
-
|
|
569
|
+
toast.success("Cloudflare API authorized.");
|
|
551
570
|
await load();
|
|
552
|
-
}
|
|
571
|
+
} else toast.progress("Waiting for Wrangler approval.");
|
|
553
572
|
} catch (e) {
|
|
554
|
-
|
|
573
|
+
toast.failed(e.message);
|
|
555
574
|
} finally {
|
|
556
575
|
setBusy(false);
|
|
557
576
|
}
|
|
558
577
|
}
|
|
559
578
|
async function run(action) {
|
|
560
579
|
setBusy(true);
|
|
561
|
-
setError("");
|
|
562
|
-
setNotice("");
|
|
563
580
|
try {
|
|
564
581
|
await action();
|
|
565
582
|
await load();
|
|
566
583
|
} catch (e) {
|
|
567
|
-
|
|
584
|
+
toast.failed(e.message);
|
|
568
585
|
} finally {
|
|
569
586
|
setBusy(false);
|
|
570
587
|
}
|
|
@@ -588,7 +605,7 @@ export function EmailScreen({ canWrite }) {
|
|
|
588
605
|
});
|
|
589
606
|
form.reset();
|
|
590
607
|
setMatcher("literal");
|
|
591
|
-
|
|
608
|
+
toast.success("Routing rule created.");
|
|
592
609
|
});
|
|
593
610
|
}
|
|
594
611
|
return (
|
|
@@ -609,8 +626,6 @@ export function EmailScreen({ canWrite }) {
|
|
|
609
626
|
Refresh
|
|
610
627
|
</button>
|
|
611
628
|
</div>
|
|
612
|
-
{error && <div className="error">{error}</div>}
|
|
613
|
-
{notice && <div className="notice">{notice}</div>}
|
|
614
629
|
{!authorized && (
|
|
615
630
|
<section className="panel connect-panel">
|
|
616
631
|
<div className="section-heading">
|
|
@@ -673,7 +688,7 @@ export function EmailScreen({ canWrite }) {
|
|
|
673
688
|
method: "POST",
|
|
674
689
|
body: JSON.stringify({ enabled: !data.settings.enabled }),
|
|
675
690
|
});
|
|
676
|
-
|
|
691
|
+
toast.success(
|
|
677
692
|
data.settings.enabled
|
|
678
693
|
? "Email routing disabled."
|
|
679
694
|
: "Email routing enabled. Cloudflare adds the required MX and SPF records.",
|
|
@@ -688,7 +703,6 @@ export function EmailScreen({ canWrite }) {
|
|
|
688
703
|
<div className="section-heading">
|
|
689
704
|
<h2>Destination addresses</h2>
|
|
690
705
|
</div>
|
|
691
|
-
{data.address_error && <div className="error">{data.address_error}</div>}
|
|
692
706
|
{canWrite && (
|
|
693
707
|
<form
|
|
694
708
|
className="inline-form address-form"
|
|
@@ -702,7 +716,7 @@ export function EmailScreen({ canWrite }) {
|
|
|
702
716
|
body: JSON.stringify({ email }),
|
|
703
717
|
});
|
|
704
718
|
form.reset();
|
|
705
|
-
|
|
719
|
+
toast.success("Verification email sent to that destination.");
|
|
706
720
|
});
|
|
707
721
|
}}
|
|
708
722
|
>
|
|
@@ -735,8 +749,15 @@ export function EmailScreen({ canWrite }) {
|
|
|
735
749
|
<button
|
|
736
750
|
className="text-button danger-text"
|
|
737
751
|
disabled={busy}
|
|
738
|
-
onClick={() => {
|
|
739
|
-
if (
|
|
752
|
+
onClick={async () => {
|
|
753
|
+
if (
|
|
754
|
+
await confirm({
|
|
755
|
+
title: `Remove ${address.email}?`,
|
|
756
|
+
message:
|
|
757
|
+
"This address will stop receiving forwarded mail.",
|
|
758
|
+
confirmLabel: "Remove",
|
|
759
|
+
})
|
|
760
|
+
)
|
|
740
761
|
run(() =>
|
|
741
762
|
api(`cloudflare/email/addresses/${address.id}`, {
|
|
742
763
|
method: "DELETE",
|
|
@@ -876,7 +897,7 @@ export function EmailScreen({ canWrite }) {
|
|
|
876
897
|
enabled: !rule.enabled,
|
|
877
898
|
}),
|
|
878
899
|
});
|
|
879
|
-
|
|
900
|
+
toast.success(
|
|
880
901
|
rule.enabled ? "Rule disabled." : "Rule enabled.",
|
|
881
902
|
);
|
|
882
903
|
})
|
|
@@ -887,9 +908,13 @@ export function EmailScreen({ canWrite }) {
|
|
|
887
908
|
<button
|
|
888
909
|
className="text-button danger-text"
|
|
889
910
|
disabled={busy}
|
|
890
|
-
onClick={() => {
|
|
911
|
+
onClick={async () => {
|
|
891
912
|
if (
|
|
892
|
-
|
|
913
|
+
await confirm({
|
|
914
|
+
title: `Delete “${rule.name}”?`,
|
|
915
|
+
message: "This routing rule will be removed.",
|
|
916
|
+
confirmLabel: "Delete",
|
|
917
|
+
})
|
|
893
918
|
)
|
|
894
919
|
run(() =>
|
|
895
920
|
api(
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
const ConfirmContext = createContext(null);
|
|
5
|
+
|
|
6
|
+
export function useConfirm() {
|
|
7
|
+
const confirm = useContext(ConfirmContext);
|
|
8
|
+
if (!confirm) throw Error("Confirm dialog is unavailable");
|
|
9
|
+
return confirm;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function requestFrom(options) {
|
|
13
|
+
const value = typeof options === "string" ? { message: options } : options || {};
|
|
14
|
+
return {
|
|
15
|
+
title: value.title || "Please confirm",
|
|
16
|
+
message: value.message || "",
|
|
17
|
+
confirmLabel: value.confirmLabel || "OK",
|
|
18
|
+
cancelLabel: value.cancelLabel || "Cancel",
|
|
19
|
+
danger: value.danger !== false,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ConfirmProvider({ children }) {
|
|
24
|
+
const [request, setRequest] = useState(null);
|
|
25
|
+
const resolver = useRef(null);
|
|
26
|
+
const finish = useCallback((accepted) => {
|
|
27
|
+
resolver.current?.(accepted);
|
|
28
|
+
resolver.current = null;
|
|
29
|
+
setRequest(null);
|
|
30
|
+
}, []);
|
|
31
|
+
const confirm = useCallback((options) => {
|
|
32
|
+
if (resolver.current) resolver.current(false);
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
resolver.current = resolve;
|
|
35
|
+
setRequest(requestFrom(options));
|
|
36
|
+
});
|
|
37
|
+
}, []);
|
|
38
|
+
return (
|
|
39
|
+
<ConfirmContext.Provider value={confirm}>
|
|
40
|
+
{children}
|
|
41
|
+
{request && <ConfirmDialog request={request} onFinish={finish} />}
|
|
42
|
+
</ConfirmContext.Provider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ConfirmDialog({ request, onFinish }) {
|
|
47
|
+
const cancelRef = useRef(null);
|
|
48
|
+
const confirmRef = useRef(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
cancelRef.current?.focus();
|
|
51
|
+
const onKey = (event) => {
|
|
52
|
+
if (event.key === "Escape") {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
onFinish(false);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.key !== "Tab") return;
|
|
58
|
+
const items = [cancelRef.current, confirmRef.current].filter(Boolean);
|
|
59
|
+
const first = items[0];
|
|
60
|
+
const last = items.at(-1);
|
|
61
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
last?.focus();
|
|
64
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
first?.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", onKey);
|
|
70
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
71
|
+
}, [onFinish]);
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className="modal-backdrop alert-backdrop"
|
|
75
|
+
onMouseDown={(event) => {
|
|
76
|
+
if (event.target === event.currentTarget) onFinish(false);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<section
|
|
80
|
+
className="modal alert-modal"
|
|
81
|
+
role="alertdialog"
|
|
82
|
+
aria-modal="true"
|
|
83
|
+
aria-labelledby="confirm-title"
|
|
84
|
+
aria-describedby="confirm-message"
|
|
85
|
+
>
|
|
86
|
+
<h2 id="confirm-title">{request.title}</h2>
|
|
87
|
+
{request.message && <p id="confirm-message">{request.message}</p>}
|
|
88
|
+
<div className="modal-actions">
|
|
89
|
+
<button
|
|
90
|
+
ref={cancelRef}
|
|
91
|
+
className="secondary"
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => onFinish(false)}
|
|
94
|
+
>
|
|
95
|
+
{request.cancelLabel}
|
|
96
|
+
</button>
|
|
97
|
+
<button
|
|
98
|
+
ref={confirmRef}
|
|
99
|
+
className={request.danger ? "alert-confirm" : "primary"}
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => onFinish(true)}
|
|
102
|
+
>
|
|
103
|
+
{request.confirmLabel}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|