@kyro-cms/admin 0.3.2 → 0.3.5
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,443 +0,0 @@
|
|
|
1
|
-
import { buildSchema, graphql } from "graphql";
|
|
2
|
-
import { dataStore } from "../dataStore";
|
|
3
|
-
import { collections, globals } from "../config";
|
|
4
|
-
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
|
|
5
|
-
import jwt from "jsonwebtoken";
|
|
6
|
-
|
|
7
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
8
|
-
|
|
9
|
-
function generateSchemaTypes() {
|
|
10
|
-
const queryLines: string[] = [
|
|
11
|
-
`_schema: SchemaInfo!`,
|
|
12
|
-
`ping: String!`,
|
|
13
|
-
`collections: [CollectionInfo!]!`,
|
|
14
|
-
`globals: [GlobalInfo!]!`,
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
for (const [slug, collection] of Object.entries(collections)) {
|
|
18
|
-
if (slug === "roles") continue;
|
|
19
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
20
|
-
queryLines.push(`${safeSlug}(page: Int, limit: Int): CollectionResult!`);
|
|
21
|
-
queryLines.push(`${safeSlug}ById(id: ID!): JSON`);
|
|
22
|
-
queryLines.push(`${safeSlug}BySlug(slug: String!): JSON`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
for (const slug of Object.keys(globals)) {
|
|
26
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
27
|
-
queryLines.push(`${safeSlug}: GlobalResult!`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const mutationLines: string[] = [];
|
|
31
|
-
mutationLines.push(`login(email: String!, password: String!): AuthPayload!`);
|
|
32
|
-
mutationLines.push(
|
|
33
|
-
`register(email: String!, password: String!): AuthPayload!`,
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
for (const slug of Object.keys(collections)) {
|
|
37
|
-
if (slug === "roles" || slug === "users") continue;
|
|
38
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
39
|
-
const name = capitalize(safeSlug);
|
|
40
|
-
mutationLines.push(`create${name}(input: JSON!): JSON!`);
|
|
41
|
-
mutationLines.push(`update${name}(id: ID!, input: JSON!): JSON!`);
|
|
42
|
-
mutationLines.push(`delete${name}(id: ID!): Boolean!`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const slug of Object.keys(globals)) {
|
|
46
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
47
|
-
const name = capitalize(safeSlug);
|
|
48
|
-
mutationLines.push(`update${name}(input: JSON!): GlobalResult!`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return `
|
|
52
|
-
scalar JSON
|
|
53
|
-
|
|
54
|
-
type Query {
|
|
55
|
-
${queryLines.join("\n ")}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type Mutation {
|
|
59
|
-
${mutationLines.join("\n ")}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type Meta {
|
|
63
|
-
page: Int!
|
|
64
|
-
limit: Int!
|
|
65
|
-
totalDocs: Int!
|
|
66
|
-
totalPages: Int!
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
type CollectionResult {
|
|
70
|
-
docs: [JSON!]!
|
|
71
|
-
totalDocs: Int!
|
|
72
|
-
totalPages: Int!
|
|
73
|
-
page: Int!
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type GlobalResult {
|
|
77
|
-
data: JSON
|
|
78
|
-
success: Boolean!
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
type AuthPayload {
|
|
82
|
-
token: String
|
|
83
|
-
user: JSON
|
|
84
|
-
error: String
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type SchemaInfo {
|
|
88
|
-
types: [TypeInfo!]!
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type TypeInfo {
|
|
92
|
-
name: String!
|
|
93
|
-
fields: [FieldInfo!]!
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
type FieldInfo {
|
|
97
|
-
name: String!
|
|
98
|
-
type: String!
|
|
99
|
-
required: Boolean!
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type CollectionInfo {
|
|
103
|
-
slug: String!
|
|
104
|
-
name: String
|
|
105
|
-
fields: [String!]!
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type GlobalInfo {
|
|
109
|
-
slug: String!
|
|
110
|
-
name: String
|
|
111
|
-
}
|
|
112
|
-
`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const schemaString = generateSchemaTypes();
|
|
116
|
-
const typeDefs = buildSchema(schemaString);
|
|
117
|
-
|
|
118
|
-
function capitalize(str: string): string {
|
|
119
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface Context {
|
|
123
|
-
user?: {
|
|
124
|
-
id: string;
|
|
125
|
-
email: string;
|
|
126
|
-
role: string;
|
|
127
|
-
};
|
|
128
|
-
apiKeyId?: string;
|
|
129
|
-
permissions?: string[];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const rootValue = {
|
|
133
|
-
_schema: () => {
|
|
134
|
-
const types = Object.entries(collections).map(([slug, collection]) => ({
|
|
135
|
-
name: capitalize(slug),
|
|
136
|
-
fields: collection.fields.map((f: any) => ({
|
|
137
|
-
name: f.name,
|
|
138
|
-
type: getGraphQLType(f.type),
|
|
139
|
-
required: f.required || false,
|
|
140
|
-
})),
|
|
141
|
-
}));
|
|
142
|
-
return { types };
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
ping: () => "pong",
|
|
146
|
-
|
|
147
|
-
collections: () => {
|
|
148
|
-
return Object.entries(collections).map(([slug, collection]) => ({
|
|
149
|
-
slug,
|
|
150
|
-
name: (collection as CollectionConfig).label || slug,
|
|
151
|
-
fields: (collection as CollectionConfig).fields.map((f: any) => f.name),
|
|
152
|
-
}));
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
globals: () => {
|
|
156
|
-
return Object.entries(globals).map(([slug, global]) => ({
|
|
157
|
-
slug,
|
|
158
|
-
name: (global as GlobalConfig).label || slug,
|
|
159
|
-
}));
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
...generateQueryResolvers(),
|
|
163
|
-
|
|
164
|
-
...generateMutationResolvers(),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
function getGraphQLType(fieldType: string): string {
|
|
168
|
-
const typeMap: Record<string, string> = {
|
|
169
|
-
text: "String",
|
|
170
|
-
richtext: "String",
|
|
171
|
-
email: "String",
|
|
172
|
-
password: "String",
|
|
173
|
-
url: "String",
|
|
174
|
-
number: "Float",
|
|
175
|
-
integer: "Int",
|
|
176
|
-
boolean: "Boolean",
|
|
177
|
-
select: "String",
|
|
178
|
-
multiselect: "[String!]!",
|
|
179
|
-
date: "String",
|
|
180
|
-
datetime: "String",
|
|
181
|
-
media: "String",
|
|
182
|
-
reference: "String",
|
|
183
|
-
array: "[String!]!",
|
|
184
|
-
json: "JSON",
|
|
185
|
-
code: "String",
|
|
186
|
-
markdown: "String",
|
|
187
|
-
slug: "String",
|
|
188
|
-
};
|
|
189
|
-
return typeMap[fieldType] || "String";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function generateQueryResolvers() {
|
|
193
|
-
const resolvers: Record<string, any> = {};
|
|
194
|
-
|
|
195
|
-
for (const slug of Object.keys(collections)) {
|
|
196
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
197
|
-
resolvers[safeSlug] = async ({
|
|
198
|
-
page,
|
|
199
|
-
limit,
|
|
200
|
-
}: {
|
|
201
|
-
page?: number;
|
|
202
|
-
limit?: number;
|
|
203
|
-
}) => {
|
|
204
|
-
return await dataStore.find(slug, {
|
|
205
|
-
page: page || 1,
|
|
206
|
-
limit: limit || 25,
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
resolvers[`${safeSlug}ById`] = async ({ id }: { id: string }) => {
|
|
211
|
-
return await dataStore.findById(slug, id);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
resolvers[`${safeSlug}BySlug`] = async ({
|
|
215
|
-
slug: itemSlug,
|
|
216
|
-
}: {
|
|
217
|
-
slug: string;
|
|
218
|
-
}) => {
|
|
219
|
-
const docs = await dataStore.find(slug, { limit: 100 });
|
|
220
|
-
return docs.docs.find((d: any) => d.slug === itemSlug) || null;
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (const slug of Object.keys(globals)) {
|
|
225
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
226
|
-
resolvers[safeSlug] = async () => {
|
|
227
|
-
return { data: await dataStore.findGlobal(slug), success: true };
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return resolvers;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function generateMutationResolvers() {
|
|
235
|
-
return {
|
|
236
|
-
login: async ({ email, password }: { email: string; password: string }) => {
|
|
237
|
-
try {
|
|
238
|
-
const { SQLiteAuthAdapter } = await import("../auth/sqlite-adapter");
|
|
239
|
-
const adapter = new SQLiteAuthAdapter();
|
|
240
|
-
|
|
241
|
-
const user = await adapter.findUserByEmail(email);
|
|
242
|
-
if (!user) {
|
|
243
|
-
return { error: "Invalid credentials" };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const valid = await adapter.verifyPassword(
|
|
247
|
-
password,
|
|
248
|
-
user.passwordHash || "",
|
|
249
|
-
);
|
|
250
|
-
if (!valid) {
|
|
251
|
-
return { error: "Invalid credentials" };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const { passwordHash, ...safeUser } = user;
|
|
255
|
-
const token = jwt.sign(
|
|
256
|
-
{
|
|
257
|
-
sub: safeUser.id,
|
|
258
|
-
email: safeUser.email,
|
|
259
|
-
role: safeUser.role,
|
|
260
|
-
tenantId: safeUser.tenantId,
|
|
261
|
-
},
|
|
262
|
-
JWT_SECRET,
|
|
263
|
-
{ expiresIn: "7d" },
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
return { token, user: safeUser };
|
|
267
|
-
} catch {
|
|
268
|
-
return { error: "Authentication failed" };
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
register: async ({
|
|
273
|
-
email,
|
|
274
|
-
password,
|
|
275
|
-
}: {
|
|
276
|
-
email: string;
|
|
277
|
-
password: string;
|
|
278
|
-
}) => {
|
|
279
|
-
try {
|
|
280
|
-
const { SQLiteAuthAdapter } = await import("../auth/sqlite-adapter");
|
|
281
|
-
const adapter = new SQLiteAuthAdapter();
|
|
282
|
-
|
|
283
|
-
const existing = await adapter.findUserByEmail(email);
|
|
284
|
-
if (existing) {
|
|
285
|
-
return { error: "Email already exists" };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const passwordHash = await adapter.hashPassword(password);
|
|
289
|
-
const user = await adapter.createUser({
|
|
290
|
-
email,
|
|
291
|
-
passwordHash,
|
|
292
|
-
role: "customer",
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const { passwordHash: _, ...safeUser } = user;
|
|
296
|
-
const token = jwt.sign(
|
|
297
|
-
{
|
|
298
|
-
sub: safeUser.id,
|
|
299
|
-
email: safeUser.email,
|
|
300
|
-
role: safeUser.role,
|
|
301
|
-
tenantId: safeUser.tenantId,
|
|
302
|
-
},
|
|
303
|
-
JWT_SECRET,
|
|
304
|
-
{ expiresIn: "7d" },
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
return { token, user: safeUser };
|
|
308
|
-
} catch {
|
|
309
|
-
return { error: "Registration failed" };
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function generateCollectionMutations() {
|
|
316
|
-
const resolvers: Record<string, any> = {};
|
|
317
|
-
|
|
318
|
-
for (const slug of Object.keys(collections)) {
|
|
319
|
-
if (slug === "roles" || slug === "users") continue;
|
|
320
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
321
|
-
const name = capitalize(safeSlug);
|
|
322
|
-
|
|
323
|
-
resolvers[`create${name}`] = async ({ input }: { input: any }) => {
|
|
324
|
-
return await dataStore.create(slug, input);
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
resolvers[`update${name}`] = async ({
|
|
328
|
-
id,
|
|
329
|
-
input,
|
|
330
|
-
}: {
|
|
331
|
-
id: string;
|
|
332
|
-
input: any;
|
|
333
|
-
}) => {
|
|
334
|
-
return await dataStore.update(slug, id, input);
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
resolvers[`delete${name}`] = async ({ id }: { id: string }) => {
|
|
338
|
-
return await dataStore.delete(slug, id);
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (const slug of Object.keys(globals)) {
|
|
343
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
344
|
-
const name = capitalize(safeSlug);
|
|
345
|
-
|
|
346
|
-
resolvers[`update${name}`] = async ({ input }: { input: any }) => {
|
|
347
|
-
await dataStore.updateGlobal(slug, input);
|
|
348
|
-
return { data: await dataStore.findGlobal(slug), success: true };
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return resolvers;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export async function executeGraphQL(
|
|
356
|
-
query: string,
|
|
357
|
-
variables?: Record<string, any>,
|
|
358
|
-
authToken?: string,
|
|
359
|
-
apiKey?: string,
|
|
360
|
-
) {
|
|
361
|
-
let context: Context = {};
|
|
362
|
-
|
|
363
|
-
if (apiKey) {
|
|
364
|
-
try {
|
|
365
|
-
const result = await validateApiKeyFromDataStore(apiKey);
|
|
366
|
-
if (result.valid && result.user) {
|
|
367
|
-
context.user = {
|
|
368
|
-
id: result.userId as string,
|
|
369
|
-
email: result.user.email,
|
|
370
|
-
role: result.user.role,
|
|
371
|
-
};
|
|
372
|
-
context.apiKeyId = result.apiKeyId;
|
|
373
|
-
context.permissions = result.permissions;
|
|
374
|
-
}
|
|
375
|
-
} catch {
|
|
376
|
-
// Invalid API key, continue without auth
|
|
377
|
-
}
|
|
378
|
-
} else if (authToken) {
|
|
379
|
-
try {
|
|
380
|
-
const payload = jwt.verify(authToken, JWT_SECRET) as jwt.JwtPayload;
|
|
381
|
-
context.user = {
|
|
382
|
-
id: payload.sub as string,
|
|
383
|
-
email: (payload as any).email,
|
|
384
|
-
role: (payload as any).role,
|
|
385
|
-
};
|
|
386
|
-
} catch {
|
|
387
|
-
// Invalid token, continue without auth
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const result = await graphql({
|
|
392
|
-
schema: typeDefs,
|
|
393
|
-
source: query,
|
|
394
|
-
rootValue,
|
|
395
|
-
contextValue: context,
|
|
396
|
-
variableValues: variables,
|
|
397
|
-
} as any);
|
|
398
|
-
|
|
399
|
-
return result;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async function validateApiKeyFromDataStore(apiKey: string) {
|
|
403
|
-
if (!apiKey.startsWith("kyro_")) {
|
|
404
|
-
return { valid: false };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const keyPrefix = apiKey.substring(0, 8);
|
|
408
|
-
const result = await dataStore.find("_api_keys", { limit: 100 });
|
|
409
|
-
const keys = (result.docs || []).filter(
|
|
410
|
-
(k: any) => k.keyPrefix === keyPrefix,
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
for (const record of keys) {
|
|
414
|
-
if (record.key === apiKey) {
|
|
415
|
-
if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
|
|
416
|
-
return { valid: false, error: "API key expired" };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
await dataStore.update("_api_keys", record.id, {
|
|
421
|
-
lastUsedAt: new Date().toISOString(),
|
|
422
|
-
});
|
|
423
|
-
} catch {}
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
valid: true,
|
|
427
|
-
userId: record.userId,
|
|
428
|
-
user: {
|
|
429
|
-
id: record.userId,
|
|
430
|
-
email: record.email,
|
|
431
|
-
role: record.role || "author",
|
|
432
|
-
tenantId: record.tenantId,
|
|
433
|
-
},
|
|
434
|
-
permissions: record.permissions || [],
|
|
435
|
-
apiKeyId: record.id,
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return { valid: false };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export { schemaString };
|
package/src/lib/rate-limit.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import { randomBytes } from "crypto";
|
|
3
|
-
|
|
4
|
-
const DB_PATH =
|
|
5
|
-
process.env.KYRO_AUTH_DB_PATH || process.env.KYRO_DB_PATH || "./data/auth.db";
|
|
6
|
-
|
|
7
|
-
interface RateLimitConfig {
|
|
8
|
-
maxAttempts: number;
|
|
9
|
-
windowMs: number;
|
|
10
|
-
lockoutMs: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface RateLimitResult {
|
|
14
|
-
allowed: boolean;
|
|
15
|
-
remaining: number;
|
|
16
|
-
resetAt: Date | null;
|
|
17
|
-
lockedUntil: Date | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const DEFAULT_CONFIG: RateLimitConfig = {
|
|
21
|
-
maxAttempts: 5,
|
|
22
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
23
|
-
lockoutMs: 15 * 60 * 1000, // 15 minutes lockout
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function getDb() {
|
|
27
|
-
return new Database(DB_PATH);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function checkRateLimit(
|
|
31
|
-
identifier: string,
|
|
32
|
-
action: string = "login",
|
|
33
|
-
config: RateLimitConfig = DEFAULT_CONFIG,
|
|
34
|
-
): Promise<RateLimitResult> {
|
|
35
|
-
const db = getDb();
|
|
36
|
-
const now = new Date();
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Clean up expired records first
|
|
40
|
-
db.prepare(
|
|
41
|
-
`
|
|
42
|
-
DELETE FROM rate_limits
|
|
43
|
-
WHERE expires_at IS NOT NULL AND expires_at < ?
|
|
44
|
-
`,
|
|
45
|
-
).run(now.toISOString());
|
|
46
|
-
|
|
47
|
-
// Get existing record
|
|
48
|
-
const existing = db
|
|
49
|
-
.prepare(
|
|
50
|
-
`
|
|
51
|
-
SELECT * FROM rate_limits
|
|
52
|
-
WHERE identifier = ? AND action = ?
|
|
53
|
-
`,
|
|
54
|
-
)
|
|
55
|
-
.get(identifier, action) as any;
|
|
56
|
-
|
|
57
|
-
if (!existing) {
|
|
58
|
-
// First attempt - create record
|
|
59
|
-
const id = randomBytes(16).toString("hex");
|
|
60
|
-
db.prepare(
|
|
61
|
-
`
|
|
62
|
-
INSERT INTO rate_limits (id, identifier, action, attempts, first_attempt, last_attempt, created_at)
|
|
63
|
-
VALUES (?, ?, ?, 1, ?, ?, ?)
|
|
64
|
-
`,
|
|
65
|
-
).run(
|
|
66
|
-
id,
|
|
67
|
-
identifier,
|
|
68
|
-
action,
|
|
69
|
-
now.toISOString(),
|
|
70
|
-
now.toISOString(),
|
|
71
|
-
now.toISOString(),
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
allowed: true,
|
|
76
|
-
remaining: config.maxAttempts - 1,
|
|
77
|
-
resetAt: new Date(now.getTime() + config.windowMs),
|
|
78
|
-
lockedUntil: null,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check if currently locked
|
|
83
|
-
if (existing.expires_at && new Date(existing.expires_at) > now) {
|
|
84
|
-
return {
|
|
85
|
-
allowed: false,
|
|
86
|
-
remaining: 0,
|
|
87
|
-
resetAt: null,
|
|
88
|
-
lockedUntil: new Date(existing.expires_at),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Check if within window
|
|
93
|
-
const firstAttempt = existing.first_attempt
|
|
94
|
-
? new Date(existing.first_attempt)
|
|
95
|
-
: now;
|
|
96
|
-
const windowEnd = new Date(firstAttempt.getTime() + config.windowMs);
|
|
97
|
-
|
|
98
|
-
if (now > windowEnd) {
|
|
99
|
-
// Window expired - reset attempts
|
|
100
|
-
db.prepare(
|
|
101
|
-
`
|
|
102
|
-
UPDATE rate_limits
|
|
103
|
-
SET attempts = 1, first_attempt = ?, last_attempt = ?, expires_at = NULL
|
|
104
|
-
WHERE identifier = ? AND action = ?
|
|
105
|
-
`,
|
|
106
|
-
).run(now.toISOString(), now.toISOString(), identifier, action);
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
allowed: true,
|
|
110
|
-
remaining: config.maxAttempts - 1,
|
|
111
|
-
resetAt: new Date(now.getTime() + config.windowMs),
|
|
112
|
-
lockedUntil: null,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Within window - check attempts
|
|
117
|
-
const attempts = existing.attempts || 0;
|
|
118
|
-
const remaining = config.maxAttempts - attempts;
|
|
119
|
-
|
|
120
|
-
if (attempts >= config.maxAttempts) {
|
|
121
|
-
// Lock out the user
|
|
122
|
-
const lockUntil = new Date(now.getTime() + config.lockoutMs);
|
|
123
|
-
db.prepare(
|
|
124
|
-
`
|
|
125
|
-
UPDATE rate_limits
|
|
126
|
-
SET last_attempt = ?, expires_at = ?
|
|
127
|
-
WHERE identifier = ? AND action = ?
|
|
128
|
-
`,
|
|
129
|
-
).run(now.toISOString(), lockUntil.toISOString(), identifier, action);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
allowed: false,
|
|
133
|
-
remaining: 0,
|
|
134
|
-
resetAt: windowEnd,
|
|
135
|
-
lockedUntil: lockUntil,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Increment attempts
|
|
140
|
-
db.prepare(
|
|
141
|
-
`
|
|
142
|
-
UPDATE rate_limits
|
|
143
|
-
SET attempts = attempts + 1, last_attempt = ?
|
|
144
|
-
WHERE identifier = ? AND action = ?
|
|
145
|
-
`,
|
|
146
|
-
).run(now.toISOString(), identifier, action);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
allowed: true,
|
|
150
|
-
remaining: remaining - 1,
|
|
151
|
-
resetAt: windowEnd,
|
|
152
|
-
lockedUntil: null,
|
|
153
|
-
};
|
|
154
|
-
} finally {
|
|
155
|
-
db.close();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export async function resetRateLimit(
|
|
160
|
-
identifier: string,
|
|
161
|
-
action: string = "login",
|
|
162
|
-
): Promise<void> {
|
|
163
|
-
const db = getDb();
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
db.prepare(
|
|
167
|
-
`
|
|
168
|
-
DELETE FROM rate_limits WHERE identifier = ? AND action = ?
|
|
169
|
-
`,
|
|
170
|
-
).run(identifier, action);
|
|
171
|
-
} finally {
|
|
172
|
-
db.close();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export async function getAccountLockStatus(email: string): Promise<{
|
|
177
|
-
isLocked: boolean;
|
|
178
|
-
lockedUntil: Date | null;
|
|
179
|
-
failedAttempts: number;
|
|
180
|
-
}> {
|
|
181
|
-
const db = getDb();
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const user = db
|
|
185
|
-
.prepare(
|
|
186
|
-
`
|
|
187
|
-
SELECT locked, locked_until, failed_login_attempts
|
|
188
|
-
FROM users
|
|
189
|
-
WHERE LOWER(email) = LOWER(?)
|
|
190
|
-
`,
|
|
191
|
-
)
|
|
192
|
-
.get(email) as any;
|
|
193
|
-
|
|
194
|
-
if (!user) {
|
|
195
|
-
return { isLocked: false, lockedUntil: null, failedAttempts: 0 };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const now = new Date();
|
|
199
|
-
const lockedUntil = user.locked_until ? new Date(user.locked_until) : null;
|
|
200
|
-
const isLocked = user.locked === 1 && (!lockedUntil || lockedUntil > now);
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
isLocked,
|
|
204
|
-
lockedUntil: isLocked ? lockedUntil : null,
|
|
205
|
-
failedAttempts: user.failed_login_attempts || 0,
|
|
206
|
-
};
|
|
207
|
-
} finally {
|
|
208
|
-
db.close();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export async function recordFailedLogin(email: string): Promise<void> {
|
|
213
|
-
const db = getDb();
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const now = new Date();
|
|
217
|
-
const user = db
|
|
218
|
-
.prepare(
|
|
219
|
-
`
|
|
220
|
-
SELECT id FROM users WHERE LOWER(email) = LOWER(?)
|
|
221
|
-
`,
|
|
222
|
-
)
|
|
223
|
-
.get(email) as any;
|
|
224
|
-
|
|
225
|
-
if (!user) return;
|
|
226
|
-
|
|
227
|
-
// Increment failed attempts
|
|
228
|
-
db.prepare(
|
|
229
|
-
`
|
|
230
|
-
UPDATE users
|
|
231
|
-
SET failed_login_attempts = COALESCE(failed_login_attempts, 0) + 1,
|
|
232
|
-
last_login = ?,
|
|
233
|
-
locked = CASE
|
|
234
|
-
WHEN COALESCE(failed_login_attempts, 0) >= 4 THEN 1
|
|
235
|
-
ELSE 0
|
|
236
|
-
END,
|
|
237
|
-
locked_until = CASE
|
|
238
|
-
WHEN COALESCE(failed_login_attempts, 0) >= 4 THEN ?
|
|
239
|
-
ELSE NULL
|
|
240
|
-
END
|
|
241
|
-
WHERE id = ?
|
|
242
|
-
`,
|
|
243
|
-
).run(
|
|
244
|
-
now.toISOString(),
|
|
245
|
-
new Date(now.getTime() + 15 * 60 * 1000).toISOString(),
|
|
246
|
-
user.id,
|
|
247
|
-
);
|
|
248
|
-
} finally {
|
|
249
|
-
db.close();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export async function unlockAccount(email: string): Promise<void> {
|
|
254
|
-
const db = getDb();
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
db.prepare(
|
|
258
|
-
`
|
|
259
|
-
UPDATE users
|
|
260
|
-
SET locked = 0, locked_until = NULL, failed_login_attempts = 0
|
|
261
|
-
WHERE LOWER(email) = LOWER(?)
|
|
262
|
-
`,
|
|
263
|
-
).run(email);
|
|
264
|
-
} finally {
|
|
265
|
-
db.close();
|
|
266
|
-
}
|
|
267
|
-
}
|