nuxt-auto-crud 1.11.0 → 1.12.0
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 +47 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -2
- package/dist/runtime/auth.d.ts +6 -0
- package/dist/runtime/server/api/[model]/[id].delete.js +4 -20
- package/dist/runtime/server/api/[model]/[id].get.js +4 -20
- package/dist/runtime/server/api/[model]/[id].patch.js +4 -20
- package/dist/runtime/server/api/[model]/index.get.js +5 -23
- package/dist/runtime/server/api/[model]/index.post.js +5 -21
- package/dist/runtime/server/api/_relations.get.js +3 -21
- package/dist/runtime/server/api/_schema/[table].get.js +2 -20
- package/dist/runtime/server/api/_schema/index.get.js +3 -21
- package/dist/runtime/server/utils/auth.d.ts +2 -0
- package/dist/runtime/server/utils/auth.js +29 -9
- package/dist/runtime/server/utils/handler.d.ts +3 -0
- package/dist/runtime/server/utils/handler.js +26 -0
- package/dist/runtime/server/utils/schema.js +20 -16
- package/package.json +1 -1
- package/src/runtime/auth.d.ts +6 -0
- package/src/runtime/server/api/[model]/[id].delete.ts +4 -28
- package/src/runtime/server/api/[model]/[id].get.ts +4 -27
- package/src/runtime/server/api/[model]/[id].patch.ts +6 -28
- package/src/runtime/server/api/[model]/index.get.ts +5 -31
- package/src/runtime/server/api/[model]/index.post.ts +5 -28
- package/src/runtime/server/api/_relations.get.ts +4 -27
- package/src/runtime/server/api/_schema/[table].get.ts +3 -25
- package/src/runtime/server/api/_schema/index.get.ts +4 -27
- package/src/runtime/server/utils/auth.ts +35 -9
- package/src/runtime/server/utils/handler.ts +36 -0
- package/src/runtime/server/utils/schema.ts +29 -23
- package/dist/runtime/server/plugins/seed.d.ts +0 -2
- package/dist/runtime/server/plugins/seed.js +0 -29
- package/src/runtime/server/plugins/seed.ts +0 -41
package/README.md
CHANGED
|
@@ -57,10 +57,15 @@ If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
|
|
|
57
57
|
```bash
|
|
58
58
|
# Install module and required dependencies
|
|
59
59
|
npm install nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
60
|
+
|
|
61
|
+
# Optional: Install auth dependencies if using Session Auth (Recommended)
|
|
62
|
+
npm install nuxt-auth-utils nuxt-authorization
|
|
63
|
+
|
|
60
64
|
npm install --save-dev wrangler drizzle-kit
|
|
61
65
|
|
|
62
66
|
# Or using bun
|
|
63
67
|
bun add nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
68
|
+
bun add nuxt-auth-utils nuxt-authorization
|
|
64
69
|
bun add --dev wrangler drizzle-kit
|
|
65
70
|
```
|
|
66
71
|
|
|
@@ -245,10 +250,19 @@ The module enables authentication by default. To test APIs without authenticatio
|
|
|
245
250
|
|
|
246
251
|
### Session Auth (Default)
|
|
247
252
|
|
|
248
|
-
Requires `nuxt-auth-utils` and `nuxt-authorization
|
|
253
|
+
Requires `nuxt-auth-utils` and `nuxt-authorization` to be installed in your project.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install nuxt-auth-utils nuxt-authorization
|
|
257
|
+
```
|
|
249
258
|
|
|
250
259
|
```typescript
|
|
251
260
|
export default defineNuxtConfig({
|
|
261
|
+
modules: [
|
|
262
|
+
'nuxt-auth-utils',
|
|
263
|
+
'nuxt-authorization',
|
|
264
|
+
'nuxt-auto-crud'
|
|
265
|
+
],
|
|
252
266
|
autoCrud: {
|
|
253
267
|
auth: {
|
|
254
268
|
type: 'session',
|
|
@@ -261,7 +275,7 @@ export default defineNuxtConfig({
|
|
|
261
275
|
|
|
262
276
|
### JWT Auth
|
|
263
277
|
|
|
264
|
-
Useful for backend-only apps.
|
|
278
|
+
Useful for backend-only apps. Does **not** require `nuxt-auth-utils`.
|
|
265
279
|
|
|
266
280
|
```typescript
|
|
267
281
|
export default defineNuxtConfig({
|
|
@@ -276,6 +290,36 @@ export default defineNuxtConfig({
|
|
|
276
290
|
})
|
|
277
291
|
```
|
|
278
292
|
|
|
293
|
+
### Disabling Auth
|
|
294
|
+
|
|
295
|
+
You can disable authentication entirely for testing or public APIs.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
export default defineNuxtConfig({
|
|
299
|
+
autoCrud: {
|
|
300
|
+
auth: {
|
|
301
|
+
authentication: false,
|
|
302
|
+
authorization: false
|
|
303
|
+
}
|
|
304
|
+
// OR simply:
|
|
305
|
+
// auth: false
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## 🧪 Testing
|
|
311
|
+
|
|
312
|
+
This module is tested using **Vitest**.
|
|
313
|
+
|
|
314
|
+
- **Unit Tests:** We cover utility functions and helpers.
|
|
315
|
+
- **E2E Tests:** We verify the API endpoints using a real Nuxt server instance.
|
|
316
|
+
|
|
317
|
+
To run the tests locally:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
npm run test
|
|
321
|
+
```
|
|
322
|
+
|
|
279
323
|
## 🛡️ Resource Configuration (RBAC)
|
|
280
324
|
|
|
281
325
|
You can define fine-grained access control and resource policies using `autocrud.config.ts` in your project root. This file is optional and useful when you need specific rules per resource.
|
|
@@ -302,6 +346,7 @@ export default {
|
|
|
302
346
|
## ⚠️ Known Issues
|
|
303
347
|
|
|
304
348
|
- **Foreign Key Naming:** Currently, if you have multiple foreign keys referring to the same table (e.g., `customer_id` and `author_id` both referring to the `users` table), the automatic relation handling might assume `user_id` for both. This is a known limitation in the current alpha version.
|
|
349
|
+
- **Sidebar Visibility:** For non-admin users, the sidebar resource list might not be fully visible or populated in the current playground implementation. This is a known limitation.
|
|
305
350
|
|
|
306
351
|
## 🎮 Try the Playground
|
|
307
352
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImportsDir
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImportsDir } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const module$1 = defineNuxtModule({
|
|
4
4
|
meta: {
|
|
@@ -22,6 +22,11 @@ const module$1 = defineNuxtModule({
|
|
|
22
22
|
options.drizzlePath
|
|
23
23
|
);
|
|
24
24
|
nuxt.options.alias["#site/drizzle"] = drizzlePath;
|
|
25
|
+
const abilityPath = resolver.resolve(
|
|
26
|
+
nuxt.options.rootDir,
|
|
27
|
+
"server/utils/ability"
|
|
28
|
+
);
|
|
29
|
+
nuxt.options.alias["#site/ability"] = abilityPath;
|
|
25
30
|
nuxt.options.alias["#authorization"] = nuxt.options.alias["#authorization"] || "nuxt-authorization/utils";
|
|
26
31
|
const { loadConfig } = await import('c12');
|
|
27
32
|
const { config: externalConfig } = await loadConfig({
|
|
@@ -116,7 +121,6 @@ const module$1 = defineNuxtModule({
|
|
|
116
121
|
});
|
|
117
122
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
118
123
|
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
119
|
-
addServerPlugin(resolver.resolve("./runtime/server/plugins/seed"));
|
|
120
124
|
}
|
|
121
125
|
});
|
|
122
126
|
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel, getModelSingularName
|
|
3
|
+
import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
-
import {
|
|
6
|
-
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
|
+
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
7
6
|
export default eventHandler(async (event) => {
|
|
8
|
-
const { resources } = useAutoCrudConfig();
|
|
9
7
|
const { model, id } = getRouterParams(event);
|
|
10
|
-
const isAdmin = await
|
|
11
|
-
if (!isAdmin) {
|
|
12
|
-
const resourceConfig = resources?.[model];
|
|
13
|
-
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("delete");
|
|
14
|
-
if (!isPublic) {
|
|
15
|
-
throw createError({
|
|
16
|
-
statusCode: 401,
|
|
17
|
-
message: "Unauthorized"
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
8
|
+
const isAdmin = await ensureResourceAccess(event, model, "delete");
|
|
21
9
|
const table = getTableForModel(model);
|
|
22
10
|
const singularName = getModelSingularName(model);
|
|
23
11
|
const deletedRecord = await useDrizzle().delete(table).where(eq(table.id, Number(id))).returning().get();
|
|
@@ -27,9 +15,5 @@ export default eventHandler(async (event) => {
|
|
|
27
15
|
message: `${singularName} not found`
|
|
28
16
|
});
|
|
29
17
|
}
|
|
30
|
-
|
|
31
|
-
return filterHiddenFields(model, deletedRecord);
|
|
32
|
-
} else {
|
|
33
|
-
return filterPublicColumns(model, deletedRecord);
|
|
34
|
-
}
|
|
18
|
+
return formatResourceResult(model, deletedRecord, isAdmin);
|
|
35
19
|
});
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel
|
|
3
|
+
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
-
import {
|
|
6
|
-
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
|
+
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
7
6
|
export default eventHandler(async (event) => {
|
|
8
|
-
const { resources } = useAutoCrudConfig();
|
|
9
7
|
const { model, id } = getRouterParams(event);
|
|
10
|
-
const isAdmin = await
|
|
11
|
-
if (!isAdmin) {
|
|
12
|
-
const resourceConfig = resources?.[model];
|
|
13
|
-
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("read");
|
|
14
|
-
if (!isPublic) {
|
|
15
|
-
throw createError({
|
|
16
|
-
statusCode: 401,
|
|
17
|
-
message: "Unauthorized"
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
8
|
+
const isAdmin = await ensureResourceAccess(event, model, "read");
|
|
21
9
|
const table = getTableForModel(model);
|
|
22
10
|
const record = await useDrizzle().select().from(table).where(eq(table.id, Number(id))).get();
|
|
23
11
|
if (!record) {
|
|
@@ -26,9 +14,5 @@ export default eventHandler(async (event) => {
|
|
|
26
14
|
message: "Record not found"
|
|
27
15
|
});
|
|
28
16
|
}
|
|
29
|
-
|
|
30
|
-
return filterHiddenFields(model, record);
|
|
31
|
-
} else {
|
|
32
|
-
return filterPublicColumns(model, record);
|
|
33
|
-
}
|
|
17
|
+
return formatResourceResult(model, record, isAdmin);
|
|
34
18
|
});
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel, filterUpdatableFields
|
|
3
|
+
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
-
import {
|
|
6
|
-
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
|
+
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
7
6
|
export default eventHandler(async (event) => {
|
|
8
|
-
const { resources } = useAutoCrudConfig();
|
|
9
7
|
const { model, id } = getRouterParams(event);
|
|
10
|
-
const isAdmin = await
|
|
11
|
-
if (!isAdmin) {
|
|
12
|
-
const resourceConfig = resources?.[model];
|
|
13
|
-
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("update");
|
|
14
|
-
if (!isPublic) {
|
|
15
|
-
throw createError({
|
|
16
|
-
statusCode: 401,
|
|
17
|
-
message: "Unauthorized"
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
8
|
+
const isAdmin = await ensureResourceAccess(event, model, "update");
|
|
21
9
|
const table = getTableForModel(model);
|
|
22
10
|
const body = await readBody(event);
|
|
23
11
|
const payload = filterUpdatableFields(model, body);
|
|
@@ -31,9 +19,5 @@ export default eventHandler(async (event) => {
|
|
|
31
19
|
message: "Record not found"
|
|
32
20
|
});
|
|
33
21
|
}
|
|
34
|
-
|
|
35
|
-
return filterHiddenFields(model, updatedRecord);
|
|
36
|
-
} else {
|
|
37
|
-
return filterPublicColumns(model, updatedRecord);
|
|
38
|
-
}
|
|
22
|
+
return formatResourceResult(model, updatedRecord, isAdmin);
|
|
39
23
|
});
|
|
@@ -1,31 +1,13 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams
|
|
2
|
-
import { getTableForModel
|
|
1
|
+
import { eventHandler, getRouterParams } from "h3";
|
|
2
|
+
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
3
3
|
import { useDrizzle } from "#site/drizzle";
|
|
4
|
-
import { useAutoCrudConfig } from "../../utils/config.js";
|
|
5
|
-
import { checkAdminAccess } from "../../utils/auth.js";
|
|
6
4
|
import { desc } from "drizzle-orm";
|
|
5
|
+
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
7
6
|
export default eventHandler(async (event) => {
|
|
8
7
|
console.log("[GET] Request received", event.path);
|
|
9
|
-
const { resources } = useAutoCrudConfig();
|
|
10
8
|
const { model } = getRouterParams(event);
|
|
11
|
-
const isAdmin = await
|
|
12
|
-
if (!isAdmin) {
|
|
13
|
-
const resourceConfig = resources?.[model];
|
|
14
|
-
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("list");
|
|
15
|
-
if (!isPublic) {
|
|
16
|
-
throw createError({
|
|
17
|
-
statusCode: 401,
|
|
18
|
-
message: "Unauthorized"
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
9
|
+
const isAdmin = await ensureResourceAccess(event, model, "list");
|
|
22
10
|
const table = getTableForModel(model);
|
|
23
11
|
const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all();
|
|
24
|
-
return results.map((item) =>
|
|
25
|
-
if (isAdmin) {
|
|
26
|
-
return filterHiddenFields(model, item);
|
|
27
|
-
} else {
|
|
28
|
-
return filterPublicColumns(model, item);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
12
|
+
return results.map((item) => formatResourceResult(model, item, isAdmin));
|
|
31
13
|
});
|
|
@@ -1,29 +1,13 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams, readBody
|
|
2
|
-
import { getTableForModel,
|
|
1
|
+
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
|
+
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
3
3
|
import { useDrizzle } from "#site/drizzle";
|
|
4
|
-
import {
|
|
5
|
-
import { checkAdminAccess } from "../../utils/auth.js";
|
|
4
|
+
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
6
5
|
export default eventHandler(async (event) => {
|
|
7
|
-
const { resources } = useAutoCrudConfig();
|
|
8
6
|
const { model } = getRouterParams(event);
|
|
9
|
-
const isAdmin = await
|
|
10
|
-
if (!isAdmin) {
|
|
11
|
-
const resourceConfig = resources?.[model];
|
|
12
|
-
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("create");
|
|
13
|
-
if (!isPublic) {
|
|
14
|
-
throw createError({
|
|
15
|
-
statusCode: 401,
|
|
16
|
-
message: "Unauthorized"
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
}
|
|
7
|
+
const isAdmin = await ensureResourceAccess(event, model, "create");
|
|
20
8
|
const table = getTableForModel(model);
|
|
21
9
|
const body = await readBody(event);
|
|
22
10
|
const payload = filterUpdatableFields(model, body);
|
|
23
11
|
const newRecord = await useDrizzle().insert(table).values(payload).returning().get();
|
|
24
|
-
|
|
25
|
-
return filterHiddenFields(model, newRecord);
|
|
26
|
-
} else {
|
|
27
|
-
return filterPublicColumns(model, newRecord);
|
|
28
|
-
}
|
|
12
|
+
return formatResourceResult(model, newRecord, isAdmin);
|
|
29
13
|
});
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import { eventHandler
|
|
2
|
-
import { requireUserSession } from "#imports";
|
|
1
|
+
import { eventHandler } from "h3";
|
|
3
2
|
import { getRelations } from "../utils/schema.js";
|
|
4
|
-
import {
|
|
5
|
-
import { verifyJwtToken } from "../utils/jwt.js";
|
|
3
|
+
import { ensureAuthenticated } from "../utils/auth.js";
|
|
6
4
|
export default eventHandler(async (event) => {
|
|
7
|
-
|
|
8
|
-
if (auth?.authentication) {
|
|
9
|
-
let isAuthenticated = false;
|
|
10
|
-
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
11
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
|
|
12
|
-
} else {
|
|
13
|
-
try {
|
|
14
|
-
await requireUserSession(event);
|
|
15
|
-
isAuthenticated = true;
|
|
16
|
-
} catch {
|
|
17
|
-
isAuthenticated = false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (!isAuthenticated) {
|
|
21
|
-
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
5
|
+
await ensureAuthenticated(event);
|
|
24
6
|
return getRelations();
|
|
25
7
|
});
|
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
import { eventHandler, createError, getRouterParam } from "h3";
|
|
2
|
-
import { requireUserSession } from "#imports";
|
|
3
2
|
import { getSchema } from "../../utils/schema.js";
|
|
4
|
-
import {
|
|
5
|
-
import { verifyJwtToken } from "../../utils/jwt.js";
|
|
3
|
+
import { ensureAuthenticated } from "../../utils/auth.js";
|
|
6
4
|
export default eventHandler(async (event) => {
|
|
7
|
-
|
|
5
|
+
await ensureAuthenticated(event);
|
|
8
6
|
const tableName = getRouterParam(event, "table");
|
|
9
|
-
if (auth?.authentication) {
|
|
10
|
-
let isAuthenticated = false;
|
|
11
|
-
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
12
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
|
|
13
|
-
} else {
|
|
14
|
-
try {
|
|
15
|
-
await requireUserSession(event);
|
|
16
|
-
isAuthenticated = true;
|
|
17
|
-
} catch {
|
|
18
|
-
isAuthenticated = false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
if (!isAuthenticated) {
|
|
22
|
-
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
7
|
if (!tableName) {
|
|
26
8
|
throw createError({ statusCode: 400, message: "Table name is required" });
|
|
27
9
|
}
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import { eventHandler
|
|
2
|
-
import { requireUserSession } from "#imports";
|
|
1
|
+
import { eventHandler } from "h3";
|
|
3
2
|
import { getAllSchemas } from "../../utils/schema.js";
|
|
4
|
-
import {
|
|
5
|
-
import { verifyJwtToken } from "../../utils/jwt.js";
|
|
3
|
+
import { ensureAuthenticated } from "../../utils/auth.js";
|
|
6
4
|
export default eventHandler(async (event) => {
|
|
7
|
-
|
|
8
|
-
if (auth?.authentication) {
|
|
9
|
-
let isAuthenticated = false;
|
|
10
|
-
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
11
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
|
|
12
|
-
} else {
|
|
13
|
-
try {
|
|
14
|
-
await requireUserSession(event);
|
|
15
|
-
isAuthenticated = true;
|
|
16
|
-
} catch {
|
|
17
|
-
isAuthenticated = false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (!isAuthenticated) {
|
|
21
|
-
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
5
|
+
await ensureAuthenticated(event);
|
|
24
6
|
return getAllSchemas();
|
|
25
7
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import "../../auth.d.ts";
|
|
1
2
|
import { createError } from "h3";
|
|
2
|
-
import
|
|
3
|
+
import globalAbility from "#site/ability";
|
|
4
|
+
import { requireUserSession, allows } from "#imports";
|
|
3
5
|
import { useAutoCrudConfig } from "./config.js";
|
|
4
6
|
import { verifyJwtToken } from "./jwt.js";
|
|
5
7
|
export async function checkAdminAccess(event, model, action) {
|
|
@@ -17,14 +19,12 @@ export async function checkAdminAccess(event, model, action) {
|
|
|
17
19
|
try {
|
|
18
20
|
await requireUserSession(event);
|
|
19
21
|
if (auth.authorization) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
}
|
|
22
|
+
const allowed = await allows(event, globalAbility, model, action);
|
|
23
|
+
if (!allowed) {
|
|
24
|
+
throw createError({
|
|
25
|
+
statusCode: 403,
|
|
26
|
+
message: "Forbidden"
|
|
27
|
+
});
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return true;
|
|
@@ -35,3 +35,23 @@ export async function checkAdminAccess(event, model, action) {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export async function ensureAuthenticated(event) {
|
|
39
|
+
const { auth } = useAutoCrudConfig();
|
|
40
|
+
if (!auth?.authentication) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let isAuthenticated = false;
|
|
44
|
+
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
45
|
+
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
await requireUserSession(event);
|
|
49
|
+
isAuthenticated = true;
|
|
50
|
+
} catch {
|
|
51
|
+
isAuthenticated = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!isAuthenticated) {
|
|
55
|
+
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
export declare function ensureResourceAccess(event: H3Event, model: string, action: string): Promise<boolean>;
|
|
3
|
+
export declare function formatResourceResult(model: string, data: Record<string, unknown>, isAdmin: boolean): Record<string, unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
import { useAutoCrudConfig } from "./config.js";
|
|
3
|
+
import { checkAdminAccess } from "./auth.js";
|
|
4
|
+
import { filterHiddenFields, filterPublicColumns } from "./modelMapper.js";
|
|
5
|
+
export async function ensureResourceAccess(event, model, action) {
|
|
6
|
+
const { resources } = useAutoCrudConfig();
|
|
7
|
+
const isAdmin = await checkAdminAccess(event, model, action);
|
|
8
|
+
if (!isAdmin) {
|
|
9
|
+
const resourceConfig = resources?.[model];
|
|
10
|
+
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes(action);
|
|
11
|
+
if (!isPublic) {
|
|
12
|
+
throw createError({
|
|
13
|
+
statusCode: 401,
|
|
14
|
+
message: "Unauthorized"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return isAdmin;
|
|
19
|
+
}
|
|
20
|
+
export function formatResourceResult(model, data, isAdmin) {
|
|
21
|
+
if (isAdmin) {
|
|
22
|
+
return filterHiddenFields(model, data);
|
|
23
|
+
} else {
|
|
24
|
+
return filterPublicColumns(model, data);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -7,22 +7,7 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
7
7
|
for (const [key, col] of Object.entries(columns)) {
|
|
8
8
|
const column = col;
|
|
9
9
|
const isRequired = column.notNull;
|
|
10
|
-
|
|
11
|
-
let selectOptions = void 0;
|
|
12
|
-
const enumValues = column.enumValues || column.config?.enumValues;
|
|
13
|
-
if (enumValues) {
|
|
14
|
-
type = "enum";
|
|
15
|
-
selectOptions = enumValues;
|
|
16
|
-
} else if (column.dataType === "number" || column.columnType === "SQLiteInteger" || column.columnType === "SQLiteReal") {
|
|
17
|
-
type = "number";
|
|
18
|
-
if (column.name.endsWith("_at") || column.name.endsWith("At")) {
|
|
19
|
-
type = "date";
|
|
20
|
-
}
|
|
21
|
-
} else if (column.dataType === "boolean") {
|
|
22
|
-
type = "boolean";
|
|
23
|
-
} else if (column.dataType === "date" || column.dataType === "string" && (column.name.endsWith("_at") || column.name.endsWith("At"))) {
|
|
24
|
-
type = "date";
|
|
25
|
-
}
|
|
10
|
+
const { type, selectOptions } = mapColumnType(column);
|
|
26
11
|
fields.push({
|
|
27
12
|
name: key,
|
|
28
13
|
type,
|
|
@@ -35,6 +20,25 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
35
20
|
fields
|
|
36
21
|
};
|
|
37
22
|
}
|
|
23
|
+
function mapColumnType(column) {
|
|
24
|
+
const enumValues = column.enumValues || column.config?.enumValues;
|
|
25
|
+
if (enumValues) {
|
|
26
|
+
return { type: "enum", selectOptions: enumValues };
|
|
27
|
+
}
|
|
28
|
+
if (column.dataType === "boolean") {
|
|
29
|
+
return { type: "boolean" };
|
|
30
|
+
}
|
|
31
|
+
if (column.dataType === "date" || column.dataType === "string" && (column.name.endsWith("_at") || column.name.endsWith("At"))) {
|
|
32
|
+
return { type: "date" };
|
|
33
|
+
}
|
|
34
|
+
if (column.dataType === "number" || column.columnType === "SQLiteInteger" || column.columnType === "SQLiteReal") {
|
|
35
|
+
if (column.name.endsWith("_at") || column.name.endsWith("At")) {
|
|
36
|
+
return { type: "date" };
|
|
37
|
+
}
|
|
38
|
+
return { type: "number" };
|
|
39
|
+
}
|
|
40
|
+
return { type: "string" };
|
|
41
|
+
}
|
|
38
42
|
export async function getRelations() {
|
|
39
43
|
const relations = {};
|
|
40
44
|
for (const [tableName, table] of Object.entries(modelTableMap)) {
|
package/package.json
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
// server/api/[model]/[id].delete.ts
|
|
2
2
|
import { eventHandler, getRouterParams, createError } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
|
-
import { getTableForModel, getModelSingularName
|
|
5
|
-
|
|
4
|
+
import { getTableForModel, getModelSingularName } from '../../utils/modelMapper'
|
|
6
5
|
import type { TableWithId } from '../../types'
|
|
7
6
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
7
|
import { useDrizzle } from '#site/drizzle'
|
|
9
|
-
|
|
10
|
-
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
-
import { checkAdminAccess } from '../../utils/auth'
|
|
8
|
+
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
12
9
|
|
|
13
10
|
export default eventHandler(async (event) => {
|
|
14
|
-
const { resources } = useAutoCrudConfig()
|
|
15
11
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
16
|
-
|
|
17
|
-
const isAdmin = await checkAdminAccess(event, model, 'delete')
|
|
18
|
-
|
|
19
|
-
// Check public access if not admin
|
|
20
|
-
if (!isAdmin) {
|
|
21
|
-
const resourceConfig = resources?.[model]
|
|
22
|
-
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('delete'))
|
|
23
|
-
|
|
24
|
-
if (!isPublic) {
|
|
25
|
-
throw createError({
|
|
26
|
-
statusCode: 401,
|
|
27
|
-
message: 'Unauthorized',
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
}
|
|
12
|
+
const isAdmin = await ensureResourceAccess(event, model, 'delete')
|
|
31
13
|
|
|
32
14
|
const table = getTableForModel(model) as TableWithId
|
|
33
|
-
|
|
34
15
|
const singularName = getModelSingularName(model)
|
|
35
16
|
|
|
36
17
|
const deletedRecord = await useDrizzle()
|
|
@@ -46,10 +27,5 @@ export default eventHandler(async (event) => {
|
|
|
46
27
|
})
|
|
47
28
|
}
|
|
48
29
|
|
|
49
|
-
|
|
50
|
-
return filterHiddenFields(model, deletedRecord as Record<string, unknown>)
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
return filterPublicColumns(model, deletedRecord as Record<string, unknown>)
|
|
54
|
-
}
|
|
30
|
+
return formatResourceResult(model, deletedRecord as Record<string, unknown>, isAdmin)
|
|
55
31
|
})
|
|
@@ -1,33 +1,15 @@
|
|
|
1
1
|
// server/api/[model]/[id].get.ts
|
|
2
2
|
import { eventHandler, getRouterParams, createError } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
|
-
import { getTableForModel
|
|
5
|
-
|
|
4
|
+
import { getTableForModel } from '../../utils/modelMapper'
|
|
6
5
|
import type { TableWithId } from '../../types'
|
|
7
6
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
7
|
import { useDrizzle } from '#site/drizzle'
|
|
9
|
-
|
|
10
|
-
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
-
import { checkAdminAccess } from '../../utils/auth'
|
|
8
|
+
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
12
9
|
|
|
13
10
|
export default eventHandler(async (event) => {
|
|
14
|
-
const { resources } = useAutoCrudConfig()
|
|
15
11
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
16
|
-
|
|
17
|
-
const isAdmin = await checkAdminAccess(event, model, 'read')
|
|
18
|
-
|
|
19
|
-
// Check public access if not admin
|
|
20
|
-
if (!isAdmin) {
|
|
21
|
-
const resourceConfig = resources?.[model]
|
|
22
|
-
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('read'))
|
|
23
|
-
|
|
24
|
-
if (!isPublic) {
|
|
25
|
-
throw createError({
|
|
26
|
-
statusCode: 401,
|
|
27
|
-
message: 'Unauthorized',
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
}
|
|
12
|
+
const isAdmin = await ensureResourceAccess(event, model, 'read')
|
|
31
13
|
|
|
32
14
|
const table = getTableForModel(model) as TableWithId
|
|
33
15
|
|
|
@@ -44,10 +26,5 @@ export default eventHandler(async (event) => {
|
|
|
44
26
|
})
|
|
45
27
|
}
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
return filterHiddenFields(model, record as Record<string, unknown>)
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
return filterPublicColumns(model, record as Record<string, unknown>)
|
|
52
|
-
}
|
|
29
|
+
return formatResourceResult(model, record as Record<string, unknown>, isAdmin)
|
|
53
30
|
})
|
|
@@ -1,33 +1,15 @@
|
|
|
1
1
|
// server/api/[model]/[id].patch.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody, createError } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
|
-
import { getTableForModel, filterUpdatableFields
|
|
5
|
-
|
|
4
|
+
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
6
5
|
import type { TableWithId } from '../../types'
|
|
7
6
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
7
|
import { useDrizzle } from '#site/drizzle'
|
|
9
|
-
|
|
10
|
-
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
-
import { checkAdminAccess } from '../../utils/auth'
|
|
8
|
+
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
12
9
|
|
|
13
10
|
export default eventHandler(async (event) => {
|
|
14
|
-
const { resources } = useAutoCrudConfig()
|
|
15
11
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
16
|
-
|
|
17
|
-
const isAdmin = await checkAdminAccess(event, model, 'update')
|
|
18
|
-
|
|
19
|
-
// Check public access if not admin
|
|
20
|
-
if (!isAdmin) {
|
|
21
|
-
const resourceConfig = resources?.[model]
|
|
22
|
-
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('update'))
|
|
23
|
-
|
|
24
|
-
if (!isPublic) {
|
|
25
|
-
throw createError({
|
|
26
|
-
statusCode: 401,
|
|
27
|
-
message: 'Unauthorized',
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
}
|
|
12
|
+
const isAdmin = await ensureResourceAccess(event, model, 'update')
|
|
31
13
|
|
|
32
14
|
const table = getTableForModel(model) as TableWithId
|
|
33
15
|
|
|
@@ -36,7 +18,8 @@ export default eventHandler(async (event) => {
|
|
|
36
18
|
|
|
37
19
|
// Automatically update updatedAt if it exists
|
|
38
20
|
if ('updatedAt' in table) {
|
|
39
|
-
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
(payload as any).updatedAt = new Date()
|
|
40
23
|
}
|
|
41
24
|
|
|
42
25
|
const updatedRecord = await useDrizzle()
|
|
@@ -53,10 +36,5 @@ export default eventHandler(async (event) => {
|
|
|
53
36
|
})
|
|
54
37
|
}
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
return filterHiddenFields(model, updatedRecord as Record<string, unknown>)
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
return filterPublicColumns(model, updatedRecord as Record<string, unknown>)
|
|
61
|
-
}
|
|
39
|
+
return formatResourceResult(model, updatedRecord as Record<string, unknown>, isAdmin)
|
|
62
40
|
})
|
|
@@ -1,46 +1,20 @@
|
|
|
1
1
|
// server/api/[model]/index.get.ts
|
|
2
|
-
import { eventHandler, getRouterParams
|
|
3
|
-
import { getTableForModel
|
|
4
|
-
|
|
2
|
+
import { eventHandler, getRouterParams } from 'h3'
|
|
3
|
+
import { getTableForModel } from '../../utils/modelMapper'
|
|
5
4
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
6
5
|
import { useDrizzle } from '#site/drizzle'
|
|
7
|
-
|
|
8
|
-
import { useAutoCrudConfig } from '../../utils/config'
|
|
9
|
-
import { checkAdminAccess } from '../../utils/auth'
|
|
10
|
-
|
|
11
6
|
import { desc } from 'drizzle-orm'
|
|
12
7
|
import type { TableWithId } from '../../types'
|
|
8
|
+
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
13
9
|
|
|
14
10
|
export default eventHandler(async (event) => {
|
|
15
11
|
console.log('[GET] Request received', event.path)
|
|
16
|
-
const { resources } = useAutoCrudConfig()
|
|
17
12
|
const { model } = getRouterParams(event) as { model: string }
|
|
18
|
-
|
|
19
|
-
const isAdmin = await checkAdminAccess(event, model, 'list')
|
|
20
|
-
|
|
21
|
-
// Check public access if not admin
|
|
22
|
-
if (!isAdmin) {
|
|
23
|
-
const resourceConfig = resources?.[model]
|
|
24
|
-
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('list'))
|
|
25
|
-
|
|
26
|
-
if (!isPublic) {
|
|
27
|
-
throw createError({
|
|
28
|
-
statusCode: 401,
|
|
29
|
-
message: 'Unauthorized',
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
}
|
|
13
|
+
const isAdmin = await ensureResourceAccess(event, model, 'list')
|
|
33
14
|
|
|
34
15
|
const table = getTableForModel(model) as TableWithId
|
|
35
16
|
|
|
36
17
|
const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all()
|
|
37
18
|
|
|
38
|
-
return results.map((item: Record<string, unknown>) =>
|
|
39
|
-
if (isAdmin) {
|
|
40
|
-
return filterHiddenFields(model, item as Record<string, unknown>)
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
return filterPublicColumns(model, item as Record<string, unknown>)
|
|
44
|
-
}
|
|
45
|
-
})
|
|
19
|
+
return results.map((item: Record<string, unknown>) => formatResourceResult(model, item, isAdmin))
|
|
46
20
|
})
|
|
@@ -1,31 +1,13 @@
|
|
|
1
1
|
// server/api/[model]/index.post.ts
|
|
2
|
-
import { eventHandler, getRouterParams, readBody
|
|
3
|
-
import { getTableForModel,
|
|
4
|
-
|
|
2
|
+
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
|
+
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
5
4
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
6
5
|
import { useDrizzle } from '#site/drizzle'
|
|
7
|
-
|
|
8
|
-
import { useAutoCrudConfig } from '../../utils/config'
|
|
9
|
-
import { checkAdminAccess } from '../../utils/auth'
|
|
6
|
+
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
10
7
|
|
|
11
8
|
export default eventHandler(async (event) => {
|
|
12
|
-
const { resources } = useAutoCrudConfig()
|
|
13
9
|
const { model } = getRouterParams(event) as { model: string }
|
|
14
|
-
|
|
15
|
-
const isAdmin = await checkAdminAccess(event, model, 'create')
|
|
16
|
-
|
|
17
|
-
// Check public access if not admin
|
|
18
|
-
if (!isAdmin) {
|
|
19
|
-
const resourceConfig = resources?.[model]
|
|
20
|
-
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('create'))
|
|
21
|
-
|
|
22
|
-
if (!isPublic) {
|
|
23
|
-
throw createError({
|
|
24
|
-
statusCode: 401,
|
|
25
|
-
message: 'Unauthorized',
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
}
|
|
10
|
+
const isAdmin = await ensureResourceAccess(event, model, 'create')
|
|
29
11
|
|
|
30
12
|
const table = getTableForModel(model)
|
|
31
13
|
|
|
@@ -34,10 +16,5 @@ export default eventHandler(async (event) => {
|
|
|
34
16
|
|
|
35
17
|
const newRecord = await useDrizzle().insert(table).values(payload).returning().get()
|
|
36
18
|
|
|
37
|
-
|
|
38
|
-
return filterHiddenFields(model, newRecord as Record<string, unknown>)
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
return filterPublicColumns(model, newRecord as Record<string, unknown>)
|
|
42
|
-
}
|
|
19
|
+
return formatResourceResult(model, newRecord as Record<string, unknown>, isAdmin)
|
|
43
20
|
})
|
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
import { eventHandler
|
|
2
|
-
|
|
3
|
-
import { requireUserSession } from '#imports'
|
|
1
|
+
import { eventHandler } from 'h3'
|
|
2
|
+
|
|
4
3
|
import { getRelations } from '../utils/schema'
|
|
5
|
-
import {
|
|
6
|
-
import { verifyJwtToken } from '../utils/jwt'
|
|
4
|
+
import { ensureAuthenticated } from '../utils/auth'
|
|
7
5
|
|
|
8
6
|
export default eventHandler(async (event) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (auth?.authentication) {
|
|
12
|
-
let isAuthenticated = false
|
|
13
|
-
if (auth.type === 'jwt' && auth.jwtSecret) {
|
|
14
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
try {
|
|
18
|
-
await requireUserSession(event)
|
|
19
|
-
isAuthenticated = true
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
isAuthenticated = false
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!isAuthenticated) {
|
|
27
|
-
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
7
|
+
await ensureAuthenticated(event)
|
|
31
8
|
return getRelations()
|
|
32
9
|
})
|
|
@@ -1,34 +1,12 @@
|
|
|
1
1
|
import { eventHandler, createError, getRouterParam } from 'h3'
|
|
2
|
-
|
|
3
|
-
import { requireUserSession } from '#imports'
|
|
2
|
+
|
|
4
3
|
import { getSchema } from '../../utils/schema'
|
|
5
|
-
import {
|
|
6
|
-
import { verifyJwtToken } from '../../utils/jwt'
|
|
4
|
+
import { ensureAuthenticated } from '../../utils/auth'
|
|
7
5
|
|
|
8
6
|
export default eventHandler(async (event) => {
|
|
9
|
-
|
|
7
|
+
await ensureAuthenticated(event)
|
|
10
8
|
const tableName = getRouterParam(event, 'table')
|
|
11
9
|
|
|
12
|
-
if (auth?.authentication) {
|
|
13
|
-
let isAuthenticated = false
|
|
14
|
-
if (auth.type === 'jwt' && auth.jwtSecret) {
|
|
15
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
try {
|
|
19
|
-
await requireUserSession(event)
|
|
20
|
-
isAuthenticated = true
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
isAuthenticated = false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!isAuthenticated) {
|
|
28
|
-
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
10
|
if (!tableName) {
|
|
33
11
|
throw createError({ statusCode: 400, message: 'Table name is required' })
|
|
34
12
|
}
|
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
import { eventHandler
|
|
2
|
-
|
|
3
|
-
import { requireUserSession } from '#imports'
|
|
1
|
+
import { eventHandler } from 'h3'
|
|
2
|
+
|
|
4
3
|
import { getAllSchemas } from '../../utils/schema'
|
|
5
|
-
import {
|
|
6
|
-
import { verifyJwtToken } from '../../utils/jwt'
|
|
4
|
+
import { ensureAuthenticated } from '../../utils/auth'
|
|
7
5
|
|
|
8
6
|
export default eventHandler(async (event) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (auth?.authentication) {
|
|
12
|
-
let isAuthenticated = false
|
|
13
|
-
if (auth.type === 'jwt' && auth.jwtSecret) {
|
|
14
|
-
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
try {
|
|
18
|
-
await requireUserSession(event)
|
|
19
|
-
isAuthenticated = true
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
isAuthenticated = false
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!isAuthenticated) {
|
|
27
|
-
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
7
|
+
await ensureAuthenticated(event)
|
|
31
8
|
return getAllSchemas()
|
|
32
9
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import '../../auth.d.ts'
|
|
1
2
|
import type { H3Event } from 'h3'
|
|
2
3
|
import { createError } from 'h3'
|
|
4
|
+
import globalAbility from '#site/ability'
|
|
3
5
|
// @ts-expect-error - #imports is available in runtime
|
|
4
|
-
import { requireUserSession } from '#imports'
|
|
6
|
+
import { requireUserSession, allows } from '#imports'
|
|
5
7
|
import { useAutoCrudConfig } from './config'
|
|
6
8
|
import { verifyJwtToken } from './jwt'
|
|
7
9
|
|
|
@@ -26,14 +28,12 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
26
28
|
|
|
27
29
|
// Check authorization if enabled
|
|
28
30
|
if (auth.authorization) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
})
|
|
36
|
-
}
|
|
31
|
+
const allowed = await allows(event, globalAbility, model, action)
|
|
32
|
+
if (!allowed) {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 403,
|
|
35
|
+
message: 'Forbidden',
|
|
36
|
+
})
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -49,3 +49,29 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
49
49
|
return false
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export async function ensureAuthenticated(event: H3Event): Promise<void> {
|
|
54
|
+
const { auth } = useAutoCrudConfig()
|
|
55
|
+
|
|
56
|
+
if (!auth?.authentication) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let isAuthenticated = false
|
|
61
|
+
if (auth.type === 'jwt' && auth.jwtSecret) {
|
|
62
|
+
isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
try {
|
|
66
|
+
await requireUserSession(event)
|
|
67
|
+
isAuthenticated = true
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
isAuthenticated = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isAuthenticated) {
|
|
75
|
+
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createError } from 'h3'
|
|
2
|
+
import type { H3Event } from 'h3'
|
|
3
|
+
import { useAutoCrudConfig } from './config'
|
|
4
|
+
import { checkAdminAccess } from './auth'
|
|
5
|
+
import { filterHiddenFields, filterPublicColumns } from './modelMapper'
|
|
6
|
+
|
|
7
|
+
export async function ensureResourceAccess(event: H3Event, model: string, action: string): Promise<boolean> {
|
|
8
|
+
const { resources } = useAutoCrudConfig()
|
|
9
|
+
const isAdmin = await checkAdminAccess(event, model, action)
|
|
10
|
+
|
|
11
|
+
// Check public access if not admin
|
|
12
|
+
if (!isAdmin) {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const resourceConfig = (resources as any)?.[model]
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && (resourceConfig.public as any[]).includes(action))
|
|
17
|
+
|
|
18
|
+
if (!isPublic) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 401,
|
|
21
|
+
message: 'Unauthorized',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return isAdmin
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatResourceResult(model: string, data: Record<string, unknown>, isAdmin: boolean) {
|
|
30
|
+
if (isAdmin) {
|
|
31
|
+
return filterHiddenFields(model, data)
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return filterPublicColumns(model, data)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -11,30 +11,8 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
11
11
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
12
|
const column = col as any
|
|
13
13
|
const isRequired = column.notNull
|
|
14
|
-
let type = 'string'
|
|
15
|
-
let selectOptions: string[] | undefined = undefined
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
const enumValues = column.enumValues || column.config?.enumValues
|
|
19
|
-
|
|
20
|
-
if (enumValues) {
|
|
21
|
-
type = 'enum'
|
|
22
|
-
selectOptions = enumValues
|
|
23
|
-
}
|
|
24
|
-
// Map Drizzle types to frontend types
|
|
25
|
-
else if (column.dataType === 'number' || column.columnType === 'SQLiteInteger' || column.columnType === 'SQLiteReal') {
|
|
26
|
-
type = 'number'
|
|
27
|
-
// Check if it is a timestamp
|
|
28
|
-
if (column.name.endsWith('_at') || column.name.endsWith('At')) {
|
|
29
|
-
type = 'date'
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
else if (column.dataType === 'boolean') {
|
|
33
|
-
type = 'boolean'
|
|
34
|
-
}
|
|
35
|
-
else if (column.dataType === 'date' || (column.dataType === 'string' && (column.name.endsWith('_at') || column.name.endsWith('At')))) {
|
|
36
|
-
type = 'date'
|
|
37
|
-
}
|
|
15
|
+
const { type, selectOptions } = mapColumnType(column)
|
|
38
16
|
|
|
39
17
|
fields.push({
|
|
40
18
|
name: key,
|
|
@@ -50,6 +28,34 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
50
28
|
}
|
|
51
29
|
}
|
|
52
30
|
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
function mapColumnType(column: any): { type: string, selectOptions?: string[] } {
|
|
33
|
+
// Check for enum values (Drizzle stores them in enumValues or config.enumValues)
|
|
34
|
+
const enumValues = column.enumValues || column.config?.enumValues
|
|
35
|
+
|
|
36
|
+
if (enumValues) {
|
|
37
|
+
return { type: 'enum', selectOptions: enumValues }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (column.dataType === 'boolean') {
|
|
41
|
+
return { type: 'boolean' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (column.dataType === 'date' || (column.dataType === 'string' && (column.name.endsWith('_at') || column.name.endsWith('At')))) {
|
|
45
|
+
return { type: 'date' }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (column.dataType === 'number' || column.columnType === 'SQLiteInteger' || column.columnType === 'SQLiteReal') {
|
|
49
|
+
// Check if it is a timestamp
|
|
50
|
+
if (column.name.endsWith('_at') || column.name.endsWith('At')) {
|
|
51
|
+
return { type: 'date' }
|
|
52
|
+
}
|
|
53
|
+
return { type: 'number' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { type: 'string' }
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
export async function getRelations() {
|
|
54
60
|
const relations: Record<string, Record<string, string>> = {}
|
|
55
61
|
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
|
-
import { useDrizzle } from "#site/drizzle";
|
|
3
|
-
import * as tables from "#site/schema";
|
|
4
|
-
import { useAutoCrudConfig } from "../utils/config.js";
|
|
5
|
-
import { defineNitroPlugin, hashPassword, onHubReady } from "#imports";
|
|
6
|
-
export default defineNitroPlugin(async () => {
|
|
7
|
-
onHubReady(async () => {
|
|
8
|
-
const { auth } = useAutoCrudConfig();
|
|
9
|
-
if (!auth?.authentication || !tables.users) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
const db = useDrizzle();
|
|
13
|
-
const existingAdmin = await db.select().from(tables.users).where(eq(tables.users.email, "admin@example.com")).get();
|
|
14
|
-
if (!existingAdmin) {
|
|
15
|
-
console.log("Seeding admin user...");
|
|
16
|
-
const hashedPassword = await hashPassword("$1Password");
|
|
17
|
-
await db.insert(tables.users).values({
|
|
18
|
-
email: "admin@example.com",
|
|
19
|
-
password: hashedPassword,
|
|
20
|
-
name: "Admin User",
|
|
21
|
-
avatar: "https://i.pravatar.cc/150?u=admin",
|
|
22
|
-
role: "admin",
|
|
23
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
24
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
25
|
-
});
|
|
26
|
-
console.log("Admin user seeded.");
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { eq } from 'drizzle-orm'
|
|
2
|
-
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
3
|
-
import { useDrizzle } from '#site/drizzle'
|
|
4
|
-
// @ts-expect-error - #site/schema is an alias defined by the module
|
|
5
|
-
import * as tables from '#site/schema'
|
|
6
|
-
import { useAutoCrudConfig } from '../utils/config'
|
|
7
|
-
// @ts-expect-error - #imports is available in runtime
|
|
8
|
-
import { defineNitroPlugin, hashPassword, onHubReady } from '#imports'
|
|
9
|
-
|
|
10
|
-
export default defineNitroPlugin(async () => {
|
|
11
|
-
onHubReady(async () => {
|
|
12
|
-
const { auth } = useAutoCrudConfig()
|
|
13
|
-
|
|
14
|
-
// Only seed if auth is enabled and we have a users table
|
|
15
|
-
if (!auth?.authentication || !tables.users) {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const db = useDrizzle()
|
|
20
|
-
|
|
21
|
-
// Check if admin exists
|
|
22
|
-
const existingAdmin = await db.select().from(tables.users).where(eq(tables.users.email, 'admin@example.com')).get()
|
|
23
|
-
|
|
24
|
-
if (!existingAdmin) {
|
|
25
|
-
console.log('Seeding admin user...')
|
|
26
|
-
|
|
27
|
-
const hashedPassword = await hashPassword('$1Password')
|
|
28
|
-
|
|
29
|
-
await db.insert(tables.users).values({
|
|
30
|
-
email: 'admin@example.com',
|
|
31
|
-
password: hashedPassword,
|
|
32
|
-
name: 'Admin User',
|
|
33
|
-
avatar: 'https://i.pravatar.cc/150?u=admin',
|
|
34
|
-
role: 'admin',
|
|
35
|
-
createdAt: new Date(),
|
|
36
|
-
updatedAt: new Date(),
|
|
37
|
-
})
|
|
38
|
-
console.log('Admin user seeded.')
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
})
|