nuxt-auto-crud 1.3.0 → 1.4.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 +79 -11
- package/dist/module.d.mts +49 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +24 -1
- package/dist/runtime/server/api/[model]/[id].delete.d.ts +1 -1
- package/dist/runtime/server/api/[model]/[id].delete.js +20 -2
- package/dist/runtime/server/api/[model]/[id].get.d.ts +1 -1
- package/dist/runtime/server/api/[model]/[id].get.js +21 -4
- package/dist/runtime/server/api/[model]/[id].patch.d.ts +1 -1
- package/dist/runtime/server/api/[model]/[id].patch.js +29 -5
- package/dist/runtime/server/api/[model]/index.get.js +25 -4
- package/dist/runtime/server/api/[model]/index.post.d.ts +1 -1
- package/dist/runtime/server/api/[model]/index.post.js +23 -8
- package/dist/runtime/server/utils/auth.d.ts +2 -0
- package/dist/runtime/server/utils/auth.js +39 -0
- package/dist/runtime/server/utils/config.d.ts +2 -0
- package/dist/runtime/server/utils/config.js +4 -0
- package/dist/runtime/server/utils/jwt.d.ts +2 -0
- package/dist/runtime/server/utils/jwt.js +19 -0
- package/dist/runtime/server/utils/modelMapper.d.ts +31 -0
- package/dist/runtime/server/utils/modelMapper.js +38 -0
- package/package.json +17 -7
- package/src/runtime/server/api/[model]/[id].delete.ts +29 -3
- package/src/runtime/server/api/[model]/[id].get.ts +29 -5
- package/src/runtime/server/api/[model]/[id].patch.ts +40 -9
- package/src/runtime/server/api/[model]/index.get.ts +33 -5
- package/src/runtime/server/api/[model]/index.post.ts +32 -15
- package/src/runtime/server/utils/auth.ts +55 -0
- package/src/runtime/server/utils/config.ts +6 -0
- package/src/runtime/server/utils/jwt.ts +23 -0
- package/src/runtime/server/utils/modelMapper.ts +83 -0
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# Nuxt Auto CRUD
|
|
2
2
|
|
|
3
|
-
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
-
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
-
[![License][license-src]][license-href]
|
|
6
|
-
[![Nuxt][nuxt-src]][nuxt-href]
|
|
7
3
|
|
|
8
|
-
|
|
4
|
+
|
|
5
|
+
> **Note:** This module is currently in its alpha stage. However, you can use it to accelerate MVP development. It has not been tested thoroughly enough for production use; only happy-path testing is performed for each release.
|
|
6
|
+
|
|
7
|
+
Auto-generate RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
|
|
9
8
|
|
|
10
9
|
- [✨ Release Notes](/CHANGELOG.md)
|
|
11
10
|
- [🎮 Try the Playground](/playground)
|
|
@@ -27,10 +26,13 @@ Start a new project with everything pre-configured using our template:
|
|
|
27
26
|
```bash
|
|
28
27
|
npx nuxi init -t gh:clifordpereira/nuxt-auto-crud_template <project-name>
|
|
29
28
|
cd <project-name>
|
|
29
|
+
bun install
|
|
30
30
|
bun db:generate
|
|
31
31
|
bun run dev
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Detailed instructions can be found in [https://auto-crud.clifland.in/](https://auto-crud.clifland.in/)
|
|
35
|
+
|
|
34
36
|
### Add User
|
|
35
37
|
Open Nuxt DevTools (bottom-middle icon) > `...` menu > **Database** icon to add users.
|
|
36
38
|
> **Note:** If the users table doesn't appear, restart the server (`Ctrl + C` and `bun run dev`).
|
|
@@ -44,10 +46,16 @@ Visit [http://localhost:3000/api/users](http://localhost:3000/api/users).
|
|
|
44
46
|
|
|
45
47
|
If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
|
|
46
48
|
|
|
49
|
+
> **Note:** These instructions assume you are using NuxtHub. If you are using a custom SQLite setup (e.g. better-sqlite3, Turso), please see [Custom Setup](./custom-setup.md).
|
|
50
|
+
|
|
47
51
|
### 1. Install dependencies
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
# Install module and required dependencies
|
|
55
|
+
npm install nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
56
|
+
npm install --save-dev wrangler drizzle-kit
|
|
57
|
+
|
|
58
|
+
# Or using bun
|
|
51
59
|
bun add nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
52
60
|
bun add --dev wrangler drizzle-kit
|
|
53
61
|
```
|
|
@@ -161,11 +169,11 @@ Want to see it in action? Clone this repo and try the playground:
|
|
|
161
169
|
git clone https://github.com/clifordpereira/nuxt-auto-crud.git
|
|
162
170
|
cd nuxt-auto-crud
|
|
163
171
|
|
|
164
|
-
# Install dependencies
|
|
172
|
+
# Install dependencies (parent folder)
|
|
165
173
|
bun install
|
|
166
174
|
|
|
167
|
-
# Run the playground
|
|
168
|
-
cd playground
|
|
175
|
+
# Run the playground (fullstack with auth)
|
|
176
|
+
cd playground-fullstack
|
|
169
177
|
bun install
|
|
170
178
|
bun db:generate
|
|
171
179
|
bun run dev
|
|
@@ -219,7 +227,57 @@ await $fetch("/api/users/1", {
|
|
|
219
227
|
});
|
|
220
228
|
```
|
|
221
229
|
|
|
222
|
-
##
|
|
230
|
+
## Use Cases
|
|
231
|
+
|
|
232
|
+
### 1. Full-stack App (with Auth)
|
|
233
|
+
|
|
234
|
+
If you are building a full-stack Nuxt application, you can easily integrate `nuxt-auth-utils` and `nuxt-authorization` to secure your auto-generated APIs.
|
|
235
|
+
|
|
236
|
+
First, install the modules:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npx nuxi@latest module add auth-utils
|
|
240
|
+
npm install nuxt-authorization
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Then, configure `nuxt-auto-crud` in your `nuxt.config.ts`:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
export default defineNuxtConfig({
|
|
247
|
+
modules: [
|
|
248
|
+
'nuxt-auto-crud',
|
|
249
|
+
'nuxt-auth-utils'
|
|
250
|
+
],
|
|
251
|
+
autoCrud: {
|
|
252
|
+
auth: {
|
|
253
|
+
enabled: true, // Enables requireUserSession() check
|
|
254
|
+
authorization: true // Enables authorize(model, action) check
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
When `authorization` is enabled, the module will call `authorize(model, action)` where action is one of: `create`, `read`, `update`, `delete`.
|
|
261
|
+
|
|
262
|
+
### 2. Backend-only App (API Mode)
|
|
263
|
+
|
|
264
|
+
If you are using Nuxt as a backend for a separate client application (e.g., mobile app, SPA), you can use this module to quickly generate REST APIs.
|
|
265
|
+
|
|
266
|
+
In this case, you might handle authentication differently (e.g., validating tokens in middleware) or disable the built-in auth checks if you have a global auth middleware.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
export default defineNuxtConfig({
|
|
270
|
+
modules: ['nuxt-auto-crud'],
|
|
271
|
+
autoCrud: {
|
|
272
|
+
auth: {
|
|
273
|
+
enabled: false, // Default
|
|
274
|
+
authorization: false // Default
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
223
281
|
|
|
224
282
|
### Module Options
|
|
225
283
|
|
|
@@ -242,11 +300,21 @@ By default, the following fields are protected from updates:
|
|
|
242
300
|
|
|
243
301
|
You can customize updatable fields in your schema by modifying the `modelMapper.ts` utility.
|
|
244
302
|
|
|
303
|
+
### Hidden Fields
|
|
304
|
+
|
|
305
|
+
By default, the following fields are hidden from API responses for security:
|
|
306
|
+
|
|
307
|
+
- `password`
|
|
308
|
+
- `secret`
|
|
309
|
+
- `token`
|
|
310
|
+
|
|
311
|
+
You can customize hidden fields by modifying the `modelMapper.ts` utility.
|
|
312
|
+
|
|
245
313
|
## 🔧 Requirements
|
|
246
314
|
|
|
247
315
|
- Nuxt 3 or 4
|
|
248
|
-
-
|
|
249
|
-
-
|
|
316
|
+
- Drizzle ORM (SQLite)
|
|
317
|
+
- NuxtHub (Recommended) or [Custom SQLite Setup](./custom-setup.md)
|
|
250
318
|
|
|
251
319
|
## 🤝 Contributing
|
|
252
320
|
|
package/dist/module.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
3
|
interface ModuleOptions {
|
|
4
|
-
/**
|
|
5
4
|
/**
|
|
6
5
|
* Path to the database schema file
|
|
7
6
|
* @default 'server/database/schema'
|
|
@@ -12,6 +11,55 @@ interface ModuleOptions {
|
|
|
12
11
|
* @default 'server/utils/drizzle'
|
|
13
12
|
*/
|
|
14
13
|
drizzlePath?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Authentication configuration
|
|
16
|
+
*/
|
|
17
|
+
auth?: {
|
|
18
|
+
/**
|
|
19
|
+
* Authentication type
|
|
20
|
+
* @default 'session'
|
|
21
|
+
*/
|
|
22
|
+
type?: 'session' | 'jwt';
|
|
23
|
+
/**
|
|
24
|
+
* JWT Secret (required if type is 'jwt')
|
|
25
|
+
*/
|
|
26
|
+
jwtSecret?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Enable authentication checks (requires nuxt-auth-utils for session)
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Enable authorization checks (requires nuxt-authorization)
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
authorization?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Resource-specific configuration
|
|
40
|
+
* Define public access and column visibility
|
|
41
|
+
*/
|
|
42
|
+
resources?: {
|
|
43
|
+
[modelName: string]: {
|
|
44
|
+
/**
|
|
45
|
+
* Actions allowed without authentication
|
|
46
|
+
* true = all actions
|
|
47
|
+
* array = specific actions ('list', 'create', 'read', 'update', 'delete')
|
|
48
|
+
*/
|
|
49
|
+
public?: boolean | ('list' | 'create' | 'read' | 'update' | 'delete')[];
|
|
50
|
+
/**
|
|
51
|
+
* Columns to return for unauthenticated requests
|
|
52
|
+
* If not specified, all columns (except hidden ones) are returned
|
|
53
|
+
*/
|
|
54
|
+
publicColumns?: string[];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
declare module '@nuxt/schema' {
|
|
60
|
+
interface RuntimeConfig {
|
|
61
|
+
autoCrud: ModuleOptions;
|
|
62
|
+
}
|
|
15
63
|
}
|
|
16
64
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
17
65
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const module$1 = defineNuxtModule({
|
|
|
9
9
|
schemaPath: "server/database/schema",
|
|
10
10
|
drizzlePath: "server/utils/drizzle"
|
|
11
11
|
},
|
|
12
|
-
setup(options, nuxt) {
|
|
12
|
+
async setup(options, nuxt) {
|
|
13
13
|
const resolver = createResolver(import.meta.url);
|
|
14
14
|
const schemaPath = resolver.resolve(
|
|
15
15
|
nuxt.options.rootDir,
|
|
@@ -21,6 +21,29 @@ const module$1 = defineNuxtModule({
|
|
|
21
21
|
options.drizzlePath
|
|
22
22
|
);
|
|
23
23
|
nuxt.options.alias["#site/drizzle"] = drizzlePath;
|
|
24
|
+
nuxt.options.alias["#authorization"] = nuxt.options.alias["#authorization"] || "nuxt-authorization/utils";
|
|
25
|
+
const { loadConfig } = await import('c12');
|
|
26
|
+
const { config: externalConfig } = await loadConfig({
|
|
27
|
+
name: "autocrud",
|
|
28
|
+
cwd: nuxt.options.rootDir
|
|
29
|
+
});
|
|
30
|
+
const mergedAuth = {
|
|
31
|
+
...externalConfig?.auth,
|
|
32
|
+
...options.auth
|
|
33
|
+
};
|
|
34
|
+
const mergedResources = {
|
|
35
|
+
...externalConfig?.resources,
|
|
36
|
+
...options.resources
|
|
37
|
+
};
|
|
38
|
+
nuxt.options.runtimeConfig.autoCrud = {
|
|
39
|
+
auth: {
|
|
40
|
+
enabled: mergedAuth.enabled ?? false,
|
|
41
|
+
authorization: mergedAuth.authorization ?? false,
|
|
42
|
+
type: mergedAuth.type ?? "session",
|
|
43
|
+
jwtSecret: mergedAuth.jwtSecret
|
|
44
|
+
},
|
|
45
|
+
resources: mergedResources || {}
|
|
46
|
+
};
|
|
24
47
|
const apiDir = resolver.resolve("./runtime/server/api");
|
|
25
48
|
addServerHandler({
|
|
26
49
|
route: "/api/:model",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
|
|
3
|
+
import { getTableForModel, getModelSingularName, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
+
import { useAutoCrudConfig } from "../../utils/config.js";
|
|
6
|
+
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
7
|
export default eventHandler(async (event) => {
|
|
8
|
+
const { resources } = useAutoCrudConfig();
|
|
6
9
|
const { model, id } = getRouterParams(event);
|
|
10
|
+
const isAdmin = await checkAdminAccess(event, model, "delete");
|
|
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
|
+
}
|
|
7
21
|
const table = getTableForModel(model);
|
|
8
22
|
const singularName = getModelSingularName(model);
|
|
9
23
|
const deletedRecord = await useDrizzle().delete(table).where(eq(table.id, Number(id))).returning().get();
|
|
@@ -13,5 +27,9 @@ export default eventHandler(async (event) => {
|
|
|
13
27
|
message: `${singularName} not found`
|
|
14
28
|
});
|
|
15
29
|
}
|
|
16
|
-
|
|
30
|
+
if (isAdmin) {
|
|
31
|
+
return filterHiddenFields(model, deletedRecord);
|
|
32
|
+
} else {
|
|
33
|
+
return filterPublicColumns(model, deletedRecord);
|
|
34
|
+
}
|
|
17
35
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel,
|
|
3
|
+
import { getTableForModel, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
+
import { useAutoCrudConfig } from "../../utils/config.js";
|
|
6
|
+
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
7
|
export default eventHandler(async (event) => {
|
|
8
|
+
const { resources } = useAutoCrudConfig();
|
|
6
9
|
const { model, id } = getRouterParams(event);
|
|
10
|
+
const isAdmin = await checkAdminAccess(event, model, "read");
|
|
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
|
+
}
|
|
7
21
|
const table = getTableForModel(model);
|
|
8
|
-
const singularName = getModelSingularName(model);
|
|
9
22
|
const record = await useDrizzle().select().from(table).where(eq(table.id, Number(id))).get();
|
|
10
23
|
if (!record) {
|
|
11
24
|
throw createError({
|
|
12
25
|
statusCode: 404,
|
|
13
|
-
message:
|
|
26
|
+
message: "Record not found"
|
|
14
27
|
});
|
|
15
28
|
}
|
|
16
|
-
|
|
29
|
+
if (isAdmin) {
|
|
30
|
+
return filterHiddenFields(model, record);
|
|
31
|
+
} else {
|
|
32
|
+
return filterPublicColumns(model, record);
|
|
33
|
+
}
|
|
17
34
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
1
|
+
import { eventHandler, getRouterParams, readBody, createError } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
|
-
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
3
|
+
import { getTableForModel, filterUpdatableFields, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
|
|
4
4
|
import { useDrizzle } from "#site/drizzle";
|
|
5
|
+
import { useAutoCrudConfig } from "../../utils/config.js";
|
|
6
|
+
import { checkAdminAccess } from "../../utils/auth.js";
|
|
5
7
|
export default eventHandler(async (event) => {
|
|
8
|
+
const { resources } = useAutoCrudConfig();
|
|
6
9
|
const { model, id } = getRouterParams(event);
|
|
10
|
+
const isAdmin = await checkAdminAccess(event, model, "update");
|
|
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
|
+
}
|
|
7
21
|
const table = getTableForModel(model);
|
|
8
22
|
const body = await readBody(event);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
23
|
+
const payload = filterUpdatableFields(model, body);
|
|
24
|
+
const updatedRecord = await useDrizzle().update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
|
|
25
|
+
if (!updatedRecord) {
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 404,
|
|
28
|
+
message: "Record not found"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (isAdmin) {
|
|
32
|
+
return filterHiddenFields(model, updatedRecord);
|
|
33
|
+
} else {
|
|
34
|
+
return filterPublicColumns(model, updatedRecord);
|
|
35
|
+
}
|
|
12
36
|
});
|
|
@@ -1,9 +1,30 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams } from "h3";
|
|
2
|
-
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
1
|
+
import { eventHandler, getRouterParams, createError } from "h3";
|
|
2
|
+
import { getTableForModel, filterHiddenFields, filterPublicColumns } 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";
|
|
4
6
|
export default eventHandler(async (event) => {
|
|
7
|
+
console.log("[GET] Request received", event.path);
|
|
8
|
+
const { resources } = useAutoCrudConfig();
|
|
5
9
|
const { model } = getRouterParams(event);
|
|
10
|
+
const isAdmin = await checkAdminAccess(event, model, "list");
|
|
11
|
+
if (!isAdmin) {
|
|
12
|
+
const resourceConfig = resources?.[model];
|
|
13
|
+
const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("list");
|
|
14
|
+
if (!isPublic) {
|
|
15
|
+
throw createError({
|
|
16
|
+
statusCode: 401,
|
|
17
|
+
message: "Unauthorized"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
const table = getTableForModel(model);
|
|
7
|
-
const
|
|
8
|
-
return
|
|
22
|
+
const results = await useDrizzle().select().from(table).all();
|
|
23
|
+
return results.map((item) => {
|
|
24
|
+
if (isAdmin) {
|
|
25
|
+
return filterHiddenFields(model, item);
|
|
26
|
+
} else {
|
|
27
|
+
return filterPublicColumns(model, item);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
9
30
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
|
-
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
1
|
+
import { eventHandler, getRouterParams, readBody, createError } from "h3";
|
|
2
|
+
import { getTableForModel, filterHiddenFields, filterUpdatableFields, filterPublicColumns } 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";
|
|
4
6
|
export default eventHandler(async (event) => {
|
|
7
|
+
const { resources } = useAutoCrudConfig();
|
|
5
8
|
const { model } = getRouterParams(event);
|
|
9
|
+
const isAdmin = await checkAdminAccess(event, model, "create");
|
|
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
|
+
}
|
|
6
20
|
const table = getTableForModel(model);
|
|
7
21
|
const body = await readBody(event);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
const payload = filterUpdatableFields(model, body);
|
|
23
|
+
const newRecord = await useDrizzle().insert(table).values(payload).returning().get();
|
|
24
|
+
if (isAdmin) {
|
|
25
|
+
return filterHiddenFields(model, newRecord);
|
|
26
|
+
} else {
|
|
27
|
+
return filterPublicColumns(model, newRecord);
|
|
28
|
+
}
|
|
14
29
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
import { useAutoCrudConfig } from "./config.js";
|
|
3
|
+
import { verifyJwtToken } from "./jwt.js";
|
|
4
|
+
export async function checkAdminAccess(event, model, action) {
|
|
5
|
+
const { auth } = useAutoCrudConfig();
|
|
6
|
+
if (!auth?.enabled) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (auth.type === "jwt") {
|
|
10
|
+
if (!auth.jwtSecret) {
|
|
11
|
+
console.warn("JWT Secret is not configured but auth type is jwt");
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return verifyJwtToken(event, auth.jwtSecret);
|
|
15
|
+
}
|
|
16
|
+
if (typeof requireUserSession !== "function") {
|
|
17
|
+
throw new TypeError("requireUserSession is not available");
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
await requireUserSession(event);
|
|
21
|
+
if (auth.authorization) {
|
|
22
|
+
if (event.context.ability) {
|
|
23
|
+
const can = event.context.ability.can(action, model);
|
|
24
|
+
if (!can) {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 403,
|
|
27
|
+
message: "Forbidden"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e.statusCode === 403) {
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jwtVerify } from "jose";
|
|
2
|
+
import { getRequestHeader } from "h3";
|
|
3
|
+
export async function verifyJwtToken(event, secret) {
|
|
4
|
+
const authHeader = getRequestHeader(event, "Authorization");
|
|
5
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const token = authHeader.split(" ")[1];
|
|
9
|
+
if (!token) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
14
|
+
await jwtVerify(token, secretKey);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -9,6 +9,11 @@ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
|
9
9
|
* }
|
|
10
10
|
*/
|
|
11
11
|
export declare const customUpdatableFields: Record<string, string[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Custom hidden fields configuration (optional)
|
|
14
|
+
* Only define here if you want to override the default hidden fields
|
|
15
|
+
*/
|
|
16
|
+
export declare const customHiddenFields: Record<string, string[]>;
|
|
12
17
|
/**
|
|
13
18
|
* Auto-generated model table map
|
|
14
19
|
* Automatically includes all tables from schema
|
|
@@ -53,3 +58,29 @@ export declare function getModelPluralName(modelName: string): string;
|
|
|
53
58
|
* @returns Array of model names
|
|
54
59
|
*/
|
|
55
60
|
export declare function getAvailableModels(): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Gets the hidden fields for a model
|
|
63
|
+
* @param modelName - The name of the model
|
|
64
|
+
* @returns Array of field names that should be hidden
|
|
65
|
+
*/
|
|
66
|
+
export declare function getHiddenFields(modelName: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Gets the public columns for a model
|
|
69
|
+
* @param modelName - The name of the model
|
|
70
|
+
* @returns Array of field names that are public (or undefined if all are public)
|
|
71
|
+
*/
|
|
72
|
+
export declare function getPublicColumns(modelName: string): string[] | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Filters an object to only include public columns (if configured)
|
|
75
|
+
* @param modelName - The name of the model
|
|
76
|
+
* @param data - The data object to filter
|
|
77
|
+
* @returns Filtered object
|
|
78
|
+
*/
|
|
79
|
+
export declare function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* Filters an object to exclude hidden fields
|
|
82
|
+
* @param modelName - The name of the model
|
|
83
|
+
* @param data - The data object to filter
|
|
84
|
+
* @returns Filtered object without hidden fields
|
|
85
|
+
*/
|
|
86
|
+
export declare function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -3,11 +3,16 @@ import pluralize from "pluralize";
|
|
|
3
3
|
import { pascalCase } from "scule";
|
|
4
4
|
import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
|
|
5
5
|
import { createError } from "h3";
|
|
6
|
+
import { useRuntimeConfig } from "#imports";
|
|
6
7
|
const PROTECTED_FIELDS = ["id", "createdAt", "created_at"];
|
|
8
|
+
const HIDDEN_FIELDS = ["password", "secret", "token"];
|
|
7
9
|
export const customUpdatableFields = {
|
|
8
10
|
// Add custom field restrictions here if needed
|
|
9
11
|
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
10
12
|
};
|
|
13
|
+
export const customHiddenFields = {
|
|
14
|
+
// Add custom hidden fields here if needed
|
|
15
|
+
};
|
|
11
16
|
function buildModelTableMap() {
|
|
12
17
|
const tableMap = {};
|
|
13
18
|
for (const [key, value] of Object.entries(schema)) {
|
|
@@ -72,3 +77,36 @@ export function getModelPluralName(modelName) {
|
|
|
72
77
|
export function getAvailableModels() {
|
|
73
78
|
return Object.keys(modelTableMap);
|
|
74
79
|
}
|
|
80
|
+
export function getHiddenFields(modelName) {
|
|
81
|
+
if (customHiddenFields[modelName]) {
|
|
82
|
+
return customHiddenFields[modelName];
|
|
83
|
+
}
|
|
84
|
+
return HIDDEN_FIELDS;
|
|
85
|
+
}
|
|
86
|
+
export function getPublicColumns(modelName) {
|
|
87
|
+
const { resources } = useRuntimeConfig().autoCrud;
|
|
88
|
+
return resources?.[modelName]?.publicColumns;
|
|
89
|
+
}
|
|
90
|
+
export function filterPublicColumns(modelName, data) {
|
|
91
|
+
const publicColumns = getPublicColumns(modelName);
|
|
92
|
+
if (!publicColumns) {
|
|
93
|
+
return filterHiddenFields(modelName, data);
|
|
94
|
+
}
|
|
95
|
+
const filtered = {};
|
|
96
|
+
for (const [key, value] of Object.entries(data)) {
|
|
97
|
+
if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
|
|
98
|
+
filtered[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return filtered;
|
|
102
|
+
}
|
|
103
|
+
export function filterHiddenFields(modelName, data) {
|
|
104
|
+
const hiddenFields = getHiddenFields(modelName);
|
|
105
|
+
const filtered = {};
|
|
106
|
+
for (const [key, value] of Object.entries(data)) {
|
|
107
|
+
if (!hiddenFields.includes(key)) {
|
|
108
|
+
filtered[key] = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return filtered;
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auto-crud",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Exposes RESTful CRUD APIs for your Nuxt app based solely on your database migrations.",
|
|
5
5
|
"author": "Cliford Pereira",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,21 +35,24 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prepack": "nuxt-module-build build",
|
|
38
|
-
"dev": "npm run dev:prepare && nuxi dev playground",
|
|
39
|
-
"dev:build": "nuxi build playground",
|
|
40
|
-
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
38
|
+
"dev": "npm run dev:prepare && nuxi dev playground-fullstack",
|
|
39
|
+
"dev:build": "nuxi build playground-fullstack",
|
|
40
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground-fullstack",
|
|
41
41
|
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
42
42
|
"lint": "eslint .",
|
|
43
43
|
"test": "vitest run",
|
|
44
|
+
"test:api": "node scripts/test-api.mjs",
|
|
44
45
|
"test:watch": "vitest watch",
|
|
45
|
-
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
46
|
+
"test:types": "vue-tsc --noEmit && cd playground-fullstack && vue-tsc --noEmit",
|
|
46
47
|
"link": "npm link"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@nuxt/kit": "^4.2.1",
|
|
50
51
|
"@types/pluralize": "^0.0.33",
|
|
51
52
|
"pluralize": "^8.0.0",
|
|
52
|
-
"scule": "^1.0.0"
|
|
53
|
+
"scule": "^1.0.0",
|
|
54
|
+
"c12": "^2.0.1",
|
|
55
|
+
"jose": "^5.9.6"
|
|
53
56
|
},
|
|
54
57
|
"peerDependencies": {
|
|
55
58
|
"drizzle-orm": "^0.30.0"
|
|
@@ -60,12 +63,19 @@
|
|
|
60
63
|
"@nuxt/module-builder": "^1.0.2",
|
|
61
64
|
"@nuxt/schema": "^4.2.1",
|
|
62
65
|
"@nuxt/test-utils": "^3.20.1",
|
|
66
|
+
"@nuxthub/core": "^0.9.1",
|
|
63
67
|
"@types/node": "latest",
|
|
68
|
+
"better-sqlite3": "^12.4.6",
|
|
64
69
|
"changelogen": "^0.6.2",
|
|
70
|
+
"drizzle-kit": "^0.31.7",
|
|
71
|
+
"drizzle-orm": "^0.38.3",
|
|
65
72
|
"eslint": "^9.39.1",
|
|
66
73
|
"nuxt": "^4.2.1",
|
|
74
|
+
"nuxt-auth-utils": "^0.5.25",
|
|
75
|
+
"nuxt-authorization": "^0.3.5",
|
|
67
76
|
"typescript": "~5.9.3",
|
|
68
77
|
"vitest": "^4.0.13",
|
|
69
|
-
"vue-tsc": "^3.1.5"
|
|
78
|
+
"vue-tsc": "^3.1.5",
|
|
79
|
+
"wrangler": "^4.51.0"
|
|
70
80
|
}
|
|
71
81
|
}
|
|
@@ -1,15 +1,36 @@
|
|
|
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 } from '../../utils/modelMapper'
|
|
4
|
+
import { getTableForModel, getModelSingularName, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
|
|
5
5
|
|
|
6
6
|
import type { TableWithId } from '../../types'
|
|
7
7
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
8
|
import { useDrizzle } from '#site/drizzle'
|
|
9
9
|
|
|
10
|
+
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
+
import { checkAdminAccess } from '../../utils/auth'
|
|
12
|
+
|
|
10
13
|
export default eventHandler(async (event) => {
|
|
11
|
-
const {
|
|
14
|
+
const { resources } = useAutoCrudConfig()
|
|
15
|
+
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
|
+
}
|
|
31
|
+
|
|
12
32
|
const table = getTableForModel(model) as TableWithId
|
|
33
|
+
|
|
13
34
|
const singularName = getModelSingularName(model)
|
|
14
35
|
|
|
15
36
|
const deletedRecord = await useDrizzle()
|
|
@@ -25,5 +46,10 @@ export default eventHandler(async (event) => {
|
|
|
25
46
|
})
|
|
26
47
|
}
|
|
27
48
|
|
|
28
|
-
|
|
49
|
+
if (isAdmin) {
|
|
50
|
+
return filterHiddenFields(model, deletedRecord as Record<string, unknown>)
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
return filterPublicColumns(model, deletedRecord as Record<string, unknown>)
|
|
54
|
+
}
|
|
29
55
|
})
|
|
@@ -1,16 +1,35 @@
|
|
|
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,
|
|
4
|
+
import { getTableForModel, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
|
|
5
5
|
|
|
6
6
|
import type { TableWithId } from '../../types'
|
|
7
7
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
8
|
import { useDrizzle } from '#site/drizzle'
|
|
9
9
|
|
|
10
|
+
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
+
import { checkAdminAccess } from '../../utils/auth'
|
|
12
|
+
|
|
10
13
|
export default eventHandler(async (event) => {
|
|
11
|
-
const {
|
|
14
|
+
const { resources } = useAutoCrudConfig()
|
|
15
|
+
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
|
+
}
|
|
31
|
+
|
|
12
32
|
const table = getTableForModel(model) as TableWithId
|
|
13
|
-
const singularName = getModelSingularName(model)
|
|
14
33
|
|
|
15
34
|
const record = await useDrizzle()
|
|
16
35
|
.select()
|
|
@@ -21,9 +40,14 @@ export default eventHandler(async (event) => {
|
|
|
21
40
|
if (!record) {
|
|
22
41
|
throw createError({
|
|
23
42
|
statusCode: 404,
|
|
24
|
-
message:
|
|
43
|
+
message: 'Record not found',
|
|
25
44
|
})
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
if (isAdmin) {
|
|
48
|
+
return filterHiddenFields(model, record as Record<string, unknown>)
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return filterPublicColumns(model, record as Record<string, unknown>)
|
|
52
|
+
}
|
|
29
53
|
})
|
|
@@ -1,26 +1,57 @@
|
|
|
1
1
|
// server/api/[model]/[id].patch.ts
|
|
2
|
-
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
2
|
+
import { eventHandler, getRouterParams, readBody, createError } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
|
-
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
4
|
+
import { getTableForModel, filterUpdatableFields, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
|
|
5
5
|
|
|
6
6
|
import type { TableWithId } from '../../types'
|
|
7
7
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
8
8
|
import { useDrizzle } from '#site/drizzle'
|
|
9
9
|
|
|
10
|
+
import { useAutoCrudConfig } from '../../utils/config'
|
|
11
|
+
import { checkAdminAccess } from '../../utils/auth'
|
|
12
|
+
|
|
10
13
|
export default eventHandler(async (event) => {
|
|
11
|
-
const {
|
|
14
|
+
const { resources } = useAutoCrudConfig()
|
|
15
|
+
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
|
+
}
|
|
31
|
+
|
|
12
32
|
const table = getTableForModel(model) as TableWithId
|
|
13
|
-
const body = await readBody(event)
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
const
|
|
34
|
+
const body = await readBody(event)
|
|
35
|
+
const payload = filterUpdatableFields(model, body)
|
|
17
36
|
|
|
18
|
-
const
|
|
37
|
+
const updatedRecord = await useDrizzle()
|
|
19
38
|
.update(table)
|
|
20
|
-
.set(
|
|
39
|
+
.set(payload)
|
|
21
40
|
.where(eq(table.id, Number(id)))
|
|
22
41
|
.returning()
|
|
23
42
|
.get()
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
if (!updatedRecord) {
|
|
45
|
+
throw createError({
|
|
46
|
+
statusCode: 404,
|
|
47
|
+
message: 'Record not found',
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isAdmin) {
|
|
52
|
+
return filterHiddenFields(model, updatedRecord as Record<string, unknown>)
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return filterPublicColumns(model, updatedRecord as Record<string, unknown>)
|
|
56
|
+
}
|
|
26
57
|
})
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
// server/api/[model]/index.get.ts
|
|
2
|
-
import { eventHandler, getRouterParams } from 'h3'
|
|
3
|
-
import { getTableForModel } from '../../utils/modelMapper'
|
|
2
|
+
import { eventHandler, getRouterParams, createError } from 'h3'
|
|
3
|
+
import { getTableForModel, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
|
|
4
4
|
|
|
5
5
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
6
6
|
import { useDrizzle } from '#site/drizzle'
|
|
7
7
|
|
|
8
|
+
import { useAutoCrudConfig } from '../../utils/config'
|
|
9
|
+
import { checkAdminAccess } from '../../utils/auth'
|
|
10
|
+
|
|
8
11
|
export default eventHandler(async (event) => {
|
|
9
|
-
|
|
12
|
+
console.log('[GET] Request received', event.path)
|
|
13
|
+
const { resources } = useAutoCrudConfig()
|
|
14
|
+
const { model } = getRouterParams(event) as { model: string }
|
|
15
|
+
|
|
16
|
+
const isAdmin = await checkAdminAccess(event, model, 'list')
|
|
17
|
+
|
|
18
|
+
// Check public access if not admin
|
|
19
|
+
if (!isAdmin) {
|
|
20
|
+
const resourceConfig = resources?.[model]
|
|
21
|
+
const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('list'))
|
|
22
|
+
|
|
23
|
+
if (!isPublic) {
|
|
24
|
+
throw createError({
|
|
25
|
+
statusCode: 401,
|
|
26
|
+
message: 'Unauthorized',
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
const table = getTableForModel(model)
|
|
11
32
|
|
|
12
|
-
const
|
|
33
|
+
const results = await useDrizzle().select().from(table).all()
|
|
13
34
|
|
|
14
|
-
return
|
|
35
|
+
return results.map((item: Record<string, unknown>) => {
|
|
36
|
+
if (isAdmin) {
|
|
37
|
+
return filterHiddenFields(model, item as Record<string, unknown>)
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return filterPublicColumns(model, item as Record<string, unknown>)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
15
43
|
})
|
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
// server/api/[model]/index.post.ts
|
|
2
|
-
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
|
-
import { getTableForModel } from '../../utils/modelMapper'
|
|
2
|
+
import { eventHandler, getRouterParams, readBody, createError } from 'h3'
|
|
3
|
+
import { getTableForModel, filterHiddenFields, filterUpdatableFields, filterPublicColumns } from '../../utils/modelMapper'
|
|
4
4
|
|
|
5
5
|
// @ts-expect-error - #site/drizzle is an alias defined by the module
|
|
6
6
|
import { useDrizzle } from '#site/drizzle'
|
|
7
7
|
|
|
8
|
+
import { useAutoCrudConfig } from '../../utils/config'
|
|
9
|
+
import { checkAdminAccess } from '../../utils/auth'
|
|
10
|
+
|
|
8
11
|
export default eventHandler(async (event) => {
|
|
9
|
-
const {
|
|
10
|
-
const
|
|
11
|
-
const body = await readBody(event)
|
|
12
|
+
const { resources } = useAutoCrudConfig()
|
|
13
|
+
const { model } = getRouterParams(event) as { model: string }
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
const
|
|
20
|
-
.insert(table)
|
|
21
|
-
.values(values)
|
|
22
|
-
.returning()
|
|
23
|
-
.get()
|
|
30
|
+
const table = getTableForModel(model)
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
const body = await readBody(event)
|
|
33
|
+
const payload = filterUpdatableFields(model, body)
|
|
34
|
+
|
|
35
|
+
const newRecord = await useDrizzle().insert(table).values(payload).returning().get()
|
|
36
|
+
|
|
37
|
+
if (isAdmin) {
|
|
38
|
+
return filterHiddenFields(model, newRecord as Record<string, unknown>)
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return filterPublicColumns(model, newRecord as Record<string, unknown>)
|
|
42
|
+
}
|
|
26
43
|
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { H3Event } from 'h3'
|
|
2
|
+
import { createError } from 'h3'
|
|
3
|
+
import { useAutoCrudConfig } from './config'
|
|
4
|
+
import { verifyJwtToken } from './jwt'
|
|
5
|
+
|
|
6
|
+
export async function checkAdminAccess(event: H3Event, model: string, action: string): Promise<boolean> {
|
|
7
|
+
const { auth } = useAutoCrudConfig()
|
|
8
|
+
|
|
9
|
+
if (!auth?.enabled) {
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (auth.type === 'jwt') {
|
|
14
|
+
if (!auth.jwtSecret) {
|
|
15
|
+
console.warn('JWT Secret is not configured but auth type is jwt')
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
return verifyJwtToken(event, auth.jwtSecret)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Session based (default)
|
|
22
|
+
// @ts-expect-error - requireUserSession is auto-imported
|
|
23
|
+
if (typeof requireUserSession !== 'function') {
|
|
24
|
+
throw new TypeError('requireUserSession is not available')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// @ts-expect-error - requireUserSession is auto-imported
|
|
29
|
+
await requireUserSession(event)
|
|
30
|
+
|
|
31
|
+
// Check authorization if enabled
|
|
32
|
+
if (auth.authorization) {
|
|
33
|
+
if (event.context.ability) {
|
|
34
|
+
const can = event.context.ability.can(action, model)
|
|
35
|
+
if (!can) {
|
|
36
|
+
throw createError({
|
|
37
|
+
statusCode: 403,
|
|
38
|
+
message: 'Forbidden',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
catch (e: unknown) {
|
|
47
|
+
// If it's a 403 (Forbidden) from our ability check, rethrow it
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
if ((e as any).statusCode === 403) {
|
|
50
|
+
throw e
|
|
51
|
+
}
|
|
52
|
+
// Otherwise (401 from requireUserSession), return false (treat as guest)
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jwtVerify } from 'jose'
|
|
2
|
+
import type { H3Event } from 'h3'
|
|
3
|
+
import { getRequestHeader } from 'h3'
|
|
4
|
+
|
|
5
|
+
export async function verifyJwtToken(event: H3Event, secret: string): Promise<boolean> {
|
|
6
|
+
const authHeader = getRequestHeader(event, 'Authorization')
|
|
7
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const token = authHeader.split(' ')[1]
|
|
12
|
+
if (!token) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const secretKey = new TextEncoder().encode(secret)
|
|
17
|
+
await jwtVerify(token, secretKey)
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -6,12 +6,18 @@ import { pascalCase } from 'scule'
|
|
|
6
6
|
import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
|
|
7
7
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
8
8
|
import { createError } from 'h3'
|
|
9
|
+
import { useRuntimeConfig } from '#imports'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Fields that should never be updatable via PATCH requests
|
|
12
13
|
*/
|
|
13
14
|
const PROTECTED_FIELDS = ['id', 'createdAt', 'created_at']
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Fields that should never be returned in API responses
|
|
18
|
+
*/
|
|
19
|
+
const HIDDEN_FIELDS = ['password', 'secret', 'token']
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* Custom updatable fields configuration (optional)
|
|
17
23
|
* Only define here if you want to override the auto-detection
|
|
@@ -26,6 +32,14 @@ export const customUpdatableFields: Record<string, string[]> = {
|
|
|
26
32
|
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Custom hidden fields configuration (optional)
|
|
37
|
+
* Only define here if you want to override the default hidden fields
|
|
38
|
+
*/
|
|
39
|
+
export const customHiddenFields: Record<string, string[]> = {
|
|
40
|
+
// Add custom hidden fields here if needed
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* Automatically builds a map of all exported tables from the schema
|
|
31
45
|
* No manual configuration needed!
|
|
@@ -163,3 +177,72 @@ export function getModelPluralName(modelName: string): string {
|
|
|
163
177
|
export function getAvailableModels(): string[] {
|
|
164
178
|
return Object.keys(modelTableMap)
|
|
165
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Gets the hidden fields for a model
|
|
183
|
+
* @param modelName - The name of the model
|
|
184
|
+
* @returns Array of field names that should be hidden
|
|
185
|
+
*/
|
|
186
|
+
export function getHiddenFields(modelName: string): string[] {
|
|
187
|
+
// Check if custom hidden fields are defined for this model
|
|
188
|
+
if (customHiddenFields[modelName]) {
|
|
189
|
+
return customHiddenFields[modelName]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return HIDDEN_FIELDS
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Gets the public columns for a model
|
|
197
|
+
* @param modelName - The name of the model
|
|
198
|
+
* @returns Array of field names that are public (or undefined if all are public)
|
|
199
|
+
*/
|
|
200
|
+
export function getPublicColumns(modelName: string): string[] | undefined {
|
|
201
|
+
const { resources } = useRuntimeConfig().autoCrud
|
|
202
|
+
return resources?.[modelName]?.publicColumns
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Filters an object to only include public columns (if configured)
|
|
207
|
+
* @param modelName - The name of the model
|
|
208
|
+
* @param data - The data object to filter
|
|
209
|
+
* @returns Filtered object
|
|
210
|
+
*/
|
|
211
|
+
export function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
212
|
+
const publicColumns = getPublicColumns(modelName)
|
|
213
|
+
|
|
214
|
+
// If no public columns configured, return all (except hidden)
|
|
215
|
+
if (!publicColumns) {
|
|
216
|
+
return filterHiddenFields(modelName, data)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const filtered: Record<string, unknown> = {}
|
|
220
|
+
|
|
221
|
+
for (const [key, value] of Object.entries(data)) {
|
|
222
|
+
// Must be in publicColumns AND not in hidden fields (double safety)
|
|
223
|
+
if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
|
|
224
|
+
filtered[key] = value
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return filtered
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Filters an object to exclude hidden fields
|
|
233
|
+
* @param modelName - The name of the model
|
|
234
|
+
* @param data - The data object to filter
|
|
235
|
+
* @returns Filtered object without hidden fields
|
|
236
|
+
*/
|
|
237
|
+
export function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
238
|
+
const hiddenFields = getHiddenFields(modelName)
|
|
239
|
+
const filtered: Record<string, unknown> = {}
|
|
240
|
+
|
|
241
|
+
for (const [key, value] of Object.entries(data)) {
|
|
242
|
+
if (!hiddenFields.includes(key)) {
|
|
243
|
+
filtered[key] = value
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return filtered
|
|
248
|
+
}
|