@kyro-cms/admin 0.12.19 → 0.12.21
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/dist/index.cjs +24 -24
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -19
- package/package.json +6 -6
- package/src/components/ApiKeysManager.tsx +177 -227
- package/src/components/AuditLogsPage.tsx +7 -7
- package/src/components/AuthorDashboard.tsx +235 -0
- package/src/components/AutoForm.tsx +15 -4
- package/src/components/CustomerDashboard.tsx +213 -0
- package/src/components/Dashboard.tsx +1 -1
- package/src/components/DashboardMetrics.tsx +74 -9
- package/src/components/DashboardQuickSections.tsx +237 -0
- package/src/components/ListView.tsx +3 -3
- package/src/components/MediaGallery.tsx +2 -2
- package/src/components/PluginsManager.tsx +1 -1
- package/src/components/SessionsManager.tsx +15 -15
- package/src/components/Sidebar.astro +564 -116
- package/src/components/UserManagement.tsx +43 -9
- package/src/components/UserMenu.tsx +123 -61
- package/src/components/autoform/AutoFormEditView.tsx +4 -4
- package/src/components/autoform/AutoFormHeader.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +8 -8
- package/src/components/fields/CodeField.tsx +0 -1
- package/src/components/fields/TabsLayout.tsx +5 -2
- package/src/components/ui/CommandPalette.tsx +1 -1
- package/src/components/ui/Dropdown.tsx +71 -16
- package/src/components/ui/PageHeader.tsx +1 -1
- package/src/components/ui/Pagination.tsx +1 -1
- package/src/components/ui/SeoPreview.tsx +25 -21
- package/src/components/ui/Toast.tsx +3 -2
- package/src/components/ui/Toaster.tsx +1 -1
- package/src/components/users/UserDetail.tsx +74 -29
- package/src/components/users/UserForm.tsx +1 -1
- package/src/components/users/UsersList.tsx +2 -2
- package/src/integration.ts +4 -0
- package/src/layouts/AdminLayout.astro +26 -34
- package/src/layouts/AuthLayout.astro +18 -9
- package/src/lib/auth-server.ts +99 -0
- package/src/pages/403.astro +54 -35
- package/src/pages/[collection]/[id].astro +8 -0
- package/src/pages/[collection]/index.astro +7 -0
- package/src/pages/audit/index.astro +4 -1
- package/src/pages/auth/check-email.astro +146 -0
- package/src/pages/auth/forgot-password.astro +95 -0
- package/src/pages/auth/login.astro +29 -12
- package/src/pages/auth/register.astro +38 -12
- package/src/pages/auth/reset-password.astro +125 -0
- package/src/pages/auth/verify-email.astro +87 -0
- package/src/pages/index.astro +220 -121
- package/src/pages/keys.astro +3 -1
- package/src/pages/media.astro +3 -1
- package/src/pages/preview/[collection]/[id].astro +23 -73
- package/src/pages/roles/index.astro +134 -101
- package/src/pages/settings/[slug].astro +43 -3
- package/src/pages/users/[id].astro +5 -1
- package/src/pages/users/index.astro +4 -0
- package/src/pages/users/new.astro +4 -0
- package/src/pages/webhooks.astro +3 -1
- package/src/routes.ts +16 -0
|
@@ -3,7 +3,10 @@ import AdminLayout from '../../layouts/AdminLayout.astro';
|
|
|
3
3
|
import i18next from '../../lib/i18n';
|
|
4
4
|
import { AuditLogsPage } from '../../components/AuditLogsPage';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { requireAuthServer } from '../../lib/auth-server';
|
|
7
|
+
|
|
8
|
+
const redirect = await requireAuthServer(Astro.request, { collectionRead: 'audit_logs' });
|
|
9
|
+
if (redirect) return redirect;
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
<AdminLayout title={i18next.t("pages." + "Audit Logs".replace(/ /g, ""), { defaultValue: "Audit Logs" })}>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
|
+
import { adminPath, apiPath } from "../../lib/paths";
|
|
4
|
+
import { getSiteSettings } from "../../lib/globals";
|
|
5
|
+
|
|
6
|
+
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
7
|
+
const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
8
|
+
|
|
9
|
+
const url = new URL(Astro.request.url);
|
|
10
|
+
const email = url.searchParams.get("email") || "";
|
|
11
|
+
const type = url.searchParams.get("type") || "register";
|
|
12
|
+
|
|
13
|
+
let title = "Check your email";
|
|
14
|
+
let description = `We sent a confirmation link to ${email ? email : "your email address"}. Click the link to complete your request.`;
|
|
15
|
+
|
|
16
|
+
if (type === "register") {
|
|
17
|
+
title = "Verify your email address";
|
|
18
|
+
description = `We've sent an account verification link to ${email ? email : "your email address"}. Please verify your email to activate your ${siteName} account.`;
|
|
19
|
+
} else if (type === "reset") {
|
|
20
|
+
title = "Password reset link sent";
|
|
21
|
+
description = `We've sent a password reset link to ${email ? email : "your email address"}. Check your inbox to set a new password.`;
|
|
22
|
+
} else if (type === "magic-link") {
|
|
23
|
+
title = "Magic link sent";
|
|
24
|
+
description = `We've sent a one-time sign in link to ${email ? email : "your email address"}.`;
|
|
25
|
+
}
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<AuthLayout title={title}>
|
|
29
|
+
<div class="surface-tile p-8 w-full text-center" style="max-width: 440px;">
|
|
30
|
+
<!-- Mail Icon Badge -->
|
|
31
|
+
<div class="mx-auto w-16 h-16 rounded-2xl bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)] flex items-center justify-center mb-6 shadow-sm">
|
|
32
|
+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
|
34
|
+
</svg>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<h1 class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
38
|
+
{title}
|
|
39
|
+
</h1>
|
|
40
|
+
|
|
41
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-3 leading-relaxed">
|
|
42
|
+
{description}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
{email && (
|
|
46
|
+
<div class="mt-4 inline-block px-3 py-1.5 rounded-lg bg-[var(--kyro-input-bg)] border border-[var(--kyro-border)] text-xs font-mono font-medium text-[var(--kyro-text-primary)]">
|
|
47
|
+
{email}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<!-- Helper Actions -->
|
|
52
|
+
<div class="mt-8 space-y-3">
|
|
53
|
+
{email && email.includes("@gmail.com") && (
|
|
54
|
+
<a
|
|
55
|
+
href="https://mail.google.com"
|
|
56
|
+
target="_blank"
|
|
57
|
+
class="w-full py-3 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg flex items-center justify-center gap-2"
|
|
58
|
+
>
|
|
59
|
+
Open Gmail Inbox
|
|
60
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
62
|
+
</svg>
|
|
63
|
+
</a>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{email && (email.includes("@outlook.com") || email.includes("@hotmail.com")) && (
|
|
67
|
+
<a
|
|
68
|
+
href="https://outlook.live.com"
|
|
69
|
+
target="_blank"
|
|
70
|
+
class="w-full py-3 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg flex items-center justify-center gap-2"
|
|
71
|
+
>
|
|
72
|
+
Open Outlook Inbox
|
|
73
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
74
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
75
|
+
</svg>
|
|
76
|
+
</a>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<button
|
|
80
|
+
id="resend-btn"
|
|
81
|
+
class="w-full py-3 px-4 bg-[var(--kyro-input-bg)] border border-[var(--kyro-border)] text-[var(--kyro-text-primary)] rounded-xl text-sm font-semibold hover:bg-[var(--kyro-border)]/50 transition-colors"
|
|
82
|
+
>
|
|
83
|
+
Resend Email
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<div id="resend-message" class="hidden text-xs font-medium mt-2"></div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Back to Login -->
|
|
90
|
+
<div class="mt-8 pt-6 border-t border-[var(--kyro-border)]">
|
|
91
|
+
<a
|
|
92
|
+
href={`${adminPath}/login`}
|
|
93
|
+
class="text-sm font-medium text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors inline-flex items-center gap-1.5"
|
|
94
|
+
>
|
|
95
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
96
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
|
97
|
+
</svg>
|
|
98
|
+
Back to sign in
|
|
99
|
+
</a>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<script is:inline define:vars={{ apiPath, email, type }}>
|
|
104
|
+
const resendBtn = document.getElementById("resend-btn");
|
|
105
|
+
const resendMsg = document.getElementById("resend-message");
|
|
106
|
+
|
|
107
|
+
resendBtn?.addEventListener("click", async () => {
|
|
108
|
+
if (!email) {
|
|
109
|
+
if (resendMsg) {
|
|
110
|
+
resendMsg.textContent = "Please return to sign in.";
|
|
111
|
+
resendMsg.className = "block text-xs font-medium text-amber-600 mt-2";
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resendBtn.disabled = true;
|
|
117
|
+
resendBtn.textContent = "Sending...";
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const endpoint = type === "reset" ? "/auth/forgot-password" : "/auth/register";
|
|
121
|
+
const res = await fetch(apiPath + endpoint, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ email }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (resendMsg) {
|
|
128
|
+
resendMsg.textContent = "Confirmation email resent successfully!";
|
|
129
|
+
resendMsg.className = "block text-xs font-medium text-emerald-600 mt-2";
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (resendMsg) {
|
|
133
|
+
resendMsg.textContent = "Failed to resend. Please try again.";
|
|
134
|
+
resendMsg.className = "block text-xs font-medium text-rose-600 mt-2";
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
if (resendBtn) {
|
|
139
|
+
resendBtn.disabled = false;
|
|
140
|
+
resendBtn.textContent = "Resend Email";
|
|
141
|
+
}
|
|
142
|
+
}, 5000);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
</AuthLayout>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
|
+
import { adminPath, apiPath } from "../../lib/paths";
|
|
4
|
+
import { getSiteSettings } from "../../lib/globals";
|
|
5
|
+
|
|
6
|
+
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
7
|
+
const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<AuthLayout title="Forgot Password">
|
|
11
|
+
<div class="surface-tile p-8 w-full" style="max-width: 420px;">
|
|
12
|
+
<div class="text-center mb-8">
|
|
13
|
+
<h1 class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
14
|
+
Forgot password?
|
|
15
|
+
</h1>
|
|
16
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-2">
|
|
17
|
+
Enter your email address and we'll send you a password reset link.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<form id="forgot-form" class="space-y-5">
|
|
22
|
+
<div>
|
|
23
|
+
<label for="email" class="block text-sm font-medium text-[var(--kyro-text-primary)] mb-2">
|
|
24
|
+
Email address
|
|
25
|
+
</label>
|
|
26
|
+
<input
|
|
27
|
+
type="email"
|
|
28
|
+
id="email"
|
|
29
|
+
name="email"
|
|
30
|
+
required
|
|
31
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
32
|
+
placeholder="admin@example.com"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
37
|
+
|
|
38
|
+
<button
|
|
39
|
+
type="submit"
|
|
40
|
+
class="w-full py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg"
|
|
41
|
+
>
|
|
42
|
+
Send Reset Link
|
|
43
|
+
</button>
|
|
44
|
+
</form>
|
|
45
|
+
|
|
46
|
+
<p class="text-center text-sm text-[var(--kyro-text-secondary)] mt-6">
|
|
47
|
+
Remember your password?{" "}
|
|
48
|
+
<a href={`${adminPath}/login`} class="font-medium text-[var(--kyro-text-primary)] hover:underline">
|
|
49
|
+
Sign in
|
|
50
|
+
</a>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<script is:inline define:vars={{ apiPath, adminPath }}>
|
|
55
|
+
document.getElementById("forgot-form")?.addEventListener("submit", async (e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
const form = e.target;
|
|
58
|
+
const message = document.getElementById("form-message");
|
|
59
|
+
const button = form.querySelector('button[type="submit"]');
|
|
60
|
+
|
|
61
|
+
const email = form.email.value;
|
|
62
|
+
|
|
63
|
+
button.disabled = true;
|
|
64
|
+
button.textContent = "Sending link...";
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(apiPath + "/auth/forgot-password", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ email }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
|
|
75
|
+
if (res.ok && data.success) {
|
|
76
|
+
message.textContent = "Reset link dispatched!";
|
|
77
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-emerald-50 text-emerald-600";
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
window.location.href = `${adminPath}/auth/check-email?email=${encodeURIComponent(email)}&type=reset`;
|
|
80
|
+
}, 600);
|
|
81
|
+
} else {
|
|
82
|
+
message.textContent = data.error || "Failed to send reset link";
|
|
83
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
84
|
+
button.disabled = false;
|
|
85
|
+
button.textContent = "Send Reset Link";
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
message.textContent = "Connection error";
|
|
89
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
90
|
+
button.disabled = false;
|
|
91
|
+
button.textContent = "Send Reset Link";
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
</AuthLayout>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
3
|
import { adminPath, apiPath } from "../../lib/paths";
|
|
4
|
+
import { getGlobal } from "../../lib/globals";
|
|
5
|
+
|
|
6
|
+
const systemSettings = await getGlobal("system", { request: Astro.request });
|
|
7
|
+
const registrationEnabled = systemSettings?.enableRegistration ?? true;
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<AuthLayout title="Sign In">
|
|
@@ -34,11 +38,18 @@ import { adminPath, apiPath } from "../../lib/paths";
|
|
|
34
38
|
</div>
|
|
35
39
|
|
|
36
40
|
<div>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
<div class="flex items-center justify-between mb-2">
|
|
42
|
+
<label
|
|
43
|
+
for="password"
|
|
44
|
+
class="block text-sm font-medium text-[var(--kyro-text-primary)]"
|
|
45
|
+
>Password</label
|
|
46
|
+
>
|
|
47
|
+
<a
|
|
48
|
+
href={`${adminPath}/auth/forgot-password`}
|
|
49
|
+
class="text-xs font-medium text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
50
|
+
>Forgot password?</a
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
42
53
|
<input
|
|
43
54
|
type="password"
|
|
44
55
|
id="password"
|
|
@@ -60,13 +71,19 @@ import { adminPath, apiPath } from "../../lib/paths";
|
|
|
60
71
|
</button>
|
|
61
72
|
</form>
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
{
|
|
75
|
+
registrationEnabled && (
|
|
76
|
+
<p class="text-center text-sm text-[var(--kyro-text-secondary)] mt-6">
|
|
77
|
+
Don't have an account?{" "}
|
|
78
|
+
<a
|
|
79
|
+
href={`${adminPath}/register`}
|
|
80
|
+
class="font-medium text-[var(--kyro-text-primary)] hover:underline"
|
|
81
|
+
>
|
|
82
|
+
Register
|
|
83
|
+
</a>
|
|
84
|
+
</p>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
70
87
|
</div>
|
|
71
88
|
|
|
72
89
|
<script define:vars={{ apiPath, adminPath }}>
|
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
3
|
|
|
4
4
|
import { adminPath, apiPath } from "../../lib/paths";
|
|
5
|
-
import { getSiteSettings } from "../../lib/globals";
|
|
5
|
+
import { getSiteSettings, getGlobal } from "../../lib/globals";
|
|
6
6
|
|
|
7
7
|
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
8
|
+
const systemSettings = await getGlobal("system", { request: Astro.request });
|
|
8
9
|
const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
10
|
+
const registrationEnabled = systemSettings?.enableRegistration ?? true;
|
|
9
11
|
---
|
|
10
12
|
|
|
11
|
-
<AuthLayout title="Create Account">
|
|
12
|
-
|
|
13
|
+
<AuthLayout title={registrationEnabled ? "Create Account" : "Registration Disabled"}>
|
|
14
|
+
{
|
|
15
|
+
!registrationEnabled ? (
|
|
16
|
+
<div class="surface-tile p-8 w-full text-center" style="max-width: 420px;">
|
|
17
|
+
<div class="mx-auto w-16 h-16 rounded-2xl bg-amber-500/10 text-amber-600 flex items-center justify-center mb-6 shadow-sm">
|
|
18
|
+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<h1 class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
23
|
+
Registration Disabled
|
|
24
|
+
</h1>
|
|
25
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-3 leading-relaxed">
|
|
26
|
+
New user registration is currently disabled by the site administrator.
|
|
27
|
+
</p>
|
|
28
|
+
<div class="mt-8">
|
|
29
|
+
<a
|
|
30
|
+
href={`${adminPath}/login`}
|
|
31
|
+
class="w-full py-3 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg inline-block"
|
|
32
|
+
>
|
|
33
|
+
Sign In to {siteName} →
|
|
34
|
+
</a>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
) : (
|
|
38
|
+
<div class="surface-tile p-8 w-full" style="max-width: 420px;">
|
|
13
39
|
<div class="text-center mb-8">
|
|
14
40
|
<h1
|
|
15
41
|
class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]"
|
|
@@ -91,6 +117,8 @@ const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
|
91
117
|
>
|
|
92
118
|
</p>
|
|
93
119
|
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
94
122
|
|
|
95
123
|
<script is:inline define:vars={{ apiPath, adminPath }}>
|
|
96
124
|
document
|
|
@@ -133,25 +161,23 @@ const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
|
133
161
|
|
|
134
162
|
if (res.ok && data.success && data.user) {
|
|
135
163
|
// Cookies set server-side via Set-Cookie headers
|
|
136
|
-
// Store user in memory only (not localStorage)
|
|
137
164
|
window.__kyroAuth = { user: data.user, verified: true };
|
|
138
165
|
message.textContent = data.isFirstUser
|
|
139
166
|
? "Super admin account created!"
|
|
140
167
|
: "Account created successfully!";
|
|
141
168
|
message.className =
|
|
142
|
-
"block p-3 rounded-xl text-sm font-
|
|
169
|
+
"block p-3 rounded-xl text-sm font-medium bg-emerald-50 text-emerald-600";
|
|
143
170
|
setTimeout(() => {
|
|
144
171
|
window.location.href = adminPath;
|
|
145
|
-
},
|
|
172
|
+
}, 800);
|
|
146
173
|
} else if (res.ok && data.success) {
|
|
147
|
-
// Email verification required
|
|
148
|
-
message.textContent =
|
|
149
|
-
data.message || "Please check your email to verify your account.";
|
|
174
|
+
// Email verification required -> Navigate to Check Email page
|
|
175
|
+
message.textContent = "Account created! Redirecting to email confirmation...";
|
|
150
176
|
message.className =
|
|
151
|
-
"block p-3 rounded-xl text-sm font-
|
|
177
|
+
"block p-3 rounded-xl text-sm font-medium bg-emerald-50 text-emerald-600";
|
|
152
178
|
setTimeout(() => {
|
|
153
|
-
window.location.href = adminPath
|
|
154
|
-
},
|
|
179
|
+
window.location.href = `${adminPath}/auth/check-email?email=${encodeURIComponent(email)}&type=register`;
|
|
180
|
+
}, 600);
|
|
155
181
|
} else {
|
|
156
182
|
message.textContent = data.error || "Registration failed";
|
|
157
183
|
message.className =
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
|
+
import { adminPath, apiPath } from "../../lib/paths";
|
|
4
|
+
|
|
5
|
+
const url = new URL(Astro.request.url);
|
|
6
|
+
const token = url.searchParams.get("token") || "";
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<AuthLayout title="Reset Password">
|
|
10
|
+
<div class="surface-tile p-8 w-full" style="max-width: 420px;">
|
|
11
|
+
<div class="text-center mb-8">
|
|
12
|
+
<h1 class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
13
|
+
Set new password
|
|
14
|
+
</h1>
|
|
15
|
+
<p class="text-sm text-[var(--kyro-text-secondary)] mt-2">
|
|
16
|
+
Please choose a strong new password for your account.
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<form id="reset-form" class="space-y-5">
|
|
21
|
+
<input type="hidden" name="token" value={token} />
|
|
22
|
+
|
|
23
|
+
<div>
|
|
24
|
+
<label for="newPassword" class="block text-sm font-medium text-[var(--kyro-text-primary)] mb-2">
|
|
25
|
+
New Password
|
|
26
|
+
</label>
|
|
27
|
+
<input
|
|
28
|
+
type="password"
|
|
29
|
+
id="newPassword"
|
|
30
|
+
name="newPassword"
|
|
31
|
+
required
|
|
32
|
+
minlength="8"
|
|
33
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
34
|
+
placeholder="Minimum 8 characters"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div>
|
|
39
|
+
<label for="confirmPassword" class="block text-sm font-medium text-[var(--kyro-text-primary)] mb-2">
|
|
40
|
+
Confirm New Password
|
|
41
|
+
</label>
|
|
42
|
+
<input
|
|
43
|
+
type="password"
|
|
44
|
+
id="confirmPassword"
|
|
45
|
+
name="confirmPassword"
|
|
46
|
+
required
|
|
47
|
+
minlength="8"
|
|
48
|
+
class="w-full px-4 py-3 border border-[var(--kyro-border)] bg-[var(--kyro-input-bg)] rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-[var(--kyro-sidebar-active)] text-[var(--kyro-text-primary)]"
|
|
49
|
+
placeholder="Re-enter new password"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div id="form-message" class="hidden p-3 rounded-xl text-sm font-bold"></div>
|
|
54
|
+
|
|
55
|
+
<button
|
|
56
|
+
type="submit"
|
|
57
|
+
class="w-full py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg"
|
|
58
|
+
>
|
|
59
|
+
Update Password
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
|
|
63
|
+
<p class="text-center text-sm text-[var(--kyro-text-secondary)] mt-6">
|
|
64
|
+
<a href={`${adminPath}/login`} class="font-medium text-[var(--kyro-text-primary)] hover:underline">
|
|
65
|
+
Back to sign in
|
|
66
|
+
</a>
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script is:inline define:vars={{ apiPath, adminPath }}>
|
|
71
|
+
document.getElementById("reset-form")?.addEventListener("submit", async (e) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
const form = e.target;
|
|
74
|
+
const message = document.getElementById("form-message");
|
|
75
|
+
const button = form.querySelector('button[type="submit"]');
|
|
76
|
+
|
|
77
|
+
const token = form.token.value;
|
|
78
|
+
const newPassword = form.newPassword.value;
|
|
79
|
+
const confirmPassword = form.confirmPassword.value;
|
|
80
|
+
|
|
81
|
+
if (!token) {
|
|
82
|
+
message.textContent = "Invalid or missing password reset token";
|
|
83
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (newPassword !== confirmPassword) {
|
|
88
|
+
message.textContent = "Passwords do not match";
|
|
89
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
button.disabled = true;
|
|
94
|
+
button.textContent = "Updating password...";
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(apiPath + "/auth/reset-password", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({ token, newPassword, confirmPassword }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const data = await res.json();
|
|
104
|
+
|
|
105
|
+
if (res.ok && data.success) {
|
|
106
|
+
message.textContent = "Password reset successfully!";
|
|
107
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-emerald-50 text-emerald-600";
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
window.location.href = `${adminPath}/login`;
|
|
110
|
+
}, 1200);
|
|
111
|
+
} else {
|
|
112
|
+
message.textContent = data.error || "Password reset failed";
|
|
113
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
114
|
+
button.disabled = false;
|
|
115
|
+
button.textContent = "Update Password";
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
message.textContent = "Connection error";
|
|
119
|
+
message.className = "block p-3 rounded-xl text-sm font-medium bg-red-50 text-red-600";
|
|
120
|
+
button.disabled = false;
|
|
121
|
+
button.textContent = "Update Password";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
</script>
|
|
125
|
+
</AuthLayout>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AuthLayout from "../../layouts/AuthLayout.astro";
|
|
3
|
+
import { adminPath, apiPath } from "../../lib/paths";
|
|
4
|
+
import { getSiteSettings } from "../../lib/globals";
|
|
5
|
+
|
|
6
|
+
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
7
|
+
const siteName = siteSettings?.siteName || "Kyro CMS";
|
|
8
|
+
|
|
9
|
+
const url = new URL(Astro.request.url);
|
|
10
|
+
const token = url.searchParams.get("token") || "";
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<AuthLayout title="Email Verification">
|
|
14
|
+
<div class="surface-tile p-8 w-full text-center" style="max-width: 420px;">
|
|
15
|
+
<div id="status-icon" class="mx-auto w-16 h-16 rounded-2xl bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)] flex items-center justify-center mb-6 shadow-sm">
|
|
16
|
+
<svg class="w-8 h-8 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
17
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
18
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
19
|
+
</svg>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<h1 id="status-title" class="text-2xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
23
|
+
Verifying your email...
|
|
24
|
+
</h1>
|
|
25
|
+
|
|
26
|
+
<p id="status-subtitle" class="text-sm text-[var(--kyro-text-secondary)] mt-3 leading-relaxed">
|
|
27
|
+
Please wait while we validate your confirmation link.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<div id="action-container" class="mt-8 hidden">
|
|
31
|
+
<a
|
|
32
|
+
href={`${adminPath}/login`}
|
|
33
|
+
class="w-full py-3 px-4 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-sm font-bold hover:opacity-90 transition-colors shadow-lg inline-block"
|
|
34
|
+
>
|
|
35
|
+
Sign In to {siteName} →
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script is:inline define:vars={{ apiPath, token, adminPath }}>
|
|
41
|
+
async function runVerification() {
|
|
42
|
+
const icon = document.getElementById("status-icon");
|
|
43
|
+
const title = document.getElementById("status-title");
|
|
44
|
+
const subtitle = document.getElementById("status-subtitle");
|
|
45
|
+
const action = document.getElementById("action-container");
|
|
46
|
+
|
|
47
|
+
if (!token) {
|
|
48
|
+
if (title) title.textContent = "Missing Token";
|
|
49
|
+
if (subtitle) subtitle.textContent = "No email verification token was provided in the URL.";
|
|
50
|
+
if (icon) {
|
|
51
|
+
icon.className = "mx-auto w-16 h-16 rounded-2xl bg-rose-500/10 text-rose-600 flex items-center justify-center mb-6 shadow-sm";
|
|
52
|
+
icon.innerHTML = `<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`;
|
|
53
|
+
}
|
|
54
|
+
if (action) action.classList.remove("hidden");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`${apiPath}/auth/verify-email?token=${encodeURIComponent(token)}`);
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
|
|
62
|
+
if (res.ok && data.success) {
|
|
63
|
+
if (title) title.textContent = "Email Verified!";
|
|
64
|
+
if (subtitle) subtitle.textContent = data.message || "Your email address has been confirmed. You can now access your account.";
|
|
65
|
+
if (icon) {
|
|
66
|
+
icon.className = "mx-auto w-16 h-16 rounded-2xl bg-emerald-500/10 text-emerald-600 flex items-center justify-center mb-6 shadow-sm";
|
|
67
|
+
icon.innerHTML = `<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
if (title) title.textContent = "Verification Failed";
|
|
71
|
+
if (subtitle) subtitle.textContent = data.error || "The verification token is invalid or has expired.";
|
|
72
|
+
if (icon) {
|
|
73
|
+
icon.className = "mx-auto w-16 h-16 rounded-2xl bg-rose-500/10 text-rose-600 flex items-center justify-center mb-6 shadow-sm";
|
|
74
|
+
icon.innerHTML = `<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (title) title.textContent = "Connection Error";
|
|
79
|
+
if (subtitle) subtitle.textContent = "Failed to connect to verification server.";
|
|
80
|
+
} finally {
|
|
81
|
+
if (action) action.classList.remove("hidden");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
runVerification();
|
|
86
|
+
</script>
|
|
87
|
+
</AuthLayout>
|