nuxt-auto-crud 1.13.0 → 1.15.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **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.
4
4
 
5
- Auto-generate RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
5
+ Auto-expose RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
6
6
 
7
7
  **Core Philosophy:**
8
8
  The main objective of this module is to **expose CRUD APIs without the need for writing code**. You define your database schema, and `nuxt-auto-crud` handles the rest.
@@ -44,7 +44,7 @@ bun run dev
44
44
  1. **Fullstack App**: The template includes the `nuxt-auto-crud` module, providing both the backend APIs and the frontend UI. [Watch Demo](https://youtu.be/M9-koXmhB9k)
45
45
  2. **Frontend Only**: You can use the template just for the frontend. In this case, you don't need to install the module in the frontend app. Instead, you would install `nuxt-auto-crud` in a separate backend setup (e.g., another Nuxt project acting as the API).
46
46
 
47
- Detailed instructions can be found in [https://auto-crud.clifland.in/](https://auto-crud.clifland.in/)
47
+ Detailed instructions can be found in [https://auto-crud.clifland.in/docs](https://auto-crud.clifland.in/docs)
48
48
 
49
49
  ### 2. Manual Setup (Existing Project)
50
50
 
@@ -207,7 +207,7 @@ The new API endpoints (e.g., `/api/posts`) will be automatically available. [Wat
207
207
 
208
208
  ### 3. Backend-only App (API Mode)
209
209
 
210
- 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.
210
+ 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 expose REST APIs.
211
211
 
212
212
  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.
213
213
 
@@ -467,12 +467,14 @@ You can customize hidden fields by modifying the `modelMapper.ts` utility.
467
467
  ## 🔗 Other Helpful Links
468
468
 
469
469
  - **Template:** [https://github.com/clifordpereira/nuxt-auto-crud_template](https://github.com/clifordpereira/nuxt-auto-crud_template)
470
- - **Docs:** [https://auto-crud.clifland.in/](https://auto-crud.clifland.in/)
470
+ - **Docs:** [https://auto-crud.clifland.in/docs](https://auto-crud.clifland.in/docs)
471
471
  - **Repo:** [https://github.com/clifordpereira/nuxt-auto-crud](https://github.com/clifordpereira/nuxt-auto-crud)
472
- - **YouTube:** [https://youtu.be/M9-koXmhB9k](https://youtu.be/M9-koXmhB9k)
473
- - **YouTube 2:** [https://youtu.be/7gW0KW1KtN0](https://youtu.be/7gW0KW1KtN0)
472
+ - **YouTube (Installation):** [https://youtu.be/M9-koXmhB9k](https://youtu.be/M9-koXmhB9k)
473
+ - **YouTube (Add Schemas):** [https://youtu.be/7gW0KW1KtN0](https://youtu.be/7gW0KW1KtN0)
474
+ - **YouTube (Various Permissions):** [https://www.youtube.com/watch?v=Yty3OCYbwOo](https://www.youtube.com/watch?v=Yty3OCYbwOo)
474
475
  - **npm:** [https://www.npmjs.com/package/nuxt-auto-crud](https://www.npmjs.com/package/nuxt-auto-crud)
475
- - **Discuss:** [https://discord.gg/hGgyEaGu](https://discord.gg/hGgyEaGu)
476
+ - **Github Discussions:** [https://github.com/clifordpereira/nuxt-auto-crud/discussions/1](https://github.com/clifordpereira/nuxt-auto-crud/discussions/1)
477
+ - **Discord:** [https://discord.gg/hGgyEaGu](https://discord.gg/hGgyEaGu)
476
478
 
477
479
  ## 🤝 Contributing
478
480
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.13.0",
4
+ "version": "1.15.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImportsDir } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImportsDir, addServerHandler, addServerImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -22,52 +22,19 @@ const module$1 = defineNuxtModule({
22
22
  options.drizzlePath
23
23
  );
24
24
  nuxt.options.alias["#site/drizzle"] = drizzlePath;
25
- const abilityPath = resolver.resolve(
26
- nuxt.options.rootDir,
27
- "shared/utils/abilities"
28
- );
29
- nuxt.options.alias["#site/ability"] = abilityPath;
30
- nuxt.options.alias["#authorization"] = nuxt.options.alias["#authorization"] || "nuxt-authorization/utils";
25
+ addImportsDir(resolver.resolve(nuxt.options.rootDir, "shared/utils"));
26
+ nuxt.options.alias["#authorization"] ||= "nuxt-authorization/utils";
31
27
  const { loadConfig } = await import('c12');
32
28
  const { config: externalConfig } = await loadConfig({
33
29
  name: "autocrud",
34
30
  cwd: nuxt.options.rootDir
35
31
  });
36
- let mergedAuth = {
37
- authentication: false,
38
- authorization: false,
39
- type: "session"
40
- };
41
- if (options.auth === true) {
42
- mergedAuth = {
43
- authentication: true,
44
- authorization: true,
45
- type: "session",
46
- ...typeof externalConfig?.auth === "object" ? externalConfig.auth : {}
47
- };
48
- } else if (options.auth === false) {
49
- mergedAuth = {
50
- authentication: false,
51
- authorization: false,
52
- type: "session"
53
- };
54
- } else {
55
- mergedAuth = {
56
- authentication: true,
57
- // Default to true if object provided? Or undefined?
58
- // If options.auth is undefined, we might want defaults.
59
- // But if defaults say auth: false, then options.auth might be undefined.
60
- // Let's stick to the plan: default is false.
61
- ...typeof externalConfig?.auth === "object" ? externalConfig.auth : {},
62
- ...typeof options.auth === "object" ? options.auth : {}
63
- };
64
- if (mergedAuth.authentication === void 0) {
65
- mergedAuth.authentication = false;
66
- }
67
- }
68
- const mergedResources = {
69
- ...externalConfig?.resources,
70
- ...options.resources
32
+ const mergedAuth = options.auth === false ? { authentication: false, authorization: false, type: "session" } : {
33
+ authentication: true,
34
+ authorization: options.auth === true,
35
+ type: "session",
36
+ ...typeof externalConfig?.auth === "object" ? externalConfig.auth : {},
37
+ ...typeof options.auth === "object" ? options.auth : {}
71
38
  };
72
39
  nuxt.options.runtimeConfig.autoCrud = {
73
40
  auth: {
@@ -76,7 +43,10 @@ const module$1 = defineNuxtModule({
76
43
  type: mergedAuth.type ?? "session",
77
44
  jwtSecret: mergedAuth.jwtSecret
78
45
  },
79
- resources: mergedResources || {}
46
+ resources: {
47
+ ...externalConfig?.resources,
48
+ ...options.resources
49
+ }
80
50
  };
81
51
  const apiDir = resolver.resolve("./runtime/server/api");
82
52
  addServerHandler({
@@ -3,6 +3,7 @@ import { eq } from "drizzle-orm";
3
3
  import { getTableForModel } from "../../utils/modelMapper.js";
4
4
  import { useDrizzle } from "#site/drizzle";
5
5
  import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
6
+ import { checkAdminAccess } from "../../utils/auth.js";
6
7
  export default eventHandler(async (event) => {
7
8
  const { model, id } = getRouterParams(event);
8
9
  const isAdmin = await ensureResourceAccess(event, model, "read");
@@ -14,5 +15,14 @@ export default eventHandler(async (event) => {
14
15
  message: "Record not found"
15
16
  });
16
17
  }
18
+ if ("status" in record && record.status !== "active") {
19
+ const canListAll = await checkAdminAccess(event, model, "list_all");
20
+ if (!canListAll) {
21
+ throw createError({
22
+ statusCode: 404,
23
+ message: "Record not found"
24
+ });
25
+ }
26
+ }
17
27
  return formatResourceResult(model, record, isAdmin);
18
28
  });
@@ -1,13 +1,25 @@
1
1
  import { eventHandler, getRouterParams } from "h3";
2
2
  import { getTableForModel } from "../../utils/modelMapper.js";
3
3
  import { useDrizzle } from "#site/drizzle";
4
- import { desc } from "drizzle-orm";
4
+ import { desc, getTableColumns, eq } from "drizzle-orm";
5
5
  import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
6
+ import { checkAdminAccess } from "../../utils/auth.js";
6
7
  export default eventHandler(async (event) => {
7
8
  console.log("[GET] Request received", event.path);
8
9
  const { model } = getRouterParams(event);
9
10
  const isAdmin = await ensureResourceAccess(event, model, "list");
11
+ let canListAll = false;
12
+ try {
13
+ canListAll = await checkAdminAccess(event, model, "list_all");
14
+ } catch {
15
+ canListAll = false;
16
+ }
10
17
  const table = getTableForModel(model);
11
- const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all();
18
+ const columns = getTableColumns(table);
19
+ let query = useDrizzle().select().from(table);
20
+ if (!canListAll && "status" in columns) {
21
+ query = query.where(eq(table.status, "active"));
22
+ }
23
+ const results = await query.orderBy(desc(table.id)).all();
12
24
  return results.map((item) => formatResourceResult(model, item, isAdmin));
13
25
  });
@@ -1,6 +1,5 @@
1
1
  import { createError } from "h3";
2
- import globalAbility from "#site/ability";
3
- import { requireUserSession, allows } from "#imports";
2
+ import { requireUserSession, allows, getUserSession, abilities as globalAbility, abilityLogic } from "#imports";
4
3
  import { useAutoCrudConfig } from "./config.js";
5
4
  import { verifyJwtToken } from "./jwt.js";
6
5
  export async function checkAdminAccess(event, model, action) {
@@ -15,42 +14,39 @@ export async function checkAdminAccess(event, model, action) {
15
14
  }
16
15
  return verifyJwtToken(event, auth.jwtSecret);
17
16
  }
17
+ let user = null;
18
18
  try {
19
- await requireUserSession(event);
20
- if (auth.authorization) {
21
- const allowed = await allows(event, globalAbility, model, action);
19
+ const session = await getUserSession(event);
20
+ user = session.user;
21
+ } catch {
22
+ }
23
+ if (auth.authorization) {
24
+ try {
25
+ const guestCheck = !user && (typeof abilityLogic === "function" ? abilityLogic : typeof globalAbility === "function" ? globalAbility : null);
26
+ const allowed = guestCheck ? await guestCheck(null, model, action) : await allows(event, globalAbility, model, action);
22
27
  if (!allowed) {
23
- throw createError({
24
- statusCode: 403,
25
- message: "Forbidden"
26
- });
28
+ if (user) throw createError({ statusCode: 403, message: "Forbidden" });
29
+ return false;
27
30
  }
31
+ return true;
32
+ } catch (err) {
33
+ if (err.statusCode === 403) throw err;
34
+ return false;
28
35
  }
36
+ }
37
+ if (user) {
29
38
  return true;
30
- } catch (e) {
31
- if (e.statusCode === 403) {
32
- throw e;
33
- }
34
- return false;
35
39
  }
40
+ return false;
36
41
  }
37
42
  export async function ensureAuthenticated(event) {
38
43
  const { auth } = useAutoCrudConfig();
39
- if (!auth?.authentication) {
40
- return;
41
- }
42
- let isAuthenticated = false;
44
+ if (!auth?.authentication) return;
43
45
  if (auth.type === "jwt" && auth.jwtSecret) {
44
- isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
45
- } else {
46
- try {
47
- await requireUserSession(event);
48
- isAuthenticated = true;
49
- } catch {
50
- isAuthenticated = false;
46
+ if (!await verifyJwtToken(event, auth.jwtSecret)) {
47
+ throw createError({ statusCode: 401, message: "Unauthorized" });
51
48
  }
49
+ return;
52
50
  }
53
- if (!isAuthenticated) {
54
- throw createError({ statusCode: 401, message: "Unauthorized" });
55
- }
51
+ await requireUserSession(event);
56
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.13.0",
3
+ "version": "1.15.2",
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",
@@ -61,6 +61,7 @@
61
61
  "@iconify-json/heroicons": "^1.2.3",
62
62
  "@iconify-json/lucide": "^1.2.77",
63
63
  "@iconify-json/simple-icons": "^1.2.61",
64
+ "@iconify-json/vscode-icons": "^1.2.37",
64
65
  "@nuxt/devtools": "^3.1.0",
65
66
  "@nuxt/eslint-config": "^1.10.0",
66
67
  "@nuxt/module-builder": "^1.0.2",
@@ -6,6 +6,7 @@ import type { TableWithId } from '../../types'
6
6
  // @ts-expect-error - #site/drizzle is an alias defined by the module
7
7
  import { useDrizzle } from '#site/drizzle'
8
8
  import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
9
+ import { checkAdminAccess } from '../../utils/auth'
9
10
 
10
11
  export default eventHandler(async (event) => {
11
12
  const { model, id } = getRouterParams(event) as { model: string, id: string }
@@ -26,5 +27,17 @@ export default eventHandler(async (event) => {
26
27
  })
27
28
  }
28
29
 
30
+ // Filter inactive rows for non-admins (or those without list_all) if status field exists
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ if ('status' in record && (record as any).status !== 'active') {
33
+ const canListAll = await checkAdminAccess(event, model, 'list_all')
34
+ if (!canListAll) {
35
+ throw createError({
36
+ statusCode: 404,
37
+ message: 'Record not found',
38
+ })
39
+ }
40
+ }
41
+
29
42
  return formatResourceResult(model, record as Record<string, unknown>, isAdmin)
30
43
  })
@@ -3,18 +3,38 @@ import { eventHandler, getRouterParams } from 'h3'
3
3
  import { getTableForModel } from '../../utils/modelMapper'
4
4
  // @ts-expect-error - #site/drizzle is an alias defined by the module
5
5
  import { useDrizzle } from '#site/drizzle'
6
- import { desc } from 'drizzle-orm'
6
+ import { desc, getTableColumns, eq } from 'drizzle-orm'
7
7
  import type { TableWithId } from '../../types'
8
8
  import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
9
9
 
10
+ import { checkAdminAccess } from '../../utils/auth'
11
+
10
12
  export default eventHandler(async (event) => {
11
13
  console.log('[GET] Request received', event.path)
12
14
  const { model } = getRouterParams(event) as { model: string }
13
15
  const isAdmin = await ensureResourceAccess(event, model, 'list')
14
16
 
17
+ let canListAll = false
18
+ try {
19
+ canListAll = await checkAdminAccess(event, model, 'list_all')
20
+ }
21
+ catch {
22
+ canListAll = false
23
+ }
24
+
15
25
  const table = getTableForModel(model) as TableWithId
26
+ const columns = getTableColumns(table)
27
+
28
+ let query = useDrizzle().select().from(table)
29
+
30
+ // Filter active rows for non-admins (or those without list_all) if status field exists
31
+
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
35
+ }
16
36
 
17
- const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all()
37
+ const results = await query.orderBy(desc(table.id)).all()
18
38
 
19
39
  return results.map((item: Record<string, unknown>) => formatResourceResult(model, item, isAdmin))
20
40
  })
@@ -2,9 +2,8 @@
2
2
  /// <reference path="../../auth.d.ts" />
3
3
  import type { H3Event } from 'h3'
4
4
  import { createError } from 'h3'
5
- import globalAbility from '#site/ability'
6
5
  // @ts-expect-error - #imports is available in runtime
7
- import { requireUserSession, allows } from '#imports'
6
+ import { requireUserSession, allows, getUserSession, abilities as globalAbility, abilityLogic } from '#imports'
8
7
  import { useAutoCrudConfig } from './config'
9
8
  import { verifyJwtToken } from './jwt'
10
9
 
@@ -24,55 +23,55 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
24
23
  }
25
24
 
26
25
  // Session based (default)
26
+ let user = null
27
27
  try {
28
- await requireUserSession(event)
28
+ const session = await getUserSession(event)
29
+ user = session.user
30
+ }
31
+ catch {
32
+ // No session or error fetching session
33
+ }
34
+
35
+ // Check authorization if enabled
36
+ if (auth.authorization) {
37
+ try {
38
+ const guestCheck = !user && (typeof abilityLogic === 'function' ? abilityLogic : (typeof globalAbility === 'function' ? globalAbility : null))
39
+
40
+ const allowed = guestCheck
41
+ ? await guestCheck(null, model, action)
42
+ : await allows(event, globalAbility, model, action)
29
43
 
30
- // Check authorization if enabled
31
- if (auth.authorization) {
32
- const allowed = await allows(event, globalAbility, model, action)
33
44
  if (!allowed) {
34
- throw createError({
35
- statusCode: 403,
36
- message: 'Forbidden',
37
- })
45
+ if (user) throw createError({ statusCode: 403, message: 'Forbidden' })
46
+ return false
38
47
  }
48
+ return true
39
49
  }
50
+ catch (err) {
51
+ if ((err as { statusCode: number }).statusCode === 403) throw err
52
+ return false
53
+ }
54
+ }
40
55
 
56
+ // If authorization is NOT enabled, we rely on authentication only.
57
+ if (user) {
41
58
  return true
42
59
  }
43
- catch (e: unknown) {
44
- // If it's a 403 (Forbidden) from our ability check, rethrow it
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- if ((e as any).statusCode === 403) {
47
- throw e
48
- }
49
- // Otherwise (401 from requireUserSession or function not available), return false (treat as guest)
50
- return false
51
- }
60
+
61
+ return false
52
62
  }
53
63
 
54
64
  export async function ensureAuthenticated(event: H3Event): Promise<void> {
55
65
  const { auth } = useAutoCrudConfig()
56
66
 
57
- if (!auth?.authentication) {
58
- return
59
- }
67
+ if (!auth?.authentication) return
60
68
 
61
- let isAuthenticated = false
62
69
  if (auth.type === 'jwt' && auth.jwtSecret) {
63
- isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
64
- }
65
- else {
66
- try {
67
- await requireUserSession(event)
68
- isAuthenticated = true
69
- }
70
- catch {
71
- isAuthenticated = false
70
+ if (!await verifyJwtToken(event, auth.jwtSecret)) {
71
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
72
72
  }
73
+ return
73
74
  }
74
75
 
75
- if (!isAuthenticated) {
76
- throw createError({ statusCode: 401, message: 'Unauthorized' })
77
- }
76
+ await requireUserSession(event)
78
77
  }