nuxt-auto-crud 1.18.1 → 1.20.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/README.md CHANGED
@@ -51,7 +51,7 @@ 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
 
@@ -121,23 +121,7 @@ export default defineConfig({
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 { db, schema } from 'hub:db'
131
- export { sql, eq, and, or } from 'drizzle-orm'
132
-
133
- export const tables = schema
134
-
135
- export function useDrizzle() {
136
- return db
137
- }
138
-
139
- export type User = typeof schema.users.$inferSelect
140
- ```
141
125
 
142
126
  #### Define your database schema
143
127
 
@@ -462,7 +446,7 @@ You can customize hidden fields by modifying the `modelMapper.ts` utility.
462
446
 
463
447
  - Nuxt 3 or 4
464
448
  - Drizzle ORM (SQLite)
465
- - NuxtHub >= 0.10.0 (Recommended) or [Custom SQLite Setup](./custom-setup.md)
449
+ - NuxtHub >= 0.10.0
466
450
 
467
451
  ## 🔗 Other Helpful Links
468
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.18.1",
4
+ "version": "1.20.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,6 @@ const module$1 = defineNuxtModule({
7
7
  },
8
8
  defaults: {
9
9
  schemaPath: "server/db/schema",
10
- drizzlePath: "server/utils/drizzle",
11
10
  auth: false
12
11
  },
13
12
  async setup(options, nuxt) {
@@ -17,17 +16,13 @@ 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"));
26
20
  const stubsPath = resolver.resolve("./runtime/server/stubs/auth");
27
21
  if (!hasNuxtModule("nuxt-auth-utils")) {
28
22
  addServerImports([
29
23
  { name: "requireUserSession", from: stubsPath },
30
- { name: "getUserSession", from: stubsPath }
24
+ { name: "getUserSession", from: stubsPath },
25
+ { name: "hashPassword", from: stubsPath }
31
26
  ]);
32
27
  }
33
28
  if (!hasNuxtModule("nuxt-authorization")) {
@@ -53,7 +48,8 @@ const module$1 = defineNuxtModule({
53
48
  },
54
49
  resources: {
55
50
  ...options.resources
56
- }
51
+ },
52
+ hashedFields: options.hashedFields ?? ["password"]
57
53
  };
58
54
  const apiDir = resolver.resolve("./runtime/server/api");
59
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
- const isAdmin = await ensureResourceAccess(event, model, "delete");
8
+ const isAdmin = await ensureResourceAccess(event, model, "delete", { id });
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,29 @@
1
1
  import { eventHandler, getRouterParams, readBody, createError } from "h3";
2
+ import { getUserSession } from "#imports";
2
3
  import { eq } from "drizzle-orm";
3
4
  import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
4
- import { useDrizzle } from "#site/drizzle";
5
- import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
5
+ import { db } from "hub:db";
6
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
6
7
  export default eventHandler(async (event) => {
7
8
  const { model, id } = getRouterParams(event);
8
- const isAdmin = await ensureResourceAccess(event, model, "update");
9
+ const isAdmin = await ensureResourceAccess(event, model, "update", { id });
9
10
  const table = getTableForModel(model);
10
11
  const body = await readBody(event);
11
12
  const payload = filterUpdatableFields(model, body);
13
+ await hashPayloadFields(payload);
12
14
  if ("updatedAt" in table) {
13
15
  payload.updatedAt = /* @__PURE__ */ new Date();
14
16
  }
15
- const updatedRecord = await useDrizzle().update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
17
+ try {
18
+ const session = await getUserSession(event);
19
+ if (session?.user?.id) {
20
+ if ("updatedBy" in table) {
21
+ payload.updatedBy = session.user.id;
22
+ }
23
+ }
24
+ } catch {
25
+ }
26
+ const updatedRecord = await db.update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
16
27
  if (!updatedRecord) {
17
28
  throw createError({
18
29
  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,27 @@
1
1
  import { eventHandler, getRouterParams, readBody } from "h3";
2
+ import { getUserSession } from "#imports";
2
3
  import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
3
- import { useDrizzle } from "#site/drizzle";
4
- import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
4
+ import { db } from "hub:db";
5
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
5
6
  export default eventHandler(async (event) => {
6
7
  const { model } = getRouterParams(event);
7
8
  const isAdmin = await ensureResourceAccess(event, model, "create");
8
9
  const table = getTableForModel(model);
9
10
  const body = await readBody(event);
10
11
  const payload = filterUpdatableFields(model, body);
11
- const newRecord = await useDrizzle().insert(table).values(payload).returning().get();
12
+ await hashPayloadFields(payload);
13
+ try {
14
+ const session = await getUserSession(event);
15
+ if (session?.user?.id) {
16
+ if ("createdBy" in table) {
17
+ payload.createdBy = session.user.id;
18
+ }
19
+ if ("updatedBy" in table) {
20
+ payload.updatedBy = session.user.id;
21
+ }
22
+ }
23
+ } catch {
24
+ }
25
+ const newRecord = await db.insert(table).values(payload).returning().get();
12
26
  return formatResourceResult(model, newRecord, isAdmin);
13
27
  });
@@ -2,6 +2,7 @@ export declare const requireUserSession: () => never;
2
2
  export declare const getUserSession: () => Promise<{
3
3
  user: null;
4
4
  }>;
5
+ export declare const hashPassword: (password: string) => Promise<string>;
5
6
  export declare const allows: () => Promise<boolean>;
6
7
  export declare const abilities: null;
7
8
  export declare const abilityLogic: null;
@@ -2,6 +2,10 @@ export const requireUserSession = () => {
2
2
  throw new Error("nuxt-auth-utils not installed");
3
3
  };
4
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
+ };
5
9
  export const allows = () => Promise.resolve(true);
6
10
  export const abilities = null;
7
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,46 @@ 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
+ const hasCreatedBy = "createdBy" in table;
41
+ const hasUserId = "userId" in table;
42
+ if (hasCreatedBy || hasUserId) {
43
+ const query = db.select().from(table).where(eq(table.id, Number(context.id)));
44
+ const record = await query.get();
45
+ if (record) {
46
+ if (hasCreatedBy) {
47
+ if (String(record.createdBy) === String(user.id)) return true;
48
+ }
49
+ if (hasUserId) {
50
+ if (String(record.userId) === String(user.id)) return true;
51
+ }
52
+ }
53
+ }
54
+ } catch (e) {
55
+ console.error("[checkAdminAccess] Ownership check failed", e);
56
+ }
57
+ }
58
+ }
28
59
  if (user) throw createError({ statusCode: 403, message: "Forbidden" });
29
60
  return false;
30
61
  }
31
62
  return true;
32
63
  } catch (err) {
33
64
  if (err.statusCode === 403) throw err;
65
+ console.error("[checkAdminAccess] Error", err);
34
66
  return false;
35
67
  }
36
68
  }
@@ -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,8 +1,9 @@
1
1
  import { createError } from "h3";
2
2
  import { checkAdminAccess } from "./auth.js";
3
3
  import { filterHiddenFields, filterPublicColumns } from "./modelMapper.js";
4
- export async function ensureResourceAccess(event, model, action) {
5
- 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);
6
7
  if (!isAuthorized) {
7
8
  throw createError({
8
9
  statusCode: 401,
@@ -11,6 +12,17 @@ export async function ensureResourceAccess(event, model, action) {
11
12
  }
12
13
  return true;
13
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
+ }
24
+ }
25
+ }
14
26
  export function formatResourceResult(model, data, isAdmin) {
15
27
  if (isAdmin) {
16
28
  return filterHiddenFields(model, data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
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",
@@ -3,18 +3,18 @@ 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) => {
11
11
  const { model, id } = getRouterParams(event) as { model: string, id: string }
12
- const isAdmin = await ensureResourceAccess(event, model, 'delete')
12
+ const isAdmin = await ensureResourceAccess(event, model, 'delete', { id })
13
13
 
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)))
@@ -1,28 +1,50 @@
1
1
  // server/api/[model]/[id].patch.ts
2
2
  import { eventHandler, getRouterParams, readBody, createError } from 'h3'
3
+ import type { H3Event } from 'h3'
4
+ import { getUserSession } from '#imports'
3
5
  import { eq } from 'drizzle-orm'
4
6
  import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
5
7
  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'
8
+
9
+ // @ts-expect-error - hub:db is a virtual alias
10
+ import { db } from 'hub:db'
11
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
9
12
 
10
13
  export default eventHandler(async (event) => {
11
14
  const { model, id } = getRouterParams(event) as { model: string, id: string }
12
- const isAdmin = await ensureResourceAccess(event, model, 'update')
15
+ // Pass the ID as context for row-level security checks (e.g. self-update)
16
+ const isAdmin = await ensureResourceAccess(event, model, 'update', { id })
13
17
 
14
18
  const table = getTableForModel(model) as TableWithId
15
19
 
16
20
  const body = await readBody(event)
17
21
  const payload = filterUpdatableFields(model, body)
18
22
 
23
+ // Auto-hash fields based on config (default: ['password'])
24
+ // Auto-hash fields based on config (default: ['password'])
25
+ await hashPayloadFields(payload)
26
+
19
27
  // Automatically update updatedAt if it exists
20
28
  if ('updatedAt' in table) {
21
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
30
  (payload as any).updatedAt = new Date()
23
31
  }
24
32
 
25
- const updatedRecord = await useDrizzle()
33
+ // Inject updatedBy if user is authenticated
34
+ try {
35
+ const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number } | null }>)(event)
36
+ if (session?.user?.id) {
37
+ if ('updatedBy' in table) {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ (payload as any).updatedBy = session.user.id
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // No session available
45
+ }
46
+
47
+ const updatedRecord = await db
26
48
  .update(table)
27
49
  .set(payload)
28
50
  .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,11 @@
1
1
  // server/api/[model]/index.post.ts
2
2
  import { eventHandler, getRouterParams, readBody } from 'h3'
3
+ import type { H3Event } from 'h3'
4
+ import { getUserSession } from '#imports'
3
5
  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'
6
+ // @ts-expect-error - hub:db is a virtual alias
7
+ import { db } from 'hub:db'
8
+ import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
7
9
 
8
10
  export default eventHandler(async (event) => {
9
11
  const { model } = getRouterParams(event) as { model: string }
@@ -14,7 +16,31 @@ export default eventHandler(async (event) => {
14
16
  const body = await readBody(event)
15
17
  const payload = filterUpdatableFields(model, body)
16
18
 
17
- const newRecord = await useDrizzle().insert(table).values(payload).returning().get()
19
+ // Auto-hash fields based on config (default: ['password'])
20
+ await hashPayloadFields(payload)
21
+
22
+ // Inject createdBy/updatedBy if user is authenticated
23
+ try {
24
+ const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number } | null }>)(event)
25
+ 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
29
+ if ('createdBy' in table) {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ (payload as any).createdBy = session.user.id
32
+ }
33
+ if ('updatedBy' in table) {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ (payload as any).updatedBy = session.user.id
36
+ }
37
+ }
38
+ }
39
+ catch {
40
+ // No session available
41
+ }
42
+
43
+ const newRecord = await db.insert(table).values(payload).returning().get()
18
44
 
19
45
  return formatResourceResult(model, newRecord as Record<string, unknown>, isAdmin)
20
46
  })
@@ -2,6 +2,10 @@ export const requireUserSession = () => {
2
2
  throw new Error('nuxt-auth-utils not installed')
3
3
  }
4
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
+ }
5
9
  export const allows = () => Promise.resolve(true)
6
10
  export const abilities = null
7
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,60 @@ 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 'createdBy' or 'userId' column for ownership
66
+
67
+ // const columns = table.columns || {}
68
+
69
+ const hasCreatedBy = 'createdBy' in table
70
+ const hasUserId = 'userId' in table
71
+
72
+ if (hasCreatedBy || hasUserId) {
73
+ // @ts-expect-error - table is dynamic
74
+ const query = db.select().from(table).where(eq(table.id, Number(context.id)))
75
+ const record = await query.get()
76
+
77
+ if (record) {
78
+ // Check createdBy
79
+ if (hasCreatedBy) {
80
+ if (String(record.createdBy) === String(user.id)) return true
81
+ }
82
+ // Check userId (legacy)
83
+ if (hasUserId) {
84
+ if (String(record.userId) === String(user.id)) return true
85
+ }
86
+ }
87
+ }
88
+ }
89
+ catch (e) {
90
+ console.error('[checkAdminAccess] Ownership check failed', e)
91
+ }
92
+ }
93
+ }
94
+
45
95
  if (user) throw createError({ statusCode: 403, message: 'Forbidden' })
46
96
  return false
47
97
  }
@@ -49,6 +99,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
49
99
  }
50
100
  catch (err) {
51
101
  if ((err as { statusCode: number }).statusCode === 403) throw err
102
+ console.error('[checkAdminAccess] Error', err)
52
103
  return false
53
104
  }
54
105
  }
@@ -73,5 +124,5 @@ export async function ensureAuthenticated(event: H3Event): Promise<void> {
73
124
  return
74
125
  }
75
126
 
76
- await requireUserSession(event)
127
+ await (requireUserSession as (event: H3Event) => Promise<void>)(event)
77
128
  }
@@ -3,10 +3,11 @@ import type { H3Event } from 'h3'
3
3
 
4
4
  import { checkAdminAccess } from './auth'
5
5
  import { filterHiddenFields, filterPublicColumns } from './modelMapper'
6
+ import { useAutoCrudConfig } from './config'
6
7
 
7
- export async function ensureResourceAccess(event: H3Event, model: string, action: string): Promise<boolean> {
8
+ export async function ensureResourceAccess(event: H3Event, model: string, action: string, context?: unknown): Promise<boolean> {
8
9
  // This throws 403 if not authorized
9
- const isAuthorized = await checkAdminAccess(event, model, action)
10
+ const isAuthorized = await checkAdminAccess(event, model, action, context)
10
11
  if (!isAuthorized) {
11
12
  throw createError({
12
13
  statusCode: 401,
@@ -17,6 +18,21 @@ export async function ensureResourceAccess(event: H3Event, model: string, action
17
18
  return true
18
19
  }
19
20
 
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
+ }
34
+ }
35
+
20
36
  export function formatResourceResult(model: string, data: Record<string, unknown>, isAdmin: boolean) {
21
37
  if (isAdmin) {
22
38
  return filterHiddenFields(model, data)