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 +17 -35
- package/dist/module.d.mts +5 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -9
- package/dist/runtime/server/api/[model]/[id].delete.js +2 -2
- package/dist/runtime/server/api/[model]/[id].get.js +2 -2
- package/dist/runtime/server/api/[model]/[id].patch.js +5 -4
- package/dist/runtime/server/api/[model]/index.get.js +2 -2
- package/dist/runtime/server/api/[model]/index.post.js +4 -3
- package/dist/runtime/server/stubs/auth.d.ts +8 -0
- package/dist/runtime/server/stubs/auth.js +11 -0
- package/dist/runtime/server/utils/auth.d.ts +1 -1
- package/dist/runtime/server/utils/auth.js +26 -2
- package/dist/runtime/server/utils/handler.d.ts +2 -1
- package/dist/runtime/server/utils/handler.js +14 -9
- package/package.json +3 -2
- package/src/runtime/server/api/[model]/[id].delete.ts +3 -3
- package/src/runtime/server/api/[model]/[id].get.ts +3 -3
- package/src/runtime/server/api/[model]/[id].patch.ts +11 -5
- package/src/runtime/server/api/[model]/index.get.ts +3 -3
- package/src/runtime/server/api/[model]/index.post.ts +7 -4
- package/src/runtime/server/stubs/auth.ts +11 -0
- package/src/runtime/server/utils/auth.ts +46 -6
- package/src/runtime/server/utils/handler.ts +19 -15
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
|
|
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
|
|
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
|
-
|
|
83
|
+
db: 'sqlite',
|
|
84
84
|
},
|
|
85
85
|
|
|
86
86
|
autoCrud: {
|
|
87
|
-
schemaPath: 'server/
|
|
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": "
|
|
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/
|
|
120
|
-
out: './server/
|
|
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/
|
|
128
|
+
Create your schema files in `server/db/schema/`. For example, `server/db/schema/users.ts`:
|
|
147
129
|
|
|
148
130
|
```typescript
|
|
149
|
-
// server/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
243
|
-
out: './server/
|
|
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/
|
|
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
|
|
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
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/
|
|
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 {
|
|
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
|
|
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,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 {
|
|
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
|
|
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 {
|
|
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,14 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
2
|
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
6
|
-
export async function ensureResourceAccess(event, model, action) {
|
|
7
|
-
const
|
|
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
|
-
|
|
16
|
-
|
|
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.
|
|
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.
|
|
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 -
|
|
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) => {
|
|
@@ -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
|
|
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)))
|
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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,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 -
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
|
|
4
4
|
import { checkAdminAccess } from './auth'
|
|
5
5
|
import { filterHiddenFields, filterPublicColumns } from './modelMapper'
|
|
6
|
+
import { useAutoCrudConfig } from './config'
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
return true
|
|
25
|
-
}
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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) {
|