@kyro-cms/admin 0.1.5 → 0.1.7
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/README.md +149 -51
- package/package.json +52 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +50 -0
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +116 -28
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +286 -0
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +50 -20
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
- package/src/pages/index.astro +0 -225
|
@@ -1,15 +1,68 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import {
|
|
2
|
+
import { getAuthAdapter } from "../../../lib/db";
|
|
3
|
+
import { createAuditContext } from "@kyro-cms/core";
|
|
3
4
|
import jwt from "jsonwebtoken";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
4
6
|
|
|
5
7
|
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
6
|
-
const
|
|
8
|
+
const REFRESH_SECRET =
|
|
9
|
+
process.env.REFRESH_SECRET ||
|
|
10
|
+
process.env.JWT_SECRET ||
|
|
11
|
+
"change-me-in-production";
|
|
12
|
+
|
|
13
|
+
const ACCESS_TOKEN_EXPIRY = process.env.JWT_EXPIRES_IN || "24h";
|
|
14
|
+
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || "7d";
|
|
7
15
|
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
function getExpirySeconds(expiry: string): number {
|
|
18
|
+
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
19
|
+
if (!match) return 86400;
|
|
20
|
+
const value = parseInt(match[1]);
|
|
21
|
+
const unit = match[2];
|
|
22
|
+
switch (unit) {
|
|
23
|
+
case "s":
|
|
24
|
+
return value;
|
|
25
|
+
case "m":
|
|
26
|
+
return value * 60;
|
|
27
|
+
case "h":
|
|
28
|
+
return value * 3600;
|
|
29
|
+
case "d":
|
|
30
|
+
return value * 86400;
|
|
31
|
+
default:
|
|
32
|
+
return 86400;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateTokens(user: { id: string; email: string; role: string }) {
|
|
37
|
+
const accessToken = jwt.sign(
|
|
38
|
+
{ sub: user.id, email: user.email, role: user.role, type: "access" },
|
|
39
|
+
JWT_SECRET,
|
|
40
|
+
{ expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const refreshToken = jwt.sign(
|
|
44
|
+
{
|
|
45
|
+
sub: user.id,
|
|
46
|
+
email: user.email,
|
|
47
|
+
role: user.role,
|
|
48
|
+
type: "refresh",
|
|
49
|
+
jti: randomBytes(16).toString("hex"),
|
|
50
|
+
},
|
|
51
|
+
REFRESH_SECRET,
|
|
52
|
+
{ expiresIn: REFRESH_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return { accessToken, refreshToken };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createAuthCookies(token: string, refreshToken: string) {
|
|
59
|
+
const accessMaxAge = getExpirySeconds(ACCESS_TOKEN_EXPIRY);
|
|
60
|
+
const refreshMaxAge = getExpirySeconds(REFRESH_TOKEN_EXPIRY);
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
`auth_token=${token}; Path=/; Max-Age=${accessMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
64
|
+
`refresh_token=${refreshToken}; Path=/; Max-Age=${refreshMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
65
|
+
].join(", ");
|
|
13
66
|
}
|
|
14
67
|
|
|
15
68
|
export const POST: APIRoute = async ({ request }) => {
|
|
@@ -35,77 +88,94 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
35
88
|
});
|
|
36
89
|
}
|
|
37
90
|
|
|
91
|
+
// Password strength validation
|
|
92
|
+
const passwordErrors: string[] = [];
|
|
93
|
+
|
|
38
94
|
if (password.length < 8) {
|
|
95
|
+
passwordErrors.push("at least 8 characters");
|
|
96
|
+
}
|
|
97
|
+
if (!/[a-z]/.test(password)) {
|
|
98
|
+
passwordErrors.push("one lowercase letter");
|
|
99
|
+
}
|
|
100
|
+
if (!/[A-Z]/.test(password)) {
|
|
101
|
+
passwordErrors.push("one uppercase letter");
|
|
102
|
+
}
|
|
103
|
+
if (!/[0-9]/.test(password)) {
|
|
104
|
+
passwordErrors.push("one number");
|
|
105
|
+
}
|
|
106
|
+
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
107
|
+
passwordErrors.push('one special character (!@#$%^&*(),.?":{}|<>)');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (passwordErrors.length > 0) {
|
|
39
111
|
return new Response(
|
|
40
|
-
JSON.stringify({
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
error:
|
|
114
|
+
"Password is too weak. Must contain: " + passwordErrors.join(", "),
|
|
115
|
+
}),
|
|
41
116
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
42
117
|
);
|
|
43
118
|
}
|
|
44
119
|
|
|
45
|
-
const adapter = await
|
|
46
|
-
await adapter.connect();
|
|
120
|
+
const adapter = await getAuthAdapter();
|
|
47
121
|
|
|
48
122
|
const existingUser = await adapter.findUserByEmail(email);
|
|
49
123
|
if (existingUser) {
|
|
50
|
-
await adapter.disconnect();
|
|
51
124
|
return new Response(
|
|
52
125
|
JSON.stringify({ error: "Email already registered" }),
|
|
53
126
|
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
54
127
|
);
|
|
55
128
|
}
|
|
56
129
|
|
|
57
|
-
const isFirstUser = !(await adapter.hasAnyUsers());
|
|
130
|
+
const isFirstUser = !(await adapter.hasAnyUsers?.());
|
|
58
131
|
|
|
59
132
|
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
60
|
-
await adapter.disconnect();
|
|
61
133
|
return new Response(
|
|
62
134
|
JSON.stringify({ error: "Registration is disabled" }),
|
|
63
135
|
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
64
136
|
);
|
|
65
137
|
}
|
|
66
138
|
|
|
67
|
-
const passwordHash = await adapter.hashPassword(password);
|
|
68
139
|
const user = await adapter.createUser({
|
|
69
140
|
email,
|
|
70
|
-
|
|
71
|
-
role: isFirstUser ? "super_admin" : "
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
if (isFirstUser) {
|
|
75
|
-
await adapter.updateUser(user.id, { emailVerified: true });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const session = await adapter.createSession(user.id, {
|
|
79
|
-
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
80
|
-
userAgent: request.headers.get("user-agent") || "",
|
|
141
|
+
password,
|
|
142
|
+
role: isFirstUser ? "super_admin" : "customer",
|
|
81
143
|
});
|
|
82
144
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
145
|
+
const context = createAuditContext(request);
|
|
146
|
+
await adapter.createAuditLog({
|
|
147
|
+
action: "register",
|
|
148
|
+
userId: user.id,
|
|
149
|
+
userEmail: user.email,
|
|
150
|
+
role: user.role,
|
|
151
|
+
resource: "auth",
|
|
152
|
+
success: true,
|
|
153
|
+
metadata: {
|
|
154
|
+
method: "password",
|
|
155
|
+
isFirstUser,
|
|
89
156
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
await adapter.disconnect();
|
|
157
|
+
...context,
|
|
158
|
+
});
|
|
95
159
|
|
|
96
|
-
const {
|
|
160
|
+
const { accessToken, refreshToken } = generateTokens(user);
|
|
97
161
|
|
|
98
162
|
return new Response(
|
|
99
163
|
JSON.stringify({
|
|
100
164
|
success: true,
|
|
101
165
|
isFirstUser,
|
|
102
|
-
user:
|
|
103
|
-
|
|
104
|
-
|
|
166
|
+
user: {
|
|
167
|
+
id: user.id,
|
|
168
|
+
email: user.email,
|
|
169
|
+
role: user.role,
|
|
170
|
+
},
|
|
171
|
+
expiresIn: getExpirySeconds(ACCESS_TOKEN_EXPIRY),
|
|
105
172
|
}),
|
|
106
173
|
{
|
|
107
174
|
status: 201,
|
|
108
|
-
headers: {
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
"Set-Cookie": createAuthCookies(accessToken, refreshToken),
|
|
178
|
+
},
|
|
109
179
|
},
|
|
110
180
|
);
|
|
111
181
|
} catch (error) {
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import {
|
|
3
|
-
import bcrypt from "bcryptjs";
|
|
4
|
-
|
|
5
|
-
async function getAuthApi() {
|
|
6
|
-
return new SQLiteAuthAdapter({
|
|
7
|
-
path: process.env.KYRO_AUTH_DB_PATH || "./data/auth.db",
|
|
8
|
-
});
|
|
9
|
-
}
|
|
2
|
+
import { getAuthAdapter } from "../../../lib/db";
|
|
10
3
|
|
|
11
4
|
export const GET: APIRoute = async ({ url }) => {
|
|
12
5
|
const page = parseInt(url.searchParams.get("page") || "1");
|
|
@@ -14,90 +7,28 @@ export const GET: APIRoute = async ({ url }) => {
|
|
|
14
7
|
const search = url.searchParams.get("search") || "";
|
|
15
8
|
|
|
16
9
|
try {
|
|
17
|
-
const adapter = await
|
|
18
|
-
await adapter.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const { password_hash, ...safeUser } = row;
|
|
32
|
-
users.push({
|
|
33
|
-
id: row.id,
|
|
34
|
-
email: row.email,
|
|
35
|
-
role: row.role,
|
|
36
|
-
tenantId: row.tenant_id,
|
|
37
|
-
emailVerified: row.email_verified === 1,
|
|
38
|
-
locked: row.locked === 1,
|
|
39
|
-
lastLogin: row.last_login,
|
|
40
|
-
failedLoginAttempts: row.failed_login_attempts || 0,
|
|
41
|
-
createdAt: row.created_at,
|
|
42
|
-
updatedAt: row.updated_at,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const totalResult = (adapter as any).db
|
|
47
|
-
.prepare("SELECT COUNT(*) as count FROM kyro_users WHERE email LIKE ?")
|
|
48
|
-
.get(`%${search}%`) as { count: number };
|
|
49
|
-
|
|
50
|
-
await adapter.disconnect();
|
|
51
|
-
|
|
52
|
-
return new Response(
|
|
53
|
-
JSON.stringify({
|
|
54
|
-
docs: users,
|
|
55
|
-
totalDocs: totalResult.count,
|
|
56
|
-
page,
|
|
57
|
-
limit,
|
|
58
|
-
totalPages: Math.ceil(totalResult.count / limit),
|
|
59
|
-
}),
|
|
60
|
-
{
|
|
61
|
-
status: 200,
|
|
62
|
-
headers: { "Content-Type": "application/json" },
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Get all users with pagination
|
|
68
|
-
const result = (adapter as any).db
|
|
69
|
-
.prepare("SELECT * FROM kyro_users LIMIT ? OFFSET ?")
|
|
70
|
-
.all(limit, (page - 1) * limit);
|
|
71
|
-
|
|
72
|
-
for (const row of result) {
|
|
73
|
-
const { password_hash, ...safeUser } = row;
|
|
74
|
-
users.push({
|
|
75
|
-
id: row.id,
|
|
76
|
-
email: row.email,
|
|
77
|
-
role: row.role,
|
|
78
|
-
tenantId: row.tenant_id,
|
|
79
|
-
emailVerified: row.email_verified === 1,
|
|
80
|
-
locked: row.locked === 1,
|
|
81
|
-
lastLogin: row.last_login,
|
|
82
|
-
failedLoginAttempts: row.failed_login_attempts || 0,
|
|
83
|
-
createdAt: row.created_at,
|
|
84
|
-
updatedAt: row.updated_at,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const totalResult = (adapter as any).db
|
|
89
|
-
.prepare("SELECT COUNT(*) as count FROM kyro_users")
|
|
90
|
-
.get() as { count: number };
|
|
91
|
-
|
|
92
|
-
await adapter.disconnect();
|
|
10
|
+
const adapter = await getAuthAdapter();
|
|
11
|
+
const allUsers = await adapter.findAllUsers();
|
|
12
|
+
|
|
13
|
+
const users = allUsers.map((u) => ({
|
|
14
|
+
id: u.id,
|
|
15
|
+
email: u.email,
|
|
16
|
+
name: u.name,
|
|
17
|
+
role: u.role,
|
|
18
|
+
tenantId: u.tenantId,
|
|
19
|
+
emailVerified: u.emailVerified,
|
|
20
|
+
locked: u.locked,
|
|
21
|
+
lastLogin: u.lastLogin,
|
|
22
|
+
createdAt: u.createdAt,
|
|
23
|
+
}));
|
|
93
24
|
|
|
94
25
|
return new Response(
|
|
95
26
|
JSON.stringify({
|
|
96
27
|
docs: users,
|
|
97
|
-
totalDocs:
|
|
28
|
+
totalDocs: users.length,
|
|
98
29
|
page,
|
|
99
30
|
limit,
|
|
100
|
-
totalPages: Math.ceil(
|
|
31
|
+
totalPages: Math.ceil(users.length / limit),
|
|
101
32
|
}),
|
|
102
33
|
{
|
|
103
34
|
status: 200,
|
|
@@ -123,7 +54,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|
|
123
54
|
export const POST: APIRoute = async ({ request }) => {
|
|
124
55
|
try {
|
|
125
56
|
const body = await request.json();
|
|
126
|
-
const { email, password,
|
|
57
|
+
const { email, password, name, role } = body;
|
|
127
58
|
|
|
128
59
|
if (!email || !password) {
|
|
129
60
|
return new Response(
|
|
@@ -135,30 +66,24 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
135
66
|
);
|
|
136
67
|
}
|
|
137
68
|
|
|
138
|
-
const adapter = await
|
|
139
|
-
await adapter.connect();
|
|
69
|
+
const adapter = await getAuthAdapter();
|
|
140
70
|
|
|
141
71
|
const existing = await adapter.findUserByEmail(email);
|
|
142
72
|
if (existing) {
|
|
143
|
-
await adapter.disconnect();
|
|
144
73
|
return new Response(JSON.stringify({ error: "Email already exists" }), {
|
|
145
74
|
status: 400,
|
|
146
75
|
headers: { "Content-Type": "application/json" },
|
|
147
76
|
});
|
|
148
77
|
}
|
|
149
78
|
|
|
150
|
-
const passwordHash = await bcrypt.hash(password, 12);
|
|
151
79
|
const user = await adapter.createUser({
|
|
152
80
|
email,
|
|
153
|
-
|
|
81
|
+
password,
|
|
82
|
+
name,
|
|
154
83
|
role: role || "customer",
|
|
155
|
-
tenantId,
|
|
156
84
|
});
|
|
157
85
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const { passwordHash: _, ...safeUser } = user;
|
|
161
|
-
return new Response(JSON.stringify({ data: safeUser }), {
|
|
86
|
+
return new Response(JSON.stringify({ data: user }), {
|
|
162
87
|
status: 201,
|
|
163
88
|
headers: { "Content-Type": "application/json" },
|
|
164
89
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { collections } from "@/lib/config";
|
|
3
|
+
|
|
4
|
+
export const GET: APIRoute = async () => {
|
|
5
|
+
try {
|
|
6
|
+
const collectionList = Object.entries(collections).map(
|
|
7
|
+
([slug, config]) => ({
|
|
8
|
+
name: config.label || slug,
|
|
9
|
+
slug,
|
|
10
|
+
description:
|
|
11
|
+
config.admin?.description ||
|
|
12
|
+
`Manage ${config.label || slug} documents`,
|
|
13
|
+
fields: Object.keys(config.fields || {}).length,
|
|
14
|
+
endpoints: {
|
|
15
|
+
list: `/api/${slug}`,
|
|
16
|
+
create: `POST /api/${slug}`,
|
|
17
|
+
read: `GET /api/${slug}/:id`,
|
|
18
|
+
update: `PATCH /api/${slug}/:id`,
|
|
19
|
+
delete: `DELETE /api/${slug}/:id`,
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
name: "Kyro CMS API",
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
description: "Headless CMS REST API",
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
endpoints: {
|
|
31
|
+
collections: "/api/collections",
|
|
32
|
+
health: "/api/health",
|
|
33
|
+
upload: "/api/upload",
|
|
34
|
+
graphql: "/api/graphql",
|
|
35
|
+
},
|
|
36
|
+
collections: collectionList,
|
|
37
|
+
globals: [],
|
|
38
|
+
}),
|
|
39
|
+
{
|
|
40
|
+
status: 200,
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Access-Control-Allow-Origin": "*",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return new Response(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
}),
|
|
53
|
+
{
|
|
54
|
+
status: 500,
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { dataStore } from "@/lib/dataStore";
|
|
3
|
+
import nodemailer, { createTransport } from "nodemailer";
|
|
4
|
+
|
|
5
|
+
export const POST: APIRoute = async ({ params, request }) => {
|
|
6
|
+
const slug = params.slug as string;
|
|
7
|
+
|
|
8
|
+
let body;
|
|
9
|
+
try {
|
|
10
|
+
body = await request.json();
|
|
11
|
+
} catch (e) {
|
|
12
|
+
body = {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const globalConfig = await dataStore.findGlobal(slug);
|
|
17
|
+
|
|
18
|
+
const testEmail = body?.email || globalConfig?.testEmail;
|
|
19
|
+
|
|
20
|
+
if (!testEmail) {
|
|
21
|
+
return new Response(
|
|
22
|
+
JSON.stringify({ error: "Please enter a test email address" }),
|
|
23
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
28
|
+
if (!emailRegex.test(testEmail)) {
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({ error: "Invalid email address format" }),
|
|
31
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const emailSettings = globalConfig;
|
|
36
|
+
const provider = emailSettings?.provider;
|
|
37
|
+
|
|
38
|
+
if (!provider) {
|
|
39
|
+
return new Response(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
error:
|
|
42
|
+
"No email provider configured. Please configure your email settings first.",
|
|
43
|
+
}),
|
|
44
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let transporter;
|
|
49
|
+
|
|
50
|
+
const transporterOptions = {
|
|
51
|
+
timeout: 10000, // 10 second timeout
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (provider === "smtp" && emailSettings?.smtp) {
|
|
55
|
+
transporter = createTransport({
|
|
56
|
+
...transporterOptions,
|
|
57
|
+
host: emailSettings.smtp.host,
|
|
58
|
+
port: parseInt(emailSettings.smtp.port) || 587,
|
|
59
|
+
secure: emailSettings.smtp.secure || false,
|
|
60
|
+
auth: {
|
|
61
|
+
user: emailSettings.smtp.username,
|
|
62
|
+
pass: emailSettings.smtp.password,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} else if (provider === "ses" && emailSettings?.ses) {
|
|
66
|
+
transporter = createTransport({
|
|
67
|
+
...transporterOptions,
|
|
68
|
+
host: `email-smtp.${emailSettings.ses.region}.amazonaws.com`,
|
|
69
|
+
port: 587,
|
|
70
|
+
secure: false,
|
|
71
|
+
auth: {
|
|
72
|
+
user: emailSettings.ses.accessKeyId,
|
|
73
|
+
pass: emailSettings.ses.secretAccessKey,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} else if (provider === "resend" && emailSettings?.resend?.apiKey) {
|
|
77
|
+
transporter = createTransport({
|
|
78
|
+
...transporterOptions,
|
|
79
|
+
host: "smtp.resend.com",
|
|
80
|
+
port: 587,
|
|
81
|
+
secure: false,
|
|
82
|
+
auth: {
|
|
83
|
+
user: "resend",
|
|
84
|
+
pass: emailSettings.resend.apiKey,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
} else if (provider === "sendgrid" && emailSettings?.sendgrid?.apiKey) {
|
|
88
|
+
transporter = createTransport({
|
|
89
|
+
...transporterOptions,
|
|
90
|
+
host: "smtp.sendgrid.net",
|
|
91
|
+
port: 587,
|
|
92
|
+
secure: false,
|
|
93
|
+
auth: {
|
|
94
|
+
user: "apikey",
|
|
95
|
+
pass: emailSettings.sendgrid.apiKey,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
return new Response(
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
error: `Provider ${provider} not supported or not configured`,
|
|
102
|
+
}),
|
|
103
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fromEmail =
|
|
108
|
+
emailSettings?.fromEmail || emailSettings?.from || "noreply@example.com";
|
|
109
|
+
const fromName = emailSettings?.fromName || "Kyro CMS";
|
|
110
|
+
|
|
111
|
+
const info = await transporter.sendMail({
|
|
112
|
+
from: `"${fromName}" <${fromEmail}>`,
|
|
113
|
+
to: testEmail,
|
|
114
|
+
subject: "Test Email - Kyro CMS",
|
|
115
|
+
html: `
|
|
116
|
+
<!DOCTYPE html>
|
|
117
|
+
<html>
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="utf-8">
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
121
|
+
<title>Test Email</title>
|
|
122
|
+
</head>
|
|
123
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333;">
|
|
124
|
+
<div style="max-width:600px; margin:0 auto; padding:20px;">
|
|
125
|
+
<h1 style="color:#0b1222;">Test Email Successful!</h1>
|
|
126
|
+
<p>This is a test email from <strong>Kyro CMS</strong>.</p>
|
|
127
|
+
<p>If you received this email, your email settings are configured correctly.</p>
|
|
128
|
+
<hr style="border:none; border-top:1px solid #e2e8f0; margin:20px 0;">
|
|
129
|
+
<p style="font-size:12px; color:#666;">
|
|
130
|
+
Sent to: ${testEmail}<br>
|
|
131
|
+
Provider: ${provider}
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
`,
|
|
137
|
+
text: `Test Email Successful!\n\nThis is a test email from Kyro CMS.\nIf you received this email, your email settings are configured correctly.\n\nSent to: ${testEmail}\nProvider: ${provider}`,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(info);
|
|
141
|
+
return new Response(
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
success: true,
|
|
144
|
+
message: `Test email sent to ${testEmail}`,
|
|
145
|
+
messageId: info.messageId,
|
|
146
|
+
}),
|
|
147
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
} catch (error: any) {
|
|
151
|
+
console.log(error);
|
|
152
|
+
let errorMessage = "Failed to send test email";
|
|
153
|
+
|
|
154
|
+
if (error.code === "ECONNREFUSED") {
|
|
155
|
+
errorMessage = "Connection refused. Check your SMTP host and port.";
|
|
156
|
+
} else if (error.code === "ETIMEDOUT") {
|
|
157
|
+
errorMessage = "Connection timed out. Check your SMTP host and port.";
|
|
158
|
+
} else if (error.code === "ENOTFOUND") {
|
|
159
|
+
errorMessage = "Host not found. Check your SMTP hostname.";
|
|
160
|
+
} else if (error.code === "EAUTH") {
|
|
161
|
+
errorMessage = "Authentication failed. Check your username/password.";
|
|
162
|
+
} else if (error.message) {
|
|
163
|
+
errorMessage = error.message;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error("[test-email] Error:", errorMessage, error.stack);
|
|
167
|
+
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
168
|
+
status: 500,
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { dataStore } from "@/lib/dataStore";
|
|
3
|
+
import { globals } from "@/lib/config";
|
|
4
|
+
|
|
5
|
+
export const GET: APIRoute = async ({ params }) => {
|
|
6
|
+
const slug = params.slug as string;
|
|
7
|
+
|
|
8
|
+
if (!globals[slug]) {
|
|
9
|
+
return new Response(JSON.stringify({ error: "Global not found" }), {
|
|
10
|
+
status: 404,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const data = await dataStore.findGlobal(slug);
|
|
15
|
+
return new Response(JSON.stringify({ data }), {
|
|
16
|
+
status: 200,
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const PATCH: APIRoute = async ({ params, request }) => {
|
|
22
|
+
const slug = params.slug as string;
|
|
23
|
+
|
|
24
|
+
if (!globals[slug]) {
|
|
25
|
+
return new Response(JSON.stringify({ error: "Global not found" }), {
|
|
26
|
+
status: 404,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const body = await request.json();
|
|
32
|
+
const data = await dataStore.updateGlobal(slug, body);
|
|
33
|
+
return new Response(JSON.stringify({ data }), {
|
|
34
|
+
status: 200,
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return new Response(JSON.stringify({ error: "Failed to update global" }), {
|
|
39
|
+
status: 500,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|