nuxt-auto-crud 1.17.3 → 1.19.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 CHANGED
@@ -51,13 +51,13 @@ Detailed instructions can be found in [https://auto-crud.clifland.in/docs/auto-c
51
51
 
52
52
  If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
53
53
 
54
- > **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).
54
+ > **Note:** These instructions have been simplified for NuxtHub.
55
55
 
56
56
  #### Install dependencies
57
57
 
58
58
  ```bash
59
59
  # Install module and required dependencies
60
- npm install nuxt-auto-crud @nuxthub/core@latest drizzle-orm
60
+ npm install nuxt-auto-crud @nuxthub/core@^0.10.0 drizzle-orm
61
61
 
62
62
  # Optional: Install auth dependencies if using Session Auth (Recommended)
63
63
  npm install nuxt-auth-utils nuxt-authorization
@@ -80,11 +80,11 @@ export default defineNuxtConfig({
80
80
  modules: ['@nuxthub/core', 'nuxt-auto-crud'],
81
81
 
82
82
  hub: {
83
- database: true,
83
+ db: 'sqlite',
84
84
  },
85
85
 
86
86
  autoCrud: {
87
- schemaPath: 'server/database/schema',
87
+ schemaPath: 'server/db/schema',
88
88
  // auth: false,
89
89
  auth: {
90
90
  type: 'session', // for Normal Authentication with nuxt-auth-utils
@@ -102,7 +102,7 @@ Add the generation script to your `package.json`:
102
102
  ```json
103
103
  {
104
104
  "scripts": {
105
- "db:generate": "drizzle-kit generate"
105
+ "db:generate": "nuxt db generate"
106
106
  }
107
107
  // ...
108
108
  }
@@ -116,37 +116,19 @@ import { defineConfig } from 'drizzle-kit'
116
116
 
117
117
  export default defineConfig({
118
118
  dialect: 'sqlite',
119
- schema: './server/database/schema/index.ts', // Point to your schema index file
120
- out: './server/database/migrations'
119
+ schema: './server/db/schema/index.ts', // Point to your schema index file
120
+ out: './server/db/migrations'
121
121
  })
122
122
  ```
123
123
 
124
- #### Setup Database Connection
125
124
 
126
- Create `server/utils/drizzle.ts` to export the database instance:
127
-
128
- ```typescript
129
- // server/utils/drizzle.ts
130
- import { drizzle } from 'drizzle-orm/d1'
131
- export { sql, eq, and, or } from 'drizzle-orm'
132
-
133
- import * as schema from '../database/schema'
134
-
135
- export const tables = schema
136
-
137
- export function useDrizzle() {
138
- return drizzle(hubDatabase(), { schema })
139
- }
140
-
141
- export type User = typeof schema.users.$inferSelect
142
- ```
143
125
 
144
126
  #### Define your database schema
145
127
 
146
- Create your schema files in `server/database/schema/`. For example, `server/database/schema/users.ts`:
128
+ Create your schema files in `server/db/schema/`. For example, `server/db/schema/users.ts`:
147
129
 
148
130
  ```typescript
149
- // server/database/schema/users.ts
131
+ // server/db/schema/users.ts
150
132
  import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
151
133
 
152
134
  export const users = sqliteTable('users', {
@@ -174,7 +156,7 @@ That's it! 🎉 Your CRUD APIs are now available at `/api/users`.
174
156
  To add a new table (e.g., `posts`), simply create a new file in your schema directory:
175
157
 
176
158
  ```typescript
177
- // server/database/schema/posts.ts
159
+ // server/db/schema/posts.ts
178
160
  import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
179
161
  import { users } from './users'
180
162
 
@@ -187,10 +169,10 @@ export const posts = sqliteTable('posts', {
187
169
  })
188
170
  ```
189
171
 
190
- Then, ensure it is exported in your `server/database/schema/index.ts` (if you are using an index file) or that your `drizzle.config.ts` is pointing to the correct location.
172
+ Then, ensure it is exported in your `server/db/schema/index.ts` (if you are using an index file) or that your `drizzle.config.ts` is pointing to the correct location.
191
173
 
192
174
  ```typescript
193
- // server/database/schema/index.ts
175
+ // server/db/schema/index.ts
194
176
  export * from './users'
195
177
  export * from './posts'
196
178
  ```
@@ -217,7 +199,7 @@ In this case, you might handle authentication differently (e.g., validating toke
217
199
  export default defineNuxtConfig({
218
200
  modules: ['nuxt-auto-crud'],
219
201
  autoCrud: {
220
- schemaPath: 'server/database/schema',
202
+ schemaPath: 'server/db/schema',
221
203
  // auth: false, // Uncomment this line for testing APIs without auth
222
204
  auth: {
223
205
  type: 'jwt', // for app providing backend apis only
@@ -239,8 +221,8 @@ import { defineConfig } from 'drizzle-kit'
239
221
 
240
222
  export default defineConfig({
241
223
  dialect: 'sqlite',
242
- schema: './server/database/schema/index.ts',
243
- out: './server/database/migrations',
224
+ schema: './server/db/schema/index.ts',
225
+ out: './server/db/migrations',
244
226
  tablesFilter: ['!_hub_migrations'],
245
227
  })
246
228
  ```
@@ -423,7 +405,7 @@ await $fetch("/api/users/1", {
423
405
  export default defineNuxtConfig({
424
406
  autoCrud: {
425
407
  // Path to your database schema file (relative to project root)
426
- schemaPath: "server/database/schema", // default
408
+ schemaPath: "server/db/schema", // default
427
409
 
428
410
  // Authentication configuration (see "Authentication Configuration" section)
429
411
  auth: {
@@ -464,7 +446,7 @@ You can customize hidden fields by modifying the `modelMapper.ts` utility.
464
446
 
465
447
  - Nuxt 3 or 4
466
448
  - Drizzle ORM (SQLite)
467
- - NuxtHub (Recommended) or [Custom SQLite Setup](./custom-setup.md)
449
+ - NuxtHub >= 0.10.0
468
450
 
469
451
  ## 🔗 Other Helpful Links
470
452
 
package/dist/module.d.mts CHANGED
@@ -6,11 +6,6 @@ interface ModuleOptions {
6
6
  * @default 'server/database/schema'
7
7
  */
8
8
  schemaPath?: string;
9
- /**
10
- * Path to the drizzle instance file (must export useDrizzle)
11
- * @default 'server/utils/drizzle'
12
- */
13
- drizzlePath?: string;
14
9
  /**
15
10
  * Authentication configuration
16
11
  */
@@ -22,6 +17,11 @@ interface ModuleOptions {
22
17
  resources?: {
23
18
  [modelName: string]: string[];
24
19
  };
20
+ /**
21
+ * Fields that should be automatically hashed before storage
22
+ * @default ['password']
23
+ */
24
+ hashedFields?: string[];
25
25
  }
26
26
  interface AuthOptions {
27
27
  /**
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.17.3",
4
+ "version": "1.19.1",
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, addImportsDir, addServerHandler, addServerImportsDir } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImportsDir, hasNuxtModule, addServerImports, addServerHandler, addServerImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -6,8 +6,7 @@ const module$1 = defineNuxtModule({
6
6
  configKey: "autoCrud"
7
7
  },
8
8
  defaults: {
9
- schemaPath: "server/database/schema",
10
- drizzlePath: "server/utils/drizzle",
9
+ schemaPath: "server/db/schema",
11
10
  auth: false
12
11
  },
13
12
  async setup(options, nuxt) {
@@ -17,12 +16,22 @@ const module$1 = defineNuxtModule({
17
16
  options.schemaPath
18
17
  );
19
18
  nuxt.options.alias["#site/schema"] = schemaPath;
20
- const drizzlePath = resolver.resolve(
21
- nuxt.options.rootDir,
22
- options.drizzlePath
23
- );
24
- nuxt.options.alias["#site/drizzle"] = drizzlePath;
25
19
  addImportsDir(resolver.resolve(nuxt.options.rootDir, "shared/utils"));
20
+ const stubsPath = resolver.resolve("./runtime/server/stubs/auth");
21
+ if (!hasNuxtModule("nuxt-auth-utils")) {
22
+ addServerImports([
23
+ { name: "requireUserSession", from: stubsPath },
24
+ { name: "getUserSession", from: stubsPath },
25
+ { name: "hashPassword", from: stubsPath }
26
+ ]);
27
+ }
28
+ if (!hasNuxtModule("nuxt-authorization")) {
29
+ addServerImports([
30
+ { name: "allows", from: stubsPath },
31
+ { name: "abilities", from: stubsPath },
32
+ { name: "abilityLogic", from: stubsPath }
33
+ ]);
34
+ }
26
35
  nuxt.options.alias["#authorization"] ||= "nuxt-authorization/utils";
27
36
  const mergedAuth = options.auth === false ? { authentication: false, authorization: false, type: "session" } : {
28
37
  authentication: true,
@@ -39,7 +48,8 @@ const module$1 = defineNuxtModule({
39
48
  },
40
49
  resources: {
41
50
  ...options.resources
42
- }
51
+ },
52
+ hashedFields: options.hashedFields ?? ["password"]
43
53
  };
44
54
  const apiDir = resolver.resolve("./runtime/server/api");
45
55
  addServerHandler({
@@ -1,14 +1,14 @@
1
1
  import { eventHandler, getRouterParams, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
3
  import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
4
- import { useDrizzle } from "#site/drizzle";
4
+ import { db } from "hub:db";
5
5
  import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
6
6
  export default eventHandler(async (event) => {
7
7
  const { model, id } = getRouterParams(event);
8
8
  const isAdmin = await ensureResourceAccess(event, model, "delete");
9
9
  const table = getTableForModel(model);
10
10
  const singularName = getModelSingularName(model);
11
- const deletedRecord = await useDrizzle().delete(table).where(eq(table.id, Number(id))).returning().get();
11
+ const deletedRecord = await db.delete(table).where(eq(table.id, Number(id))).returning().get();
12
12
  if (!deletedRecord) {
13
13
  throw createError({
14
14
  statusCode: 404,
@@ -1,14 +1,14 @@
1
1
  import { eventHandler, getRouterParams, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
3
  import { getTableForModel } from "../../utils/modelMapper.js";
4
- import { useDrizzle } from "#site/drizzle";
4
+ import { db } from "hub:db";
5
5
  import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
6
6
  import { checkAdminAccess } from "../../utils/auth.js";
7
7
  export default eventHandler(async (event) => {
8
8
  const { model, id } = getRouterParams(event);
9
9
  const isAdmin = await ensureResourceAccess(event, model, "read");
10
10
  const table = getTableForModel(model);
11
- const record = await useDrizzle().select().from(table).where(eq(table.id, Number(id))).get();
11
+ const record = await db.select().from(table).where(eq(table.id, Number(id))).get();
12
12
  if (!record) {
13
13
  throw createError({
14
14
  statusCode: 404,
@@ -1,18 +1,19 @@
1
1
  import { eventHandler, getRouterParams, readBody, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
3
  import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
4
- import { useDrizzle } from "#site/drizzle";
5
- import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
4
+ import { db } from "hub:db";
5
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
6
6
  export default eventHandler(async (event) => {
7
7
  const { model, id } = getRouterParams(event);
8
- const isAdmin = await ensureResourceAccess(event, model, "update");
8
+ const isAdmin = await ensureResourceAccess(event, model, "update", { id });
9
9
  const table = getTableForModel(model);
10
10
  const body = await readBody(event);
11
11
  const payload = filterUpdatableFields(model, body);
12
+ await hashPayloadFields(payload);
12
13
  if ("updatedAt" in table) {
13
14
  payload.updatedAt = /* @__PURE__ */ new Date();
14
15
  }
15
- const updatedRecord = await useDrizzle().update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
16
+ const updatedRecord = await db.update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
16
17
  if (!updatedRecord) {
17
18
  throw createError({
18
19
  statusCode: 404,
@@ -1,6 +1,6 @@
1
1
  import { eventHandler, getRouterParams } from "h3";
2
2
  import { getTableForModel } from "../../utils/modelMapper.js";
3
- import { useDrizzle } from "#site/drizzle";
3
+ import { db } from "hub:db";
4
4
  import { desc, getTableColumns, eq } from "drizzle-orm";
5
5
  import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
6
6
  import { checkAdminAccess } from "../../utils/auth.js";
@@ -16,7 +16,7 @@ export default eventHandler(async (event) => {
16
16
  }
17
17
  const table = getTableForModel(model);
18
18
  const columns = getTableColumns(table);
19
- let query = useDrizzle().select().from(table);
19
+ let query = db.select().from(table);
20
20
  if (!canListAll && "status" in columns) {
21
21
  query = query.where(eq(table.status, "active"));
22
22
  }
@@ -1,13 +1,14 @@
1
1
  import { eventHandler, getRouterParams, readBody } from "h3";
2
2
  import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
3
- import { useDrizzle } from "#site/drizzle";
4
- import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
3
+ import { db } from "hub:db";
4
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
5
5
  export default eventHandler(async (event) => {
6
6
  const { model } = getRouterParams(event);
7
7
  const isAdmin = await ensureResourceAccess(event, model, "create");
8
8
  const table = getTableForModel(model);
9
9
  const body = await readBody(event);
10
10
  const payload = filterUpdatableFields(model, body);
11
- const newRecord = await useDrizzle().insert(table).values(payload).returning().get();
11
+ await hashPayloadFields(payload);
12
+ const newRecord = await db.insert(table).values(payload).returning().get();
12
13
  return formatResourceResult(model, newRecord, isAdmin);
13
14
  });
@@ -0,0 +1,8 @@
1
+ export declare const requireUserSession: () => never;
2
+ export declare const getUserSession: () => Promise<{
3
+ user: null;
4
+ }>;
5
+ export declare const hashPassword: (password: string) => Promise<string>;
6
+ export declare const allows: () => Promise<boolean>;
7
+ export declare const abilities: null;
8
+ export declare const abilityLogic: null;
@@ -0,0 +1,11 @@
1
+ export const requireUserSession = () => {
2
+ throw new Error("nuxt-auth-utils not installed");
3
+ };
4
+ export const getUserSession = () => Promise.resolve({ user: null });
5
+ export const hashPassword = (password) => {
6
+ console.warn("nuxt-auth-utils not installed. Password not hashed!");
7
+ return Promise.resolve(password);
8
+ };
9
+ export const allows = () => Promise.resolve(true);
10
+ export const abilities = null;
11
+ export const abilityLogic = null;
@@ -1,3 +1,3 @@
1
1
  import type { H3Event } from 'h3';
2
- export declare function checkAdminAccess(event: H3Event, model: string, action: string): Promise<boolean>;
2
+ export declare function checkAdminAccess(event: H3Event, model: string, action: string, context?: unknown): Promise<boolean>;
3
3
  export declare function ensureAuthenticated(event: H3Event): Promise<void>;
@@ -2,7 +2,7 @@ import { createError } from "h3";
2
2
  import { requireUserSession, allows, getUserSession, abilities as globalAbility, abilityLogic } from "#imports";
3
3
  import { useAutoCrudConfig } from "./config.js";
4
4
  import { verifyJwtToken } from "./jwt.js";
5
- export async function checkAdminAccess(event, model, action) {
5
+ export async function checkAdminAccess(event, model, action, context) {
6
6
  const { auth } = useAutoCrudConfig();
7
7
  if (!auth?.authentication) {
8
8
  return true;
@@ -23,14 +23,38 @@ export async function checkAdminAccess(event, model, action) {
23
23
  if (auth.authorization) {
24
24
  try {
25
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);
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) {
29
+ const ownAction = `${action}_own`;
30
+ const userPermissions = user.permissions?.[model];
31
+ if (userPermissions && userPermissions.includes(ownAction)) {
32
+ const { db } = await import("hub:db");
33
+ const { getTableForModel } = await import("./modelMapper.js");
34
+ const { eq } = await import("drizzle-orm");
35
+ try {
36
+ const table = getTableForModel(model);
37
+ if (model === "users" && String(context.id) === String(user.id)) {
38
+ return true;
39
+ }
40
+ if ("userId" in table) {
41
+ const record = await db.select({ userId: table.userId }).from(table).where(eq(table.id, context.id)).get();
42
+ if (record && String(record.userId) === String(user.id)) {
43
+ return true;
44
+ }
45
+ }
46
+ } catch (e) {
47
+ console.error("[checkAdminAccess] Ownership check failed", e);
48
+ }
49
+ }
50
+ }
28
51
  if (user) throw createError({ statusCode: 403, message: "Forbidden" });
29
52
  return false;
30
53
  }
31
54
  return true;
32
55
  } catch (err) {
33
56
  if (err.statusCode === 403) throw err;
57
+ console.error("[checkAdminAccess] Error", err);
34
58
  return false;
35
59
  }
36
60
  }
@@ -1,3 +1,4 @@
1
1
  import type { H3Event } from 'h3';
2
- export declare function ensureResourceAccess(event: H3Event, model: string, action: string): Promise<boolean>;
2
+ export declare function ensureResourceAccess(event: H3Event, model: string, action: string, context?: unknown): Promise<boolean>;
3
+ export declare function hashPayloadFields(payload: Record<string, unknown>): Promise<void>;
3
4
  export declare function formatResourceResult(model: string, data: Record<string, unknown>, isAdmin: boolean): Record<string, unknown>;
@@ -1,22 +1,27 @@
1
1
  import { createError } from "h3";
2
- import { useAutoCrudConfig } from "./config.js";
3
2
  import { checkAdminAccess } from "./auth.js";
4
3
  import { filterHiddenFields, filterPublicColumns } from "./modelMapper.js";
5
- import { getUserSession } from "#imports";
6
- export async function ensureResourceAccess(event, model, action) {
7
- const { auth } = useAutoCrudConfig();
8
- const isAuthorized = await checkAdminAccess(event, model, action);
4
+ import { useAutoCrudConfig } from "./config.js";
5
+ export async function ensureResourceAccess(event, model, action, context) {
6
+ const isAuthorized = await checkAdminAccess(event, model, action, context);
9
7
  if (!isAuthorized) {
10
8
  throw createError({
11
9
  statusCode: 401,
12
10
  message: "Unauthorized"
13
11
  });
14
12
  }
15
- if (!auth?.authentication) {
16
- return true;
13
+ return true;
14
+ }
15
+ export async function hashPayloadFields(payload) {
16
+ const { hashedFields } = useAutoCrudConfig();
17
+ if (hashedFields) {
18
+ console.log("[hashPayloadFields] Configured hashedFields:", hashedFields);
19
+ for (const field of hashedFields) {
20
+ if (payload[field] && typeof payload[field] === "string") {
21
+ payload[field] = await hashPassword(payload[field]);
22
+ }
23
+ }
17
24
  }
18
- const session = await getUserSession(event);
19
- return !!session.user;
20
25
  }
21
26
  export function formatResourceResult(model, data, isAdmin) {
22
27
  if (isAdmin) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.17.3",
3
+ "version": "1.19.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",
@@ -63,12 +63,13 @@
63
63
  "@iconify-json/lucide": "^1.2.77",
64
64
  "@iconify-json/simple-icons": "^1.2.61",
65
65
  "@iconify-json/vscode-icons": "^1.2.37",
66
+ "@libsql/client": "^0.15.15",
66
67
  "@nuxt/devtools": "^3.1.0",
67
68
  "@nuxt/eslint-config": "^1.10.0",
68
69
  "@nuxt/module-builder": "^1.0.2",
69
70
  "@nuxt/schema": "^4.2.1",
70
71
  "@nuxt/test-utils": "^3.20.1",
71
- "@nuxthub/core": "^0.9.1",
72
+ "@nuxthub/core": "^0.10.1",
72
73
  "@types/better-sqlite3": "^7.6.13",
73
74
  "@types/node": "latest",
74
75
  "better-sqlite3": "^12.5.0",
@@ -3,8 +3,8 @@ import { eventHandler, getRouterParams, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
4
  import { getTableForModel, getModelSingularName } from '../../utils/modelMapper'
5
5
  import type { TableWithId } from '../../types'
6
- // @ts-expect-error - #site/drizzle is an alias defined by the module
7
- import { useDrizzle } from '#site/drizzle'
6
+ // @ts-expect-error - hub:db is a virtual alias
7
+ import { db } from 'hub:db'
8
8
  import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
9
9
 
10
10
  export default eventHandler(async (event) => {
@@ -14,7 +14,7 @@ export default eventHandler(async (event) => {
14
14
  const table = getTableForModel(model) as TableWithId
15
15
  const singularName = getModelSingularName(model)
16
16
 
17
- const deletedRecord = await useDrizzle()
17
+ const deletedRecord = await db
18
18
  .delete(table)
19
19
  .where(eq(table.id, Number(id)))
20
20
  .returning()
@@ -3,8 +3,8 @@ import { eventHandler, getRouterParams, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
4
  import { getTableForModel } from '../../utils/modelMapper'
5
5
  import type { TableWithId } from '../../types'
6
- // @ts-expect-error - #site/drizzle is an alias defined by the module
7
- import { useDrizzle } from '#site/drizzle'
6
+ // @ts-expect-error - hub:db is a virtual alias
7
+ import { db } from 'hub:db'
8
8
  import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
9
9
  import { checkAdminAccess } from '../../utils/auth'
10
10
 
@@ -14,7 +14,7 @@ export default eventHandler(async (event) => {
14
14
 
15
15
  const table = getTableForModel(model) as TableWithId
16
16
 
17
- const record = await useDrizzle()
17
+ const record = await db
18
18
  .select()
19
19
  .from(table)
20
20
  .where(eq(table.id, Number(id)))
@@ -3,26 +3,32 @@ import { eventHandler, getRouterParams, readBody, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
4
  import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
5
5
  import type { TableWithId } from '../../types'
6
- // @ts-expect-error - #site/drizzle is an alias defined by the module
7
- import { useDrizzle } from '#site/drizzle'
8
- import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
6
+
7
+ // @ts-expect-error - hub:db is a virtual alias
8
+ import { db } from 'hub:db'
9
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
9
10
 
10
11
  export default eventHandler(async (event) => {
11
12
  const { model, id } = getRouterParams(event) as { model: string, id: string }
12
- const isAdmin = await ensureResourceAccess(event, model, 'update')
13
+ // Pass the ID as context for row-level security checks (e.g. self-update)
14
+ const isAdmin = await ensureResourceAccess(event, model, 'update', { id })
13
15
 
14
16
  const table = getTableForModel(model) as TableWithId
15
17
 
16
18
  const body = await readBody(event)
17
19
  const payload = filterUpdatableFields(model, body)
18
20
 
21
+ // Auto-hash fields based on config (default: ['password'])
22
+ // Auto-hash fields based on config (default: ['password'])
23
+ await hashPayloadFields(payload)
24
+
19
25
  // Automatically update updatedAt if it exists
20
26
  if ('updatedAt' in table) {
21
27
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
28
  (payload as any).updatedAt = new Date()
23
29
  }
24
30
 
25
- const updatedRecord = await useDrizzle()
31
+ const updatedRecord = await db
26
32
  .update(table)
27
33
  .set(payload)
28
34
  .where(eq(table.id, Number(id)))
@@ -1,8 +1,8 @@
1
1
  // server/api/[model]/index.get.ts
2
2
  import { eventHandler, getRouterParams } from 'h3'
3
3
  import { getTableForModel } from '../../utils/modelMapper'
4
- // @ts-expect-error - #site/drizzle is an alias defined by the module
5
- import { useDrizzle } from '#site/drizzle'
4
+ // @ts-expect-error - hub:db is a virtual alias
5
+ import { db } from 'hub:db'
6
6
  import { desc, getTableColumns, eq } from 'drizzle-orm'
7
7
  import type { TableWithId } from '../../types'
8
8
  import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
@@ -25,7 +25,7 @@ export default eventHandler(async (event) => {
25
25
  const table = getTableForModel(model) as TableWithId
26
26
  const columns = getTableColumns(table)
27
27
 
28
- let query = useDrizzle().select().from(table)
28
+ let query = db.select().from(table)
29
29
 
30
30
  // Filter active rows for non-admins (or those without list_all) if status field exists
31
31
 
@@ -1,9 +1,9 @@
1
1
  // server/api/[model]/index.post.ts
2
2
  import { eventHandler, getRouterParams, readBody } from 'h3'
3
3
  import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
4
- // @ts-expect-error - #site/drizzle is an alias defined by the module
5
- import { useDrizzle } from '#site/drizzle'
6
- import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
4
+ // @ts-expect-error - hub:db is a virtual alias
5
+ import { db } from 'hub:db'
6
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
7
7
 
8
8
  export default eventHandler(async (event) => {
9
9
  const { model } = getRouterParams(event) as { model: string }
@@ -14,7 +14,10 @@ export default eventHandler(async (event) => {
14
14
  const body = await readBody(event)
15
15
  const payload = filterUpdatableFields(model, body)
16
16
 
17
- const newRecord = await useDrizzle().insert(table).values(payload).returning().get()
17
+ // Auto-hash fields based on config (default: ['password'])
18
+ await hashPayloadFields(payload)
19
+
20
+ const newRecord = await db.insert(table).values(payload).returning().get()
18
21
 
19
22
  return formatResourceResult(model, newRecord as Record<string, unknown>, isAdmin)
20
23
  })
@@ -0,0 +1,11 @@
1
+ export const requireUserSession = () => {
2
+ throw new Error('nuxt-auth-utils not installed')
3
+ }
4
+ export const getUserSession = () => Promise.resolve({ user: null })
5
+ export const hashPassword = (password: string) => {
6
+ console.warn('nuxt-auth-utils not installed. Password not hashed!')
7
+ return Promise.resolve(password)
8
+ }
9
+ export const allows = () => Promise.resolve(true)
10
+ export const abilities = null
11
+ export const abilityLogic = null
@@ -2,12 +2,12 @@
2
2
  /// <reference path="../../auth.d.ts" />
3
3
  import type { H3Event } from 'h3'
4
4
  import { createError } from 'h3'
5
- // @ts-expect-error - #imports is available in runtime
5
+
6
6
  import { requireUserSession, allows, getUserSession, abilities as globalAbility, abilityLogic } from '#imports'
7
7
  import { useAutoCrudConfig } from './config'
8
8
  import { verifyJwtToken } from './jwt'
9
9
 
10
- export async function checkAdminAccess(event: H3Event, model: string, action: string): Promise<boolean> {
10
+ export async function checkAdminAccess(event: H3Event, model: string, action: string, context?: unknown): Promise<boolean> {
11
11
  const { auth } = useAutoCrudConfig()
12
12
 
13
13
  if (!auth?.authentication) {
@@ -25,7 +25,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
25
25
  // Session based (default)
26
26
  let user = null
27
27
  try {
28
- const session = await getUserSession(event)
28
+ const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number, permissions?: Record<string, string[]> } | null }>)(event)
29
29
  user = session.user
30
30
  }
31
31
  catch {
@@ -38,10 +38,49 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
38
38
  const guestCheck = !user && (typeof abilityLogic === 'function' ? abilityLogic : (typeof globalAbility === 'function' ? globalAbility : null))
39
39
 
40
40
  const allowed = guestCheck
41
- ? await guestCheck(null, model, action)
42
- : await allows(event, globalAbility, model, action)
41
+ ? await (guestCheck as (user: unknown, model: string, action: string, context?: unknown) => Promise<boolean>)(null, model, action, context)
42
+ : await (allows as (event: H3Event, ability: unknown, model: string, action: string, context?: unknown) => Promise<boolean>)(event, globalAbility, model, action, context)
43
43
 
44
44
  if (!allowed) {
45
+ // 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
+ const ownAction = `${action}_own`
48
+ const userPermissions = user.permissions?.[model] as string[] | undefined
49
+
50
+ if (userPermissions && userPermissions.includes(ownAction)) {
51
+ // Verify ownership via DB
52
+ // @ts-expect-error - hub:db virtual alias
53
+ const { db } = await import('hub:db')
54
+ const { getTableForModel } = await import('./modelMapper')
55
+ const { eq } = await import('drizzle-orm')
56
+
57
+ try {
58
+ const table = getTableForModel(model)
59
+
60
+ // Special case: User updating their own profile (record.id === user.id)
61
+ if (model === 'users' && String((context as { id: string | number }).id) === String(user.id)) {
62
+ return true
63
+ }
64
+
65
+ // Standard case: Check 'userId' column for ownership
66
+ // We need to check if table has userId column.
67
+ // We cast to any to check property exist roughly or just try query
68
+ if ('userId' in table) {
69
+ // @ts-expect-error - dyanmic table access
70
+ const record = await db.select({ userId: table.userId }).from(table).where(eq(table.id, context.id)).get()
71
+
72
+ // If record exists and userId matches session user id
73
+ if (record && String(record.userId) === String(user.id)) {
74
+ return true
75
+ }
76
+ }
77
+ }
78
+ catch (e) {
79
+ console.error('[checkAdminAccess] Ownership check failed', e)
80
+ }
81
+ }
82
+ }
83
+
45
84
  if (user) throw createError({ statusCode: 403, message: 'Forbidden' })
46
85
  return false
47
86
  }
@@ -49,6 +88,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
49
88
  }
50
89
  catch (err) {
51
90
  if ((err as { statusCode: number }).statusCode === 403) throw err
91
+ console.error('[checkAdminAccess] Error', err)
52
92
  return false
53
93
  }
54
94
  }
@@ -73,5 +113,5 @@ export async function ensureAuthenticated(event: H3Event): Promise<void> {
73
113
  return
74
114
  }
75
115
 
76
- await requireUserSession(event)
116
+ await (requireUserSession as (event: H3Event) => Promise<void>)(event)
77
117
  }
@@ -1,17 +1,13 @@
1
1
  import { createError } from 'h3'
2
2
  import type { H3Event } from 'h3'
3
- import { useAutoCrudConfig } from './config'
3
+
4
4
  import { checkAdminAccess } from './auth'
5
5
  import { filterHiddenFields, filterPublicColumns } from './modelMapper'
6
+ import { useAutoCrudConfig } from './config'
6
7
 
7
- // @ts-expect-error - #imports is available in runtime
8
- import { getUserSession } from '#imports'
9
-
10
- export async function ensureResourceAccess(event: H3Event, model: string, action: string): Promise<boolean> {
11
- const { auth } = useAutoCrudConfig()
12
-
8
+ export async function ensureResourceAccess(event: H3Event, model: string, action: string, context?: unknown): Promise<boolean> {
13
9
  // This throws 403 if not authorized
14
- const isAuthorized = await checkAdminAccess(event, model, action)
10
+ const isAuthorized = await checkAdminAccess(event, model, action, context)
15
11
  if (!isAuthorized) {
16
12
  throw createError({
17
13
  statusCode: 401,
@@ -19,14 +15,22 @@ export async function ensureResourceAccess(event: H3Event, model: string, action
19
15
  })
20
16
  }
21
17
 
22
- // If authentication is disabled, treated as fully inclusive access
23
- if (!auth?.authentication) {
24
- return true
25
- }
18
+ return true
19
+ }
26
20
 
27
- // Check if user is authenticated
28
- const session = await getUserSession(event)
29
- return !!session.user
21
+ export async function hashPayloadFields(payload: Record<string, unknown>): Promise<void> {
22
+ // Auto-hash fields based on config (default: ['password'])
23
+ const { hashedFields } = useAutoCrudConfig()
24
+
25
+ if (hashedFields) {
26
+ console.log('[hashPayloadFields] Configured hashedFields:', hashedFields)
27
+ for (const field of hashedFields) {
28
+ if (payload[field] && typeof payload[field] === 'string') {
29
+ // @ts-expect-error - hashPassword is auto-imported from nuxt-auth-utils or stub
30
+ payload[field] = await hashPassword(payload[field])
31
+ }
32
+ }
33
+ }
30
34
  }
31
35
 
32
36
  export function formatResourceResult(model: string, data: Record<string, unknown>, isAdmin: boolean) {