@kyro-cms/admin 0.3.1 → 0.3.4
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/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import type { APIRoute } from "astro";
|
|
2
|
-
import { getAuthAdapter } from "../../../lib/db";
|
|
3
|
-
import { createAuditContext } from "@kyro-cms/core";
|
|
4
|
-
import {
|
|
5
|
-
checkRateLimit,
|
|
6
|
-
recordFailedLogin,
|
|
7
|
-
getAccountLockStatus,
|
|
8
|
-
resetRateLimit,
|
|
9
|
-
} from "../../../lib/rate-limit";
|
|
10
|
-
import jwt from "jsonwebtoken";
|
|
11
|
-
import { randomBytes } from "crypto";
|
|
12
|
-
|
|
13
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
14
|
-
const REFRESH_SECRET =
|
|
15
|
-
process.env.REFRESH_SECRET ||
|
|
16
|
-
process.env.JWT_SECRET ||
|
|
17
|
-
"change-me-in-production";
|
|
18
|
-
|
|
19
|
-
const ACCESS_TOKEN_EXPIRY = process.env.JWT_EXPIRES_IN || "24h";
|
|
20
|
-
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || "7d";
|
|
21
|
-
|
|
22
|
-
function getClientIp(request: Request): string {
|
|
23
|
-
const forwarded = request.headers.get("x-forwarded-for");
|
|
24
|
-
if (forwarded) return forwarded.split(",")[0].trim();
|
|
25
|
-
return request.headers.get("x-real-ip") || "unknown";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getExpirySeconds(expiry: string): number {
|
|
29
|
-
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
30
|
-
if (!match) return 86400;
|
|
31
|
-
const value = parseInt(match[1]);
|
|
32
|
-
const unit = match[2];
|
|
33
|
-
switch (unit) {
|
|
34
|
-
case "s":
|
|
35
|
-
return value;
|
|
36
|
-
case "m":
|
|
37
|
-
return value * 60;
|
|
38
|
-
case "h":
|
|
39
|
-
return value * 3600;
|
|
40
|
-
case "d":
|
|
41
|
-
return value * 86400;
|
|
42
|
-
default:
|
|
43
|
-
return 86400;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function generateTokens(user: { id: string; email: string; role: string }) {
|
|
48
|
-
const accessToken = jwt.sign(
|
|
49
|
-
{ sub: user.id, email: user.email, role: user.role, type: "access" },
|
|
50
|
-
JWT_SECRET,
|
|
51
|
-
{ expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const refreshToken = jwt.sign(
|
|
55
|
-
{
|
|
56
|
-
sub: user.id,
|
|
57
|
-
email: user.email,
|
|
58
|
-
role: user.role,
|
|
59
|
-
type: "refresh",
|
|
60
|
-
jti: randomBytes(16).toString("hex"),
|
|
61
|
-
},
|
|
62
|
-
REFRESH_SECRET,
|
|
63
|
-
{ expiresIn: REFRESH_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
return { accessToken, refreshToken };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function createAuthCookies(token: string, refreshToken: string) {
|
|
70
|
-
const accessMaxAge = getExpirySeconds(ACCESS_TOKEN_EXPIRY);
|
|
71
|
-
const refreshMaxAge = getExpirySeconds(REFRESH_TOKEN_EXPIRY);
|
|
72
|
-
|
|
73
|
-
return [
|
|
74
|
-
`auth_token=${token}; Path=/; Max-Age=${accessMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
75
|
-
`refresh_token=${refreshToken}; Path=/; Max-Age=${refreshMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
76
|
-
].join(", ");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
80
|
-
const clientIp = getClientIp(request);
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const body = (await request.json()) as {
|
|
84
|
-
email?: string;
|
|
85
|
-
password?: string;
|
|
86
|
-
};
|
|
87
|
-
const { email, password } = body;
|
|
88
|
-
|
|
89
|
-
if (!email || !password) {
|
|
90
|
-
return new Response(
|
|
91
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
92
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check rate limit by IP
|
|
97
|
-
const ipRateLimit = await checkRateLimit(clientIp, "login_ip");
|
|
98
|
-
if (!ipRateLimit.allowed) {
|
|
99
|
-
return new Response(
|
|
100
|
-
JSON.stringify({
|
|
101
|
-
error: "Too many login attempts. Please try again later.",
|
|
102
|
-
retryAfter: Math.ceil(
|
|
103
|
-
((ipRateLimit.lockedUntil?.getTime() ?? 0) - Date.now()) / 1000,
|
|
104
|
-
),
|
|
105
|
-
}),
|
|
106
|
-
{
|
|
107
|
-
status: 429,
|
|
108
|
-
headers: { "Content-Type": "application/json", "Retry-After": "900" },
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check rate limit by email
|
|
114
|
-
const emailRateLimit = await checkRateLimit(email, "login_email");
|
|
115
|
-
if (!emailRateLimit.allowed) {
|
|
116
|
-
return new Response(
|
|
117
|
-
JSON.stringify({
|
|
118
|
-
error: "Too many login attempts. Please try again later.",
|
|
119
|
-
retryAfter: Math.ceil(
|
|
120
|
-
((emailRateLimit.lockedUntil?.getTime() ?? 0) - Date.now()) / 1000,
|
|
121
|
-
),
|
|
122
|
-
}),
|
|
123
|
-
{
|
|
124
|
-
status: 429,
|
|
125
|
-
headers: { "Content-Type": "application/json", "Retry-After": "900" },
|
|
126
|
-
},
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Check if account is locked
|
|
131
|
-
const lockStatus = await getAccountLockStatus(email);
|
|
132
|
-
if (lockStatus.isLocked) {
|
|
133
|
-
return new Response(
|
|
134
|
-
JSON.stringify({
|
|
135
|
-
error:
|
|
136
|
-
"Account is temporarily locked due to too many failed attempts.",
|
|
137
|
-
retryAfter: Math.ceil(
|
|
138
|
-
((lockStatus.lockedUntil?.getTime() ?? 0) - Date.now()) / 1000,
|
|
139
|
-
),
|
|
140
|
-
}),
|
|
141
|
-
{ status: 423, headers: { "Content-Type": "application/json" } },
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const adapter = await getAuthAdapter();
|
|
146
|
-
const user = await adapter.verifyPassword(email, password);
|
|
147
|
-
const context = createAuditContext(request);
|
|
148
|
-
|
|
149
|
-
if (!user) {
|
|
150
|
-
// Record failed attempt
|
|
151
|
-
await recordFailedLogin(email);
|
|
152
|
-
await adapter.createAuditLog({
|
|
153
|
-
action: "login_failed",
|
|
154
|
-
userEmail: email,
|
|
155
|
-
resource: "auth",
|
|
156
|
-
success: false,
|
|
157
|
-
error: "Invalid credentials",
|
|
158
|
-
metadata: {
|
|
159
|
-
reason: "invalid_credentials",
|
|
160
|
-
attemptTime: new Date().toISOString(),
|
|
161
|
-
failedAttempts: lockStatus.failedAttempts + 1,
|
|
162
|
-
},
|
|
163
|
-
...context,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
167
|
-
status: 401,
|
|
168
|
-
headers: { "Content-Type": "application/json" },
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Reset rate limits on successful login
|
|
173
|
-
await resetRateLimit(clientIp, "login_ip");
|
|
174
|
-
await resetRateLimit(email, "login_email");
|
|
175
|
-
|
|
176
|
-
await adapter.createAuditLog({
|
|
177
|
-
action: "login",
|
|
178
|
-
userId: user.id,
|
|
179
|
-
userEmail: user.email,
|
|
180
|
-
role: user.role,
|
|
181
|
-
resource: "auth",
|
|
182
|
-
success: true,
|
|
183
|
-
metadata: { method: "password" },
|
|
184
|
-
...context,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const { accessToken, refreshToken } = generateTokens(user);
|
|
188
|
-
const expiresIn = getExpirySeconds(ACCESS_TOKEN_EXPIRY);
|
|
189
|
-
|
|
190
|
-
return new Response(
|
|
191
|
-
JSON.stringify({
|
|
192
|
-
success: true,
|
|
193
|
-
user: { id: user.id, email: user.email, role: user.role },
|
|
194
|
-
expiresIn,
|
|
195
|
-
}),
|
|
196
|
-
{
|
|
197
|
-
status: 200,
|
|
198
|
-
headers: {
|
|
199
|
-
"Content-Type": "application/json",
|
|
200
|
-
"Set-Cookie": createAuthCookies(accessToken, refreshToken),
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error("Login error:", error);
|
|
206
|
-
return new Response(JSON.stringify({ error: "Login failed" }), {
|
|
207
|
-
status: 500,
|
|
208
|
-
headers: { "Content-Type": "application/json" },
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { APIRoute } from "astro";
|
|
2
|
-
import { getAuthAdapter } from "../../../lib/db";
|
|
3
|
-
import { createAuditContext } from "@kyro-cms/core";
|
|
4
|
-
|
|
5
|
-
const CLEAR_COOKIES = [
|
|
6
|
-
"auth_token=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
|
|
7
|
-
"refresh_token=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
|
|
8
|
-
].join(", ");
|
|
9
|
-
|
|
10
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
11
|
-
try {
|
|
12
|
-
const authHeader = request.headers.get("authorization");
|
|
13
|
-
let userId: string | null = null;
|
|
14
|
-
|
|
15
|
-
if (authHeader?.startsWith("Bearer ")) {
|
|
16
|
-
const { getAuthAdapter } = await import("../../../lib/db");
|
|
17
|
-
const adapter = await getAuthAdapter();
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const { default: jwt } = await import("jsonwebtoken");
|
|
21
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
22
|
-
|
|
23
|
-
const token = authHeader.slice(7);
|
|
24
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
|
|
25
|
-
userId = decoded.sub;
|
|
26
|
-
|
|
27
|
-
if (userId) {
|
|
28
|
-
const user = await adapter.findUserById(userId);
|
|
29
|
-
const context = createAuditContext(request);
|
|
30
|
-
|
|
31
|
-
if (user) {
|
|
32
|
-
await adapter.createAuditLog({
|
|
33
|
-
action: "logout",
|
|
34
|
-
userId: user.id,
|
|
35
|
-
userEmail: user.email,
|
|
36
|
-
role: user.role,
|
|
37
|
-
resource: "auth",
|
|
38
|
-
success: true,
|
|
39
|
-
metadata: { method: "jwt" },
|
|
40
|
-
...context,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// Invalid or expired token - just clear cookies
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
50
|
-
status: 200,
|
|
51
|
-
headers: {
|
|
52
|
-
"Content-Type": "application/json",
|
|
53
|
-
"Set-Cookie": CLEAR_COOKIES,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error("Logout error:", error);
|
|
58
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
59
|
-
status: 200,
|
|
60
|
-
headers: {
|
|
61
|
-
"Content-Type": "application/json",
|
|
62
|
-
"Set-Cookie": CLEAR_COOKIES,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
};
|
package/src/pages/api/auth/me.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { APIRoute } from "astro";
|
|
2
|
-
import jwt from "jsonwebtoken";
|
|
3
|
-
|
|
4
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
5
|
-
|
|
6
|
-
export const GET: APIRoute = async ({ request }) => {
|
|
7
|
-
const authHeader = request.headers.get("authorization");
|
|
8
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
9
|
-
|
|
10
|
-
if (!token) {
|
|
11
|
-
return new Response(JSON.stringify({ error: "Not authenticated" }), {
|
|
12
|
-
status: 401,
|
|
13
|
-
headers: { "Content-Type": "application/json" },
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
19
|
-
return new Response(
|
|
20
|
-
JSON.stringify({
|
|
21
|
-
user: {
|
|
22
|
-
id: payload.sub,
|
|
23
|
-
email: (payload as any).email,
|
|
24
|
-
role: (payload as any).role,
|
|
25
|
-
tenantId: (payload as any).tenantId,
|
|
26
|
-
},
|
|
27
|
-
}),
|
|
28
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
29
|
-
);
|
|
30
|
-
} catch {
|
|
31
|
-
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
|
32
|
-
status: 401,
|
|
33
|
-
headers: { "Content-Type": "application/json" },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
};
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { APIRoute } from "astro";
|
|
2
|
-
import jwt from "jsonwebtoken";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
|
-
|
|
5
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
6
|
-
const REFRESH_SECRET =
|
|
7
|
-
process.env.REFRESH_SECRET ||
|
|
8
|
-
process.env.JWT_SECRET ||
|
|
9
|
-
"change-me-in-production";
|
|
10
|
-
|
|
11
|
-
const ACCESS_TOKEN_EXPIRY = process.env.JWT_EXPIRES_IN || "24h";
|
|
12
|
-
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || "7d";
|
|
13
|
-
|
|
14
|
-
function getExpirySeconds(expiry: string): number {
|
|
15
|
-
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
16
|
-
if (!match) return 86400;
|
|
17
|
-
const value = parseInt(match[1]);
|
|
18
|
-
const unit = match[2];
|
|
19
|
-
switch (unit) {
|
|
20
|
-
case "s":
|
|
21
|
-
return value;
|
|
22
|
-
case "m":
|
|
23
|
-
return value * 60;
|
|
24
|
-
case "h":
|
|
25
|
-
return value * 3600;
|
|
26
|
-
case "d":
|
|
27
|
-
return value * 86400;
|
|
28
|
-
default:
|
|
29
|
-
return 86400;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function generateTokens(user: { id: string; email: string; role: string }) {
|
|
34
|
-
const accessToken = jwt.sign(
|
|
35
|
-
{ sub: user.id, email: user.email, role: user.role, type: "access" },
|
|
36
|
-
JWT_SECRET,
|
|
37
|
-
{ expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const refreshToken = jwt.sign(
|
|
41
|
-
{
|
|
42
|
-
sub: user.id,
|
|
43
|
-
email: user.email,
|
|
44
|
-
role: user.role,
|
|
45
|
-
type: "refresh",
|
|
46
|
-
jti: randomBytes(16).toString("hex"),
|
|
47
|
-
},
|
|
48
|
-
REFRESH_SECRET,
|
|
49
|
-
{ expiresIn: REFRESH_TOKEN_EXPIRY as jwt.SignOptions["expiresIn"] },
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return { accessToken, refreshToken };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createAuthCookies(token: string, refreshToken: string) {
|
|
56
|
-
const accessMaxAge = getExpirySeconds(ACCESS_TOKEN_EXPIRY);
|
|
57
|
-
const refreshMaxAge = getExpirySeconds(REFRESH_TOKEN_EXPIRY);
|
|
58
|
-
|
|
59
|
-
return [
|
|
60
|
-
`auth_token=${token}; Path=/; Max-Age=${accessMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
61
|
-
`refresh_token=${refreshToken}; Path=/; Max-Age=${refreshMaxAge}; HttpOnly; Secure; SameSite=Strict`,
|
|
62
|
-
].join(", ");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
66
|
-
try {
|
|
67
|
-
const refreshToken = request.headers.get("x-refresh-token");
|
|
68
|
-
|
|
69
|
-
if (!refreshToken) {
|
|
70
|
-
return new Response(JSON.stringify({ error: "Refresh token required" }), {
|
|
71
|
-
status: 401,
|
|
72
|
-
headers: { "Content-Type": "application/json" },
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const decoded = jwt.verify(refreshToken, REFRESH_SECRET) as {
|
|
77
|
-
sub: string;
|
|
78
|
-
email: string;
|
|
79
|
-
role: string;
|
|
80
|
-
type: string;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (decoded.type !== "refresh") {
|
|
84
|
-
return new Response(JSON.stringify({ error: "Invalid token type" }), {
|
|
85
|
-
status: 401,
|
|
86
|
-
headers: { "Content-Type": "application/json" },
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const user = {
|
|
91
|
-
id: decoded.sub,
|
|
92
|
-
email: decoded.email,
|
|
93
|
-
role: decoded.role,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
|
|
97
|
-
generateTokens(user);
|
|
98
|
-
|
|
99
|
-
return new Response(
|
|
100
|
-
JSON.stringify({
|
|
101
|
-
success: true,
|
|
102
|
-
expiresIn: getExpirySeconds(ACCESS_TOKEN_EXPIRY),
|
|
103
|
-
}),
|
|
104
|
-
{
|
|
105
|
-
status: 200,
|
|
106
|
-
headers: {
|
|
107
|
-
"Content-Type": "application/json",
|
|
108
|
-
"Set-Cookie": createAuthCookies(newAccessToken, newRefreshToken),
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.error("Refresh error:", error);
|
|
114
|
-
return new Response(
|
|
115
|
-
JSON.stringify({ error: "Invalid or expired refresh token" }),
|
|
116
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type { APIRoute } from "astro";
|
|
2
|
-
import { getAuthAdapter } from "../../../lib/db";
|
|
3
|
-
import { createAuditContext } from "@kyro-cms/core";
|
|
4
|
-
import jwt from "jsonwebtoken";
|
|
5
|
-
import { randomBytes } from "crypto";
|
|
6
|
-
|
|
7
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
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";
|
|
15
|
-
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
16
|
-
|
|
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(", ");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
69
|
-
try {
|
|
70
|
-
const body = (await request.json()) as {
|
|
71
|
-
email?: string;
|
|
72
|
-
password?: string;
|
|
73
|
-
confirmPassword?: string;
|
|
74
|
-
};
|
|
75
|
-
const { email, password, confirmPassword } = body;
|
|
76
|
-
|
|
77
|
-
if (!email || !password) {
|
|
78
|
-
return new Response(
|
|
79
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
80
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (password !== confirmPassword) {
|
|
85
|
-
return new Response(JSON.stringify({ error: "Passwords do not match" }), {
|
|
86
|
-
status: 400,
|
|
87
|
-
headers: { "Content-Type": "application/json" },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Password strength validation
|
|
92
|
-
const passwordErrors: string[] = [];
|
|
93
|
-
|
|
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) {
|
|
111
|
-
return new Response(
|
|
112
|
-
JSON.stringify({
|
|
113
|
-
error:
|
|
114
|
-
"Password is too weak. Must contain: " + passwordErrors.join(", "),
|
|
115
|
-
}),
|
|
116
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const adapter = await getAuthAdapter();
|
|
121
|
-
|
|
122
|
-
const existingUser = await adapter.findUserByEmail(email);
|
|
123
|
-
if (existingUser) {
|
|
124
|
-
return new Response(
|
|
125
|
-
JSON.stringify({ error: "Email already registered" }),
|
|
126
|
-
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const isFirstUser = !(await adapter.hasAnyUsers?.());
|
|
131
|
-
|
|
132
|
-
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
133
|
-
return new Response(
|
|
134
|
-
JSON.stringify({ error: "Registration is disabled" }),
|
|
135
|
-
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const user = await adapter.createUser({
|
|
140
|
-
email,
|
|
141
|
-
password,
|
|
142
|
-
role: isFirstUser ? "super_admin" : "customer",
|
|
143
|
-
});
|
|
144
|
-
|
|
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,
|
|
156
|
-
},
|
|
157
|
-
...context,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const { accessToken, refreshToken } = generateTokens(user);
|
|
161
|
-
|
|
162
|
-
return new Response(
|
|
163
|
-
JSON.stringify({
|
|
164
|
-
success: true,
|
|
165
|
-
isFirstUser,
|
|
166
|
-
user: {
|
|
167
|
-
id: user.id,
|
|
168
|
-
email: user.email,
|
|
169
|
-
role: user.role,
|
|
170
|
-
},
|
|
171
|
-
expiresIn: getExpirySeconds(ACCESS_TOKEN_EXPIRY),
|
|
172
|
-
}),
|
|
173
|
-
{
|
|
174
|
-
status: 201,
|
|
175
|
-
headers: {
|
|
176
|
-
"Content-Type": "application/json",
|
|
177
|
-
"Set-Cookie": createAuthCookies(accessToken, refreshToken),
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error("Registration error:", error);
|
|
183
|
-
return new Response(JSON.stringify({ error: "Registration failed" }), {
|
|
184
|
-
status: 500,
|
|
185
|
-
headers: { "Content-Type": "application/json" },
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
};
|