nuxt-auto-crud 1.22.1 → 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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.22.1",
4
+ "version": "1.23.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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 checkAdminAccess(event, model, "list_all");
18
- if (!canListAll) {
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 { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
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
- const isAdmin = await ensureResourceAccess(event, model, "list");
11
+ let canListAny = false;
11
12
  let canListAll = false;
12
- try {
13
- canListAll = await checkAdminAccess(event, model, "list_all");
14
- } catch {
15
- canListAll = false;
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
- if (!canListAll && "status" in columns) {
21
- query = query.where(eq(table.status, "active"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.22.1",
3
+ "version": "1.23.1",
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",
@@ -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 checkAdminAccess(event, model, 'list_all')
32
- if (!canListAll) {
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 { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
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
- const isAdmin = await ensureResourceAccess(event, model, 'list')
16
-
18
+ let canListAny = false
17
19
  let canListAll = false
18
- try {
19
- canListAll = await checkAdminAccess(event, model, 'list_all')
20
- }
21
- catch {
22
- canListAll = false
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
- // Filter active rows for non-admins (or those without list_all) if status field exists
44
+ // 2. Build Filters
45
+ const filters = []
31
46
 
32
- if (!canListAll && 'status' in columns) {
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- // Check if table has columns before assigning (optional but safer if we had strict types)
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