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 +2 -18
- package/dist/module.d.mts +5 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -8
- package/dist/runtime/server/api/[model]/[id].delete.js +3 -3
- package/dist/runtime/server/api/[model]/[id].get.js +2 -2
- package/dist/runtime/server/api/[model]/[id].patch.js +15 -4
- package/dist/runtime/server/api/[model]/index.get.js +2 -2
- package/dist/runtime/server/api/[model]/index.post.js +17 -3
- package/dist/runtime/server/stubs/auth.d.ts +1 -0
- package/dist/runtime/server/stubs/auth.js +4 -0
- package/dist/runtime/server/utils/auth.d.ts +1 -1
- package/dist/runtime/server/utils/auth.js +34 -2
- package/dist/runtime/server/utils/handler.d.ts +2 -1
- package/dist/runtime/server/utils/handler.js +14 -2
- package/package.json +1 -1
- package/src/runtime/server/api/[model]/[id].delete.ts +4 -4
- package/src/runtime/server/api/[model]/[id].get.ts +3 -3
- package/src/runtime/server/api/[model]/[id].patch.ts +27 -5
- package/src/runtime/server/api/[model]/index.get.ts +3 -3
- package/src/runtime/server/api/[model]/index.post.ts +30 -4
- package/src/runtime/server/stubs/auth.ts +4 -0
- package/src/runtime/server/utils/auth.ts +57 -6
- package/src/runtime/server/utils/handler.ts +18 -2
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
|
|
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
|
|
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
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
@@ -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 -
|
|
7
|
-
import {
|
|
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
|
|
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 -
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
5
|
-
import {
|
|
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 =
|
|
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 -
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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)
|