nuxt-auto-crud 1.22.0 โ 1.23.1
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 +15 -135
- package/dist/module.json +1 -1
- package/dist/runtime/server/api/[model]/[id].get.js +6 -3
- package/dist/runtime/server/api/[model]/[id].patch.js +7 -0
- package/dist/runtime/server/api/[model]/index.get.js +43 -9
- package/dist/runtime/server/api/[model]/index.post.js +7 -0
- package/dist/runtime/server/utils/auth.js +1 -1
- package/package.json +1 -1
- package/src/runtime/server/api/[model]/[id].get.ts +7 -4
- package/src/runtime/server/api/[model]/[id].patch.ts +10 -0
- package/src/runtime/server/api/[model]/index.get.ts +66 -13
- package/src/runtime/server/api/[model]/index.post.ts +11 -3
- package/src/runtime/server/utils/auth.ts +2 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nuxt Auto CRUD
|
|
2
2
|
|
|
3
|
-
> **Note:** This module is
|
|
3
|
+
> **Note:** This module is production-ready and actively maintained. The core CRUD API is stable. Minor breaking changes may occasionally occur in advanced features or configuration. We recommend version pinning for production deployments.
|
|
4
4
|
|
|
5
5
|
Auto-expose RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
|
|
6
6
|
|
|
@@ -56,20 +56,13 @@ If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
|
|
|
56
56
|
#### Install dependencies
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
# Install module and required dependencies
|
|
60
59
|
npm install nuxt-auto-crud @nuxthub/core@^0.10.0 drizzle-orm
|
|
61
|
-
|
|
62
|
-
# Optional: Install auth dependencies if using Session Auth (Recommended)
|
|
63
|
-
npm install nuxt-auth-utils nuxt-authorization
|
|
64
|
-
|
|
60
|
+
npm install nuxt-auth-utils nuxt-authorization # Optional: for authentication
|
|
65
61
|
npm install --save-dev wrangler drizzle-kit
|
|
66
|
-
|
|
67
|
-
# Or using bun
|
|
68
|
-
bun add nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
69
|
-
bun add nuxt-auth-utils nuxt-authorization
|
|
70
|
-
bun add --dev wrangler drizzle-kit
|
|
71
62
|
```
|
|
72
63
|
|
|
64
|
+
> You can also use `bun` or `pnpm` instead of `npm`.
|
|
65
|
+
|
|
73
66
|
#### Configure Nuxt
|
|
74
67
|
|
|
75
68
|
Add the modules to your `nuxt.config.ts`:
|
|
@@ -227,81 +220,18 @@ export default defineConfig({
|
|
|
227
220
|
})
|
|
228
221
|
```
|
|
229
222
|
|
|
230
|
-
## ๐ Authentication
|
|
223
|
+
## ๐ Authentication
|
|
231
224
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
### Session Auth (Default)
|
|
235
|
-
|
|
236
|
-
Requires `nuxt-auth-utils` and `nuxt-authorization` to be installed in your project.
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
npm install nuxt-auth-utils nuxt-authorization
|
|
240
|
-
```
|
|
225
|
+
Authentication is enabled by default using **Session Auth** (requires `nuxt-auth-utils` and `nuxt-authorization`).
|
|
241
226
|
|
|
227
|
+
To disable auth for testing:
|
|
242
228
|
```typescript
|
|
243
|
-
|
|
244
|
-
modules: [
|
|
245
|
-
'nuxt-auth-utils',
|
|
246
|
-
'nuxt-authorization',
|
|
247
|
-
'nuxt-auto-crud'
|
|
248
|
-
],
|
|
249
|
-
autoCrud: {
|
|
250
|
-
auth: {
|
|
251
|
-
type: 'session',
|
|
252
|
-
authentication: true, // Enables requireUserSession() check
|
|
253
|
-
authorization: true // Enables authorize(model, action) check
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
})
|
|
229
|
+
autoCrud: { auth: false }
|
|
257
230
|
```
|
|
258
231
|
|
|
259
|
-
|
|
232
|
+
For **JWT Auth** (backend-only apps) or advanced configuration, see the [Authentication docs](https://auto-crud.clifland.in/docs/configuration/authentication).
|
|
260
233
|
|
|
261
|
-
Useful for backend-only apps. Does **not** require `nuxt-auth-utils`.
|
|
262
234
|
|
|
263
|
-
```typescript
|
|
264
|
-
export default defineNuxtConfig({
|
|
265
|
-
autoCrud: {
|
|
266
|
-
auth: {
|
|
267
|
-
type: 'jwt',
|
|
268
|
-
authentication: true,
|
|
269
|
-
jwtSecret: process.env.JWT_SECRET,
|
|
270
|
-
authorization: true
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Disabling Auth
|
|
277
|
-
|
|
278
|
-
You can disable authentication entirely for testing or public APIs.
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
export default defineNuxtConfig({
|
|
282
|
-
autoCrud: {
|
|
283
|
-
auth: {
|
|
284
|
-
authentication: false,
|
|
285
|
-
authorization: false
|
|
286
|
-
}
|
|
287
|
-
// OR simply:
|
|
288
|
-
// auth: false
|
|
289
|
-
}
|
|
290
|
-
})
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## ๐งช Testing
|
|
294
|
-
|
|
295
|
-
This module is tested using **Vitest**.
|
|
296
|
-
|
|
297
|
-
- **Unit Tests:** We cover utility functions and helpers.
|
|
298
|
-
- **E2E Tests:** We verify the API endpoints using a real Nuxt server instance.
|
|
299
|
-
|
|
300
|
-
To run the tests locally:
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
npm run test
|
|
304
|
-
```
|
|
305
235
|
|
|
306
236
|
## ๐ก๏ธ Public View Configuration (Field Visibility)
|
|
307
237
|
|
|
@@ -415,51 +345,7 @@ await $fetch("/api/users/1", {
|
|
|
415
345
|
> - **Fullstack App:** The module integrates with `nuxt-auth-utils`, so session cookies are handled automatically.
|
|
416
346
|
> - **Backend-only App:** You must include the `Authorization: Bearer <token>` header in your requests.
|
|
417
347
|
|
|
418
|
-
## Configuration
|
|
419
|
-
|
|
420
|
-
### Module Options
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
|
|
424
|
-
export default defineNuxtConfig({
|
|
425
|
-
autoCrud: {
|
|
426
|
-
// Path to your database schema file (relative to project root)
|
|
427
|
-
schemaPath: "server/db/schema", // default
|
|
428
|
-
|
|
429
|
-
// Authentication configuration (see "Authentication Configuration" section)
|
|
430
|
-
auth: {
|
|
431
|
-
// ...
|
|
432
|
-
},
|
|
433
|
-
|
|
434
|
-
// Public Guest View Configuration (Field Visibility)
|
|
435
|
-
resources: {
|
|
436
|
-
users: ['id', 'name', 'avatar'],
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
### Protected Fields
|
|
443
348
|
|
|
444
|
-
By default, the following fields are protected from updates:
|
|
445
|
-
|
|
446
|
-
- `id`
|
|
447
|
-
- `createdAt`
|
|
448
|
-
- `created_at`
|
|
449
|
-
- `updatedAt`
|
|
450
|
-
- `updated_at`
|
|
451
|
-
|
|
452
|
-
You can customize updatable fields in your schema by modifying the `modelMapper.ts` utility.
|
|
453
|
-
|
|
454
|
-
### Hidden Fields
|
|
455
|
-
|
|
456
|
-
By default, the following fields are hidden from API responses for security:
|
|
457
|
-
|
|
458
|
-
- `password`
|
|
459
|
-
- `secret`
|
|
460
|
-
- `token`
|
|
461
|
-
|
|
462
|
-
You can customize hidden fields by modifying the `modelMapper.ts` utility.
|
|
463
349
|
|
|
464
350
|
## ๐ง Requirements
|
|
465
351
|
|
|
@@ -467,18 +353,12 @@ You can customize hidden fields by modifying the `modelMapper.ts` utility.
|
|
|
467
353
|
- Drizzle ORM (SQLite)
|
|
468
354
|
- NuxtHub >= 0.10.0
|
|
469
355
|
|
|
470
|
-
## ๐
|
|
471
|
-
|
|
472
|
-
-
|
|
473
|
-
-
|
|
474
|
-
-
|
|
475
|
-
-
|
|
476
|
-
- **YouTube (Add Schemas):** [https://youtu.be/7gW0KW1KtN0](https://youtu.be/7gW0KW1KtN0)
|
|
477
|
-
- **YouTube (Various Permissions):** [https://www.youtube.com/watch?v=Yty3OCYbwOo](https://www.youtube.com/watch?v=Yty3OCYbwOo)
|
|
478
|
-
- **YouTube (Dynamic RBAC):** [https://www.youtube.com/watch?v=W0ju4grRC9M](https://www.youtube.com/watch?v=W0ju4grRC9M)
|
|
479
|
-
- **npm:** [https://www.npmjs.com/package/nuxt-auto-crud](https://www.npmjs.com/package/nuxt-auto-crud)
|
|
480
|
-
- **Github Discussions:** [https://github.com/clifordpereira/nuxt-auto-crud/discussions/1](https://github.com/clifordpereira/nuxt-auto-crud/discussions/1)
|
|
481
|
-
- **Discord:** [https://discord.gg/hGgyEaGu](https://discord.gg/hGgyEaGu)
|
|
356
|
+
## ๐ Links
|
|
357
|
+
|
|
358
|
+
- **๐ Documentation:** [auto-crud.clifland.in](https://auto-crud.clifland.in/docs/auto-crud)
|
|
359
|
+
- **๐ฌ Video Tutorials:** [YouTube Channel](https://www.youtube.com/@ClifordPereira)
|
|
360
|
+
- **๐ฌ Community:** [Discord](https://discord.gg/FBkQQfRFJM) โข [GitHub Discussions](https://github.com/clifordpereira/nuxt-auto-crud/discussions/1)
|
|
361
|
+
- **๐ฆ npm:** [nuxt-auto-crud](https://www.npmjs.com/package/nuxt-auto-crud)
|
|
482
362
|
|
|
483
363
|
## ๐ค Contributing
|
|
484
364
|
|
package/dist/module.json
CHANGED
|
@@ -7,15 +7,18 @@ import { checkAdminAccess } from "../../utils/auth.js";
|
|
|
7
7
|
import { RecordNotFoundError } from "../../exceptions.js";
|
|
8
8
|
export default eventHandler(async (event) => {
|
|
9
9
|
const { model, id } = getRouterParams(event);
|
|
10
|
-
const isAdmin = await ensureResourceAccess(event, model, "read");
|
|
10
|
+
const isAdmin = await ensureResourceAccess(event, model, "read", { id });
|
|
11
11
|
const table = getTableForModel(model);
|
|
12
12
|
const record = await db.select().from(table).where(eq(table.id, Number(id))).get();
|
|
13
13
|
if (!record) {
|
|
14
14
|
throw new RecordNotFoundError();
|
|
15
15
|
}
|
|
16
16
|
if ("status" in record && record.status !== "active") {
|
|
17
|
-
const canListAll = await
|
|
18
|
-
|
|
17
|
+
const [canListAll, canReadOwn] = await Promise.all([
|
|
18
|
+
checkAdminAccess(event, model, "list_all").catch(() => false),
|
|
19
|
+
checkAdminAccess(event, model, "read_own", { id }).catch(() => false)
|
|
20
|
+
]);
|
|
21
|
+
if (!canListAll && !canReadOwn) {
|
|
19
22
|
throw new RecordNotFoundError();
|
|
20
23
|
}
|
|
21
24
|
}
|
|
@@ -11,6 +11,13 @@ export default eventHandler(async (event) => {
|
|
|
11
11
|
const table = getTableForModel(model);
|
|
12
12
|
const body = await readBody(event);
|
|
13
13
|
const payload = filterUpdatableFields(model, body);
|
|
14
|
+
if ("status" in payload) {
|
|
15
|
+
const { checkAdminAccess } = await import("../../utils/auth.js");
|
|
16
|
+
const hasStatusPermission = await checkAdminAccess(event, model, "update_status", { id });
|
|
17
|
+
if (!hasStatusPermission) {
|
|
18
|
+
delete payload.status;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
await hashPayloadFields(payload);
|
|
15
22
|
if ("updatedAt" in table) {
|
|
16
23
|
payload.updatedAt = /* @__PURE__ */ new Date();
|
|
@@ -1,25 +1,59 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams } from "h3";
|
|
2
2
|
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
3
3
|
import { db } from "hub:db";
|
|
4
|
-
import { desc, getTableColumns, eq } from "drizzle-orm";
|
|
5
|
-
import {
|
|
4
|
+
import { desc, getTableColumns, eq, and, or } from "drizzle-orm";
|
|
5
|
+
import { formatResourceResult } from "../../utils/handler.js";
|
|
6
|
+
import { getUserSession } from "#imports";
|
|
6
7
|
import { checkAdminAccess } from "../../utils/auth.js";
|
|
7
8
|
export default eventHandler(async (event) => {
|
|
8
9
|
console.log("[GET] Request received", event.path);
|
|
9
10
|
const { model } = getRouterParams(event);
|
|
10
|
-
|
|
11
|
+
let canListAny = false;
|
|
11
12
|
let canListAll = false;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
let canListOwn = false;
|
|
14
|
+
const [anyAccess, allAccess, ownAccess] = await Promise.all([
|
|
15
|
+
checkAdminAccess(event, model, "list").catch(() => false),
|
|
16
|
+
checkAdminAccess(event, model, "list_all").catch(() => false),
|
|
17
|
+
checkAdminAccess(event, model, "list_own").catch(() => false)
|
|
18
|
+
]);
|
|
19
|
+
canListAny = anyAccess;
|
|
20
|
+
canListAll = allAccess;
|
|
21
|
+
canListOwn = ownAccess;
|
|
22
|
+
if (!canListAny && !canListAll && !canListOwn) {
|
|
23
|
+
throw createError({ statusCode: 403, message: "Forbidden" });
|
|
16
24
|
}
|
|
17
25
|
const table = getTableForModel(model);
|
|
18
26
|
const columns = getTableColumns(table);
|
|
27
|
+
const session = await getUserSession(event);
|
|
28
|
+
const userId = session?.user?.id;
|
|
19
29
|
let query = db.select().from(table);
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const filters = [];
|
|
31
|
+
if (canListAll) {
|
|
32
|
+
} else if (canListAny && canListOwn && userId) {
|
|
33
|
+
const ownershipFilter = "createdBy" in columns ? eq(table.createdBy, userId) : "userId" in columns ? eq(table.userId, userId) : null;
|
|
34
|
+
const activeFilter = "status" in columns ? eq(table.status, "active") : null;
|
|
35
|
+
if (ownershipFilter && activeFilter) {
|
|
36
|
+
filters.push(or(activeFilter, ownershipFilter));
|
|
37
|
+
} else if (activeFilter) {
|
|
38
|
+
filters.push(activeFilter);
|
|
39
|
+
} else if (ownershipFilter) {
|
|
40
|
+
filters.push(ownershipFilter);
|
|
41
|
+
}
|
|
42
|
+
} else if (canListAny) {
|
|
43
|
+
if ("status" in columns) {
|
|
44
|
+
filters.push(eq(table.status, "active"));
|
|
45
|
+
}
|
|
46
|
+
} else if (canListOwn && userId) {
|
|
47
|
+
if ("createdBy" in columns) {
|
|
48
|
+
filters.push(eq(table.createdBy, userId));
|
|
49
|
+
} else if ("userId" in columns) {
|
|
50
|
+
filters.push(eq(table.userId, userId));
|
|
51
|
+
}
|
|
22
52
|
}
|
|
53
|
+
if (filters.length > 0) {
|
|
54
|
+
query = query.where(and(...filters));
|
|
55
|
+
}
|
|
56
|
+
const isAdmin = true;
|
|
23
57
|
const results = await query.orderBy(desc(table.id)).all();
|
|
24
58
|
return results.map((item) => formatResourceResult(model, item, isAdmin));
|
|
25
59
|
});
|
|
@@ -9,6 +9,13 @@ export default eventHandler(async (event) => {
|
|
|
9
9
|
const table = getTableForModel(model);
|
|
10
10
|
const body = await readBody(event);
|
|
11
11
|
const payload = filterUpdatableFields(model, body);
|
|
12
|
+
if ("status" in payload) {
|
|
13
|
+
const { checkAdminAccess } = await import("../../utils/auth.js");
|
|
14
|
+
const hasStatusPermission = await checkAdminAccess(event, model, "update_status");
|
|
15
|
+
if (!hasStatusPermission) {
|
|
16
|
+
delete payload.status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
12
19
|
await hashPayloadFields(payload);
|
|
13
20
|
try {
|
|
14
21
|
const session = await getUserSession(event);
|
|
@@ -25,7 +25,7 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
25
25
|
const guestCheck = !user && (typeof abilityLogic === "function" ? abilityLogic : typeof globalAbility === "function" ? globalAbility : null);
|
|
26
26
|
const allowed = guestCheck ? await guestCheck(null, model, action, context) : await allows(event, globalAbility, model, action, context);
|
|
27
27
|
if (!allowed) {
|
|
28
|
-
if (user && (action === "update" || action === "delete") && context && typeof context === "object" && "id" in context) {
|
|
28
|
+
if (user && (action === "read" || action === "update" || action === "delete") && context && typeof context === "object" && "id" in context) {
|
|
29
29
|
const ownAction = `${action}_own`;
|
|
30
30
|
const userPermissions = user.permissions?.[model];
|
|
31
31
|
if (userPermissions && userPermissions.includes(ownAction)) {
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import { RecordNotFoundError } from '../../exceptions'
|
|
|
11
11
|
|
|
12
12
|
export default eventHandler(async (event) => {
|
|
13
13
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
14
|
-
const isAdmin = await ensureResourceAccess(event, model, 'read')
|
|
14
|
+
const isAdmin = await ensureResourceAccess(event, model, 'read', { id })
|
|
15
15
|
|
|
16
16
|
const table = getTableForModel(model) as TableWithId
|
|
17
17
|
|
|
@@ -25,11 +25,14 @@ export default eventHandler(async (event) => {
|
|
|
25
25
|
throw new RecordNotFoundError()
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Filter inactive rows for non-admins (or those without list_all) if status field exists
|
|
28
|
+
// Filter inactive rows for non-admins (or those without list_all or read_own) if status field exists
|
|
29
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
30
|
if ('status' in record && (record as any).status !== 'active') {
|
|
31
|
-
const canListAll = await
|
|
32
|
-
|
|
31
|
+
const [canListAll, canReadOwn] = await Promise.all([
|
|
32
|
+
checkAdminAccess(event, model, 'list_all').catch(() => false),
|
|
33
|
+
checkAdminAccess(event, model, 'read_own', { id }).catch(() => false),
|
|
34
|
+
])
|
|
35
|
+
if (!canListAll && !canReadOwn) {
|
|
33
36
|
throw new RecordNotFoundError()
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// server/api/[model]/[id].patch.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
3
|
import type { H3Event } from 'h3'
|
|
4
|
+
// @ts-expect-error - #imports is a virtual alias
|
|
4
5
|
import { getUserSession } from '#imports'
|
|
5
6
|
import { eq } from 'drizzle-orm'
|
|
6
7
|
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
@@ -21,6 +22,15 @@ export default eventHandler(async (event) => {
|
|
|
21
22
|
const body = await readBody(event)
|
|
22
23
|
const payload = filterUpdatableFields(model, body)
|
|
23
24
|
|
|
25
|
+
// Custom check for status update permission
|
|
26
|
+
if ('status' in payload) {
|
|
27
|
+
const { checkAdminAccess } = await import('../../utils/auth')
|
|
28
|
+
const hasStatusPermission = await checkAdminAccess(event, model, 'update_status', { id })
|
|
29
|
+
if (!hasStatusPermission) {
|
|
30
|
+
delete payload.status
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
// Auto-hash fields based on config (default: ['password'])
|
|
25
35
|
await hashPayloadFields(payload)
|
|
26
36
|
|
|
@@ -1,38 +1,91 @@
|
|
|
1
1
|
// server/api/[model]/index.get.ts
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
3
|
import { eventHandler, getRouterParams } from 'h3'
|
|
3
4
|
import { getTableForModel } from '../../utils/modelMapper'
|
|
4
5
|
// @ts-expect-error - hub:db is a virtual alias
|
|
5
6
|
import { db } from 'hub:db'
|
|
6
|
-
import { desc, getTableColumns, eq } from 'drizzle-orm'
|
|
7
|
+
import { desc, getTableColumns, eq, and, or } from 'drizzle-orm'
|
|
7
8
|
import type { TableWithId } from '../../types'
|
|
8
|
-
import {
|
|
9
|
+
import { formatResourceResult } from '../../utils/handler'
|
|
10
|
+
// @ts-expect-error - #imports is a virtual alias
|
|
11
|
+
import { getUserSession } from '#imports'
|
|
9
12
|
|
|
10
13
|
import { checkAdminAccess } from '../../utils/auth'
|
|
11
14
|
|
|
12
15
|
export default eventHandler(async (event) => {
|
|
13
16
|
console.log('[GET] Request received', event.path)
|
|
14
17
|
const { model } = getRouterParams(event) as { model: string }
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
let canListAny = false
|
|
17
19
|
let canListAll = false
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
let canListOwn = false
|
|
21
|
+
|
|
22
|
+
// 1. Check permissions
|
|
23
|
+
const [anyAccess, allAccess, ownAccess] = await Promise.all([
|
|
24
|
+
checkAdminAccess(event, model, 'list').catch(() => false),
|
|
25
|
+
checkAdminAccess(event, model, 'list_all').catch(() => false),
|
|
26
|
+
checkAdminAccess(event, model, 'list_own').catch(() => false),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
canListAny = anyAccess
|
|
30
|
+
canListAll = allAccess
|
|
31
|
+
canListOwn = ownAccess
|
|
32
|
+
|
|
33
|
+
if (!canListAny && !canListAll && !canListOwn) {
|
|
34
|
+
throw createError({ statusCode: 403, message: 'Forbidden' })
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
const table = getTableForModel(model) as TableWithId
|
|
26
38
|
const columns = getTableColumns(table)
|
|
39
|
+
const session = await (getUserSession as any)(event)
|
|
40
|
+
const userId = session?.user?.id
|
|
27
41
|
|
|
28
42
|
let query = db.select().from(table)
|
|
29
43
|
|
|
30
|
-
//
|
|
44
|
+
// 2. Build Filters
|
|
45
|
+
const filters = []
|
|
31
46
|
|
|
32
|
-
if (
|
|
33
|
-
//
|
|
34
|
-
query = query.where(eq((table as any).status, 'active')) as any
|
|
47
|
+
if (canListAll) {
|
|
48
|
+
// No filters needed for List All
|
|
35
49
|
}
|
|
50
|
+
else if (canListAny && canListOwn && userId) {
|
|
51
|
+
// Can see everyone's ACTIVE records OR OWN records (any status)
|
|
52
|
+
const ownershipFilter = 'createdBy' in columns
|
|
53
|
+
? eq((table as any).createdBy, userId)
|
|
54
|
+
: ('userId' in columns ? eq((table as any).userId, userId) : null)
|
|
55
|
+
|
|
56
|
+
const activeFilter = 'status' in columns ? eq((table as any).status, 'active') : null
|
|
57
|
+
|
|
58
|
+
if (ownershipFilter && activeFilter) {
|
|
59
|
+
filters.push(or(activeFilter, ownershipFilter))
|
|
60
|
+
}
|
|
61
|
+
else if (activeFilter) {
|
|
62
|
+
filters.push(activeFilter)
|
|
63
|
+
}
|
|
64
|
+
else if (ownershipFilter) {
|
|
65
|
+
filters.push(ownershipFilter)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (canListAny) {
|
|
69
|
+
// Only Any: see everyone's ACTIVE records
|
|
70
|
+
if ('status' in columns) {
|
|
71
|
+
filters.push(eq((table as any).status, 'active'))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (canListOwn && userId) {
|
|
75
|
+
// Only Own: see ONLY own records (all statuses)
|
|
76
|
+
if ('createdBy' in columns) {
|
|
77
|
+
filters.push(eq((table as any).createdBy, userId))
|
|
78
|
+
}
|
|
79
|
+
else if ('userId' in columns) {
|
|
80
|
+
filters.push(eq((table as any).userId, userId))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (filters.length > 0) {
|
|
85
|
+
query = query.where(and(...filters)) as any
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isAdmin = true // Result formatting control
|
|
36
89
|
|
|
37
90
|
const results = await query.orderBy(desc(table.id)).all()
|
|
38
91
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// server/api/[model]/index.post.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
3
|
import type { H3Event } from 'h3'
|
|
4
|
+
// @ts-expect-error - #imports is a virtual alias
|
|
4
5
|
import { getUserSession } from '#imports'
|
|
5
6
|
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
6
7
|
// @ts-expect-error - hub:db is a virtual alias
|
|
@@ -16,6 +17,15 @@ export default eventHandler(async (event) => {
|
|
|
16
17
|
const body = await readBody(event)
|
|
17
18
|
const payload = filterUpdatableFields(model, body)
|
|
18
19
|
|
|
20
|
+
// Custom check for status update permission (or just remove it during creation as per requirement)
|
|
21
|
+
if ('status' in payload) {
|
|
22
|
+
const { checkAdminAccess } = await import('../../utils/auth')
|
|
23
|
+
const hasStatusPermission = await checkAdminAccess(event, model, 'update_status')
|
|
24
|
+
if (!hasStatusPermission) {
|
|
25
|
+
delete payload.status
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
// Auto-hash fields based on config (default: ['password'])
|
|
20
30
|
await hashPayloadFields(payload)
|
|
21
31
|
|
|
@@ -23,9 +33,7 @@ export default eventHandler(async (event) => {
|
|
|
23
33
|
try {
|
|
24
34
|
const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number } | null }>)(event)
|
|
25
35
|
if (session?.user?.id) {
|
|
26
|
-
//
|
|
27
|
-
// Since we are passing payload to .values(), extra keys might be ignored or cause error depending on driver
|
|
28
|
-
// Using 'in' table check is good practice
|
|
36
|
+
// Using 'in' table check is good practice to ensure column exists
|
|
29
37
|
if ('createdBy' in table) {
|
|
30
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
39
|
(payload as any).createdBy = session.user.id
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { H3Event } from 'h3'
|
|
4
4
|
import { createError } from 'h3'
|
|
5
5
|
|
|
6
|
+
// @ts-expect-error - #imports is a virtual alias
|
|
6
7
|
import { requireUserSession, allows, getUserSession, abilities as globalAbility, abilityLogic } from '#imports'
|
|
7
8
|
import { useAutoCrudConfig } from './config'
|
|
8
9
|
import { verifyJwtToken } from './jwt'
|
|
@@ -43,7 +44,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
43
44
|
|
|
44
45
|
if (!allowed) {
|
|
45
46
|
// Fallback: Check for "Own Record" permission (e.g. update_own, delete_own)
|
|
46
|
-
if (user && (action === 'update' || action === 'delete') && context && typeof context === 'object' && 'id' in context) {
|
|
47
|
+
if (user && (action === 'read' || action === 'update' || action === 'delete') && context && typeof context === 'object' && 'id' in context) {
|
|
47
48
|
const ownAction = `${action}_own`
|
|
48
49
|
const userPermissions = user.permissions?.[model] as string[] | undefined
|
|
49
50
|
|
|
@@ -64,8 +65,6 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
64
65
|
|
|
65
66
|
// Standard case: Check 'createdBy' or 'userId' column for ownership
|
|
66
67
|
|
|
67
|
-
// const columns = table.columns || {}
|
|
68
|
-
|
|
69
68
|
const hasCreatedBy = 'createdBy' in table
|
|
70
69
|
const hasUserId = 'userId' in table
|
|
71
70
|
|