nuxt-auto-crud 1.22.1 → 1.24.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/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 +44 -10
- 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 +67 -14
- package/src/runtime/server/api/[model]/index.post.ts +11 -3
- package/src/runtime/server/utils/auth.ts +2 -3
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
|
-
import { eventHandler, getRouterParams } from "h3";
|
|
1
|
+
import { eventHandler, getRouterParams, createError } 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
|
-
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { eventHandler, getRouterParams, createError } 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
|
|