@michael-nussbaumer/nuxt-directus 0.1.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 +222 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +33 -0
- package/dist/module.d.ts +33 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +50 -0
- package/dist/runtime/composables/useDirectusApi.d.ts +0 -0
- package/dist/runtime/composables/useDirectusApi.js +96 -0
- package/dist/runtime/composables/useDirectusAuth.d.ts +0 -0
- package/dist/runtime/composables/useDirectusAuth.js +329 -0
- package/dist/runtime/composables/useDirectusRealtime.d.ts +0 -0
- package/dist/runtime/composables/useDirectusRealtime.js +125 -0
- package/dist/runtime/directus.d.ts +0 -0
- package/dist/runtime/directus.js +106 -0
- package/dist/runtime/middleware/directus-auth.d.ts +0 -0
- package/dist/runtime/middleware/directus-auth.js +127 -0
- package/dist/runtime/types.d.ts +0 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# @michael-nussbaumer/nuxt-directus
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@michael-nussbaumer/nuxt-directus)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A production-ready Nuxt 4 module that integrates Directus SDK with authentication, configurable global auth middleware, and automatic TypeScript type generation from Directus OpenAPI schema.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ **Directus SDK Integration** - Seamless integration with @directus/sdk
|
|
11
|
+
- 🔐 **Authentication Composables** - Login, logout, register, verify email, and more
|
|
12
|
+
- 🛡️ **Global Auth Middleware** - Configurable route protection with page meta
|
|
13
|
+
- 📘 **TypeScript Type Generation** - Auto-generate types from Directus OpenAPI schema
|
|
14
|
+
- ⚙️ **Flexible Configuration** - Customize behavior via module options
|
|
15
|
+
- 🚀 **Nuxt 4 Ready** - Built for Nuxt 4 with full TypeScript support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @michael-nussbaumer/nuxt-directus
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Add Module to `nuxt.config.ts`
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
export default defineNuxtConfig({
|
|
29
|
+
modules: ["@michael-nussbaumer/nuxt-directus"],
|
|
30
|
+
|
|
31
|
+
directus: {
|
|
32
|
+
enableGlobalMiddleware: true,
|
|
33
|
+
auth: {
|
|
34
|
+
loginPath: "/auth/login",
|
|
35
|
+
afterLoginPath: "/",
|
|
36
|
+
},
|
|
37
|
+
types: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
openApiUrl: "http://directus.local/server/specs/oas",
|
|
40
|
+
output: "./schema/schema.d.ts",
|
|
41
|
+
authHeaderEnv: "DIRECTUS_OPENAPI_TOKEN",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Set Environment Variables
|
|
48
|
+
|
|
49
|
+
Create a `.env` file:
|
|
50
|
+
|
|
51
|
+
```env
|
|
52
|
+
DIRECTUS_URL=http://localhost:8055
|
|
53
|
+
DIRECTUS_OPENAPI_TOKEN=Bearer your-token-here
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Use Auth Composable
|
|
57
|
+
|
|
58
|
+
```vue
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
const { login, user, isAuthenticated, logout } = useDirectusAuth();
|
|
61
|
+
|
|
62
|
+
const handleLogin = async () => {
|
|
63
|
+
await login({
|
|
64
|
+
email: "user@example.com",
|
|
65
|
+
password: "password",
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
</script>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Page Meta Authentication
|
|
72
|
+
|
|
73
|
+
Protect your routes using page meta:
|
|
74
|
+
|
|
75
|
+
### Public Page
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
definePageMeta({
|
|
79
|
+
auth: false,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Protected Page (Default)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
definePageMeta({
|
|
87
|
+
auth: true,
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Unauthenticated Only (Login Page)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
definePageMeta({
|
|
95
|
+
auth: {
|
|
96
|
+
unauthenticatedOnly: true,
|
|
97
|
+
navigateAuthenticatedTo: "/dashboard",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Custom Redirects
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
definePageMeta({
|
|
106
|
+
auth: {
|
|
107
|
+
navigateUnauthenticatedTo: "/custom-login",
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
Comprehensive guides for all module features:
|
|
115
|
+
|
|
116
|
+
- **[Getting Started](./docs/getting-started.md)** - Installation, setup, and basic usage
|
|
117
|
+
- **[Configuration](./docs/configuration.md)** - Module options and environment variables
|
|
118
|
+
- **[Authentication](./docs/authentication.md)** - Login, logout, registration, and user management
|
|
119
|
+
- **[API Usage](./docs/api.md)** - CRUD operations, queries, and filtering
|
|
120
|
+
- **[Real-time WebSocket](./docs/realtime.md)** - Live subscriptions and event handling
|
|
121
|
+
- **[Middleware](./docs/middleware.md)** - Route protection and authentication flows
|
|
122
|
+
- **[Type Generation](./docs/type-generation.md)** - Automatic TypeScript types from OpenAPI schema
|
|
123
|
+
|
|
124
|
+
## Quick Reference
|
|
125
|
+
|
|
126
|
+
### Module Options
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
{
|
|
130
|
+
enableGlobalMiddleware: true,
|
|
131
|
+
auth: {
|
|
132
|
+
loginPath: '/auth/login',
|
|
133
|
+
registerPath: '/auth/register',
|
|
134
|
+
afterLoginPath: '/',
|
|
135
|
+
afterLogoutPath: '/auth/login'
|
|
136
|
+
},
|
|
137
|
+
types: {
|
|
138
|
+
enabled: true,
|
|
139
|
+
openApiUrl: 'http://directus.local/server/specs/oas',
|
|
140
|
+
output: './schema/schema.d.ts',
|
|
141
|
+
authHeaderEnv: 'DIRECTUS_OPENAPI_TOKEN'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Composables
|
|
147
|
+
|
|
148
|
+
#### `useDirectusAuth()`
|
|
149
|
+
|
|
150
|
+
Authentication methods:
|
|
151
|
+
|
|
152
|
+
- `login(credentials)` - Authenticate user
|
|
153
|
+
- `logout()` - End session
|
|
154
|
+
- `register(data)` - Create account
|
|
155
|
+
- `fetchUser()` - Get current user
|
|
156
|
+
- `verifyEmail(token)` - Verify email
|
|
157
|
+
- `requestPasswordReset(email)` - Request reset
|
|
158
|
+
- `resetPassword(token, password)` - Reset password
|
|
159
|
+
|
|
160
|
+
#### `useDirectusApi()`
|
|
161
|
+
|
|
162
|
+
API operations:
|
|
163
|
+
|
|
164
|
+
- `getItems(collection, query)` - Fetch multiple items
|
|
165
|
+
- `getItem(collection, id, query)` - Fetch single item
|
|
166
|
+
- `createOne(collection, data)` - Create item
|
|
167
|
+
- `createMany(collection, data)` - Create multiple items
|
|
168
|
+
- `updateOne(collection, id, data)` - Update item
|
|
169
|
+
- `updateMany(collection, ids, data)` - Update multiple items
|
|
170
|
+
- `deleteOne(collection, id)` - Delete item
|
|
171
|
+
- `deleteMany(collection, ids)` - Delete multiple items
|
|
172
|
+
- `customRequest(method, path, options)` - Custom endpoint
|
|
173
|
+
|
|
174
|
+
#### `useDirectusRealtime()`
|
|
175
|
+
|
|
176
|
+
WebSocket subscriptions:
|
|
177
|
+
|
|
178
|
+
- `subscribe(collection, event, callback, options)` - Listen to events
|
|
179
|
+
- `unsubscribe(uid)` - Remove specific subscription
|
|
180
|
+
- `unsubscribeCollection(collection)` - Remove all for collection
|
|
181
|
+
- `unsubscribeAll()` - Remove all subscriptions
|
|
182
|
+
|
|
183
|
+
## Development
|
|
184
|
+
|
|
185
|
+
### Setup
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pnpm install
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Development Server
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pnpm run dev
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Build
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pnpm run build
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Playground
|
|
204
|
+
|
|
205
|
+
The module includes a playground for testing:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd playground
|
|
209
|
+
pnpm run dev
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT License © 2026 michael-nussbaumer Communications
|
|
215
|
+
|
|
216
|
+
## Contributing
|
|
217
|
+
|
|
218
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
219
|
+
|
|
220
|
+
## Repository
|
|
221
|
+
|
|
222
|
+
https://github.com/michael-nussbaumerCommunications/nuxt-directus-module
|
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface DirectusPermissionsConfig {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
field: string;
|
|
6
|
+
mapping?: Record<string, string>;
|
|
7
|
+
transform?: (fieldValue: any, user: any) => string | string[];
|
|
8
|
+
}
|
|
9
|
+
interface DirectusAuthConfig {
|
|
10
|
+
loginPath: string;
|
|
11
|
+
registerPath: string;
|
|
12
|
+
afterLoginPath: string;
|
|
13
|
+
afterLogoutPath: string;
|
|
14
|
+
resetPasswordUrl?: string;
|
|
15
|
+
verificationUrl?: string;
|
|
16
|
+
permissions?: DirectusPermissionsConfig;
|
|
17
|
+
}
|
|
18
|
+
interface DirectusTypesConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
openApiUrl?: string;
|
|
21
|
+
output: string;
|
|
22
|
+
redoclyConfig?: string;
|
|
23
|
+
authHeaderEnv: string;
|
|
24
|
+
}
|
|
25
|
+
interface ModuleOptions {
|
|
26
|
+
enableGlobalMiddleware: boolean;
|
|
27
|
+
auth: DirectusAuthConfig;
|
|
28
|
+
types: DirectusTypesConfig;
|
|
29
|
+
}
|
|
30
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
31
|
+
|
|
32
|
+
export { _default as default };
|
|
33
|
+
export type { DirectusAuthConfig, DirectusPermissionsConfig, DirectusTypesConfig, ModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface DirectusPermissionsConfig {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
field: string;
|
|
6
|
+
mapping?: Record<string, string>;
|
|
7
|
+
transform?: (fieldValue: any, user: any) => string | string[];
|
|
8
|
+
}
|
|
9
|
+
interface DirectusAuthConfig {
|
|
10
|
+
loginPath: string;
|
|
11
|
+
registerPath: string;
|
|
12
|
+
afterLoginPath: string;
|
|
13
|
+
afterLogoutPath: string;
|
|
14
|
+
resetPasswordUrl?: string;
|
|
15
|
+
verificationUrl?: string;
|
|
16
|
+
permissions?: DirectusPermissionsConfig;
|
|
17
|
+
}
|
|
18
|
+
interface DirectusTypesConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
openApiUrl?: string;
|
|
21
|
+
output: string;
|
|
22
|
+
redoclyConfig?: string;
|
|
23
|
+
authHeaderEnv: string;
|
|
24
|
+
}
|
|
25
|
+
interface ModuleOptions {
|
|
26
|
+
enableGlobalMiddleware: boolean;
|
|
27
|
+
auth: DirectusAuthConfig;
|
|
28
|
+
types: DirectusTypesConfig;
|
|
29
|
+
}
|
|
30
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
31
|
+
|
|
32
|
+
export { _default as default };
|
|
33
|
+
export type { DirectusAuthConfig, DirectusPermissionsConfig, DirectusTypesConfig, ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImportsDir, addRouteMiddleware, addPlugin } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
|
+
|
|
4
|
+
const module = defineNuxtModule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "@michael-nussbaumer/nuxt-directus",
|
|
7
|
+
configKey: "directus",
|
|
8
|
+
compatibility: {
|
|
9
|
+
nuxt: "^3.0.0 || ^4.0.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
defaults: {
|
|
13
|
+
enableGlobalMiddleware: true,
|
|
14
|
+
auth: {
|
|
15
|
+
loginPath: "/auth/login",
|
|
16
|
+
registerPath: "/auth/register",
|
|
17
|
+
afterLoginPath: "/",
|
|
18
|
+
afterLogoutPath: "/auth/login",
|
|
19
|
+
permissions: {
|
|
20
|
+
enabled: false,
|
|
21
|
+
field: "role"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
types: {
|
|
25
|
+
enabled: false,
|
|
26
|
+
output: "./schema/schema.d.ts",
|
|
27
|
+
authHeaderEnv: "DIRECTUS_OPENAPI_TOKEN"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async setup(options, nuxt) {
|
|
31
|
+
const resolver = createResolver(import.meta.url);
|
|
32
|
+
nuxt.options.runtimeConfig.public.directus = defu(nuxt.options.runtimeConfig.public.directus, {
|
|
33
|
+
enableGlobalMiddleware: options.enableGlobalMiddleware,
|
|
34
|
+
auth: options.auth
|
|
35
|
+
});
|
|
36
|
+
nuxt.options.alias["#directus"] = resolver.resolve("./runtime");
|
|
37
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
38
|
+
addRouteMiddleware({
|
|
39
|
+
name: "directus-auth",
|
|
40
|
+
path: resolver.resolve("./runtime/middleware/directus-auth"),
|
|
41
|
+
global: true
|
|
42
|
+
});
|
|
43
|
+
addPlugin(resolver.resolve("./runtime/directus"));
|
|
44
|
+
nuxt.hook("prepare:types", ({ references }) => {
|
|
45
|
+
references.push({ path: resolver.resolve("./runtime/types") });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export { module as default };
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useNuxtApp } from "#app";
|
|
2
|
+
import { readItems, readItem, createItem, createItems, updateItem, updateItems, deleteItem, deleteItems } from "@directus/sdk";
|
|
3
|
+
export const useDirectusApi = () => {
|
|
4
|
+
const { $directus } = useNuxtApp();
|
|
5
|
+
const client = $directus;
|
|
6
|
+
const getItems = async (collection, query) => {
|
|
7
|
+
try {
|
|
8
|
+
return await client.request(readItems(collection, query));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(`[Directus API] Error reading items from ${collection}:`, error);
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const getItem = async (collection, id, query) => {
|
|
15
|
+
try {
|
|
16
|
+
return await client.request(readItem(collection, id, query));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(`[Directus API] Error reading item ${id} from ${collection}:`, error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const createOne = async (collection, item) => {
|
|
23
|
+
try {
|
|
24
|
+
return await client.request(createItem(collection, item));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`[Directus API] Error creating item in ${collection}:`, error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const createMany = async (collection, items) => {
|
|
31
|
+
try {
|
|
32
|
+
return await client.request(createItems(collection, items));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`[Directus API] Error creating items in ${collection}:`, error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const updateOne = async (collection, id, item) => {
|
|
39
|
+
try {
|
|
40
|
+
return await client.request(updateItem(collection, id, item));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`[Directus API] Error updating item ${id} in ${collection}:`, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const updateMany = async (collection, ids, data) => {
|
|
47
|
+
try {
|
|
48
|
+
return await client.request(updateItems(collection, ids, data));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`[Directus API] Error updating items in ${collection}:`, error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const deleteOne = async (collection, id) => {
|
|
55
|
+
try {
|
|
56
|
+
return await client.request(deleteItem(collection, id));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`[Directus API] Error deleting item ${id} from ${collection}:`, error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const deleteMany = async (collection, ids) => {
|
|
63
|
+
try {
|
|
64
|
+
return await client.request(deleteItems(collection, ids));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`[Directus API] Error deleting items from ${collection}:`, error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const customRequest = async (path, options) => {
|
|
71
|
+
try {
|
|
72
|
+
return await client.request(path, options);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`[Directus API] Error making custom request to ${path}:`, error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
// Read operations
|
|
80
|
+
getItems,
|
|
81
|
+
getItem,
|
|
82
|
+
// Create operations
|
|
83
|
+
createOne,
|
|
84
|
+
createMany,
|
|
85
|
+
// Update operations
|
|
86
|
+
updateOne,
|
|
87
|
+
updateMany,
|
|
88
|
+
// Delete operations
|
|
89
|
+
deleteOne,
|
|
90
|
+
deleteMany,
|
|
91
|
+
// Custom requests
|
|
92
|
+
customRequest,
|
|
93
|
+
// Direct client access for advanced usage
|
|
94
|
+
client
|
|
95
|
+
};
|
|
96
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { ref, computed } from "vue";
|
|
2
|
+
import { useNuxtApp, navigateTo, useRuntimeConfig, useCookie } from "#app";
|
|
3
|
+
import { refresh, registerUser, registerUserVerify, passwordRequest, passwordReset, createDirectus, rest, updateMe } from "@directus/sdk";
|
|
4
|
+
export const useDirectusAuth = () => {
|
|
5
|
+
const { $directus, $directusAuth, $directusWs } = useNuxtApp();
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const isAuthenticated = computed(() => $directusAuth.isAuthenticated.value);
|
|
8
|
+
const user = computed(() => $directusAuth.currentUser.value);
|
|
9
|
+
const isLoading = ref(false);
|
|
10
|
+
const error = ref(null);
|
|
11
|
+
const login = async (credentials) => {
|
|
12
|
+
isLoading.value = true;
|
|
13
|
+
error.value = null;
|
|
14
|
+
try {
|
|
15
|
+
const client = $directus;
|
|
16
|
+
const loginData = {
|
|
17
|
+
email: credentials.email,
|
|
18
|
+
password: credentials.password
|
|
19
|
+
};
|
|
20
|
+
if (credentials.otp) {
|
|
21
|
+
loginData.otp = credentials.otp;
|
|
22
|
+
}
|
|
23
|
+
await client.login(loginData, { mode: "cookie" });
|
|
24
|
+
await $directusAuth.checkAuthStatus();
|
|
25
|
+
if (!$directusWs.isConnected.value) {
|
|
26
|
+
$directusWs.initialize();
|
|
27
|
+
}
|
|
28
|
+
const redirectCookie = useCookie("directus-auth-redirect", {
|
|
29
|
+
default: () => ""
|
|
30
|
+
});
|
|
31
|
+
const afterLoginPath = config.public.directus?.auth?.afterLoginPath || "/";
|
|
32
|
+
const redirectTo = redirectCookie.value || afterLoginPath;
|
|
33
|
+
redirectCookie.value = "";
|
|
34
|
+
await navigateTo(redirectTo);
|
|
35
|
+
return user.value;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
error.value = e.message || "Login failed";
|
|
38
|
+
throw e;
|
|
39
|
+
} finally {
|
|
40
|
+
isLoading.value = false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const logout = async () => {
|
|
44
|
+
isLoading.value = true;
|
|
45
|
+
error.value = null;
|
|
46
|
+
try {
|
|
47
|
+
const client = $directus;
|
|
48
|
+
const refresh_token = $directusAuth.getRefreshToken();
|
|
49
|
+
if (refresh_token) {
|
|
50
|
+
await client.logout({ mode: "cookie", refresh_token });
|
|
51
|
+
} else {
|
|
52
|
+
await client.logout({ mode: "cookie" });
|
|
53
|
+
}
|
|
54
|
+
const refreshTokenCookie = useCookie("directus_refresh_token", {
|
|
55
|
+
default: () => null,
|
|
56
|
+
secure: process.env.NODE_ENV === "production",
|
|
57
|
+
sameSite: process.env.NODE_ENV === "production" ? "strict" : "lax",
|
|
58
|
+
httpOnly: false
|
|
59
|
+
});
|
|
60
|
+
refreshTokenCookie.value = null;
|
|
61
|
+
const dataCookie = useCookie("directus-data", {
|
|
62
|
+
default: () => null
|
|
63
|
+
});
|
|
64
|
+
dataCookie.value = null;
|
|
65
|
+
await $directusAuth.checkAuthStatus();
|
|
66
|
+
const afterLogoutPath = config.public.directus?.auth?.afterLogoutPath || "/login";
|
|
67
|
+
await navigateTo(afterLogoutPath);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error.value = e.message || "Logout failed";
|
|
70
|
+
const refreshTokenCookie = useCookie("directus_refresh_token");
|
|
71
|
+
refreshTokenCookie.value = null;
|
|
72
|
+
const dataCookie = useCookie("directus-data");
|
|
73
|
+
dataCookie.value = null;
|
|
74
|
+
await $directusAuth.checkAuthStatus();
|
|
75
|
+
const afterLogoutPath = config.public.directus?.auth?.afterLogoutPath || "/login";
|
|
76
|
+
await navigateTo(afterLogoutPath);
|
|
77
|
+
} finally {
|
|
78
|
+
isLoading.value = false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const register = async (data) => {
|
|
82
|
+
isLoading.value = true;
|
|
83
|
+
error.value = null;
|
|
84
|
+
try {
|
|
85
|
+
const apiUrl = config.public.directusUrl;
|
|
86
|
+
const publicClient = createDirectus(apiUrl).with(rest());
|
|
87
|
+
const baseUrl = config.public.directus?.auth?.verificationUrl || `${window.location.origin}/auth/verify-email`;
|
|
88
|
+
await publicClient.request(
|
|
89
|
+
registerUser(data.email, data.password, {
|
|
90
|
+
first_name: data.first_name,
|
|
91
|
+
last_name: data.last_name,
|
|
92
|
+
verification_url: baseUrl
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
error.value = e.message || "Registration failed";
|
|
98
|
+
throw e;
|
|
99
|
+
} finally {
|
|
100
|
+
isLoading.value = false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const verifyEmail = async (token) => {
|
|
104
|
+
isLoading.value = true;
|
|
105
|
+
error.value = null;
|
|
106
|
+
try {
|
|
107
|
+
const apiUrl = config.public.directusUrl;
|
|
108
|
+
const publicClient = createDirectus(apiUrl).with(rest());
|
|
109
|
+
await publicClient.request(registerUserVerify(token));
|
|
110
|
+
return true;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
error.value = e.message || "Email verification failed";
|
|
113
|
+
throw e;
|
|
114
|
+
} finally {
|
|
115
|
+
isLoading.value = false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const fetchUser = async () => {
|
|
119
|
+
isLoading.value = true;
|
|
120
|
+
error.value = null;
|
|
121
|
+
try {
|
|
122
|
+
await $directusAuth.checkAuthStatus();
|
|
123
|
+
return user.value;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
error.value = e.message || "Failed to fetch user";
|
|
126
|
+
throw e;
|
|
127
|
+
} finally {
|
|
128
|
+
isLoading.value = false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const requestPasswordReset = async (email) => {
|
|
132
|
+
isLoading.value = true;
|
|
133
|
+
error.value = null;
|
|
134
|
+
try {
|
|
135
|
+
const apiUrl = config.public.directusUrl;
|
|
136
|
+
const resetUrl = config.public.directus?.auth?.resetPasswordUrl || `${window.location.origin}/auth/reset-password`;
|
|
137
|
+
const publicClient = createDirectus(apiUrl).with(rest());
|
|
138
|
+
await publicClient.request(passwordRequest(email, resetUrl));
|
|
139
|
+
return true;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
error.value = e.message || "Password reset request failed";
|
|
142
|
+
throw e;
|
|
143
|
+
} finally {
|
|
144
|
+
isLoading.value = false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const resetPassword = async (token, password) => {
|
|
148
|
+
isLoading.value = true;
|
|
149
|
+
error.value = null;
|
|
150
|
+
try {
|
|
151
|
+
const apiUrl = config.public.directusUrl;
|
|
152
|
+
const publicClient = createDirectus(apiUrl).with(rest());
|
|
153
|
+
await publicClient.request(passwordReset(token, password));
|
|
154
|
+
return true;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
error.value = e.message || "Password reset failed";
|
|
157
|
+
throw e;
|
|
158
|
+
} finally {
|
|
159
|
+
isLoading.value = false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const refreshToken = async () => {
|
|
163
|
+
isLoading.value = true;
|
|
164
|
+
error.value = null;
|
|
165
|
+
try {
|
|
166
|
+
const client = $directus;
|
|
167
|
+
await client.request(refresh({ mode: "cookie" }));
|
|
168
|
+
await $directusAuth.checkAuthStatus();
|
|
169
|
+
return true;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
error.value = e.message || "Token refresh failed";
|
|
172
|
+
throw e;
|
|
173
|
+
} finally {
|
|
174
|
+
isLoading.value = false;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const userRoles = computed(() => {
|
|
178
|
+
if (!user.value) return [];
|
|
179
|
+
const permissionsConfig = config.public.directus?.auth?.permissions;
|
|
180
|
+
if (!permissionsConfig?.enabled) return [];
|
|
181
|
+
const roleField = permissionsConfig.field || "role";
|
|
182
|
+
let roleValue = user.value[roleField];
|
|
183
|
+
if (permissionsConfig.transform && typeof permissionsConfig.transform === "function") {
|
|
184
|
+
try {
|
|
185
|
+
roleValue = permissionsConfig.transform(roleValue, user.value);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error("[useDirectusAuth] Error in transform function:", e);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let roles;
|
|
191
|
+
if (Array.isArray(roleValue)) {
|
|
192
|
+
roles = roleValue;
|
|
193
|
+
} else if (roleValue != null) {
|
|
194
|
+
roles = [roleValue];
|
|
195
|
+
} else {
|
|
196
|
+
roles = [];
|
|
197
|
+
}
|
|
198
|
+
if (permissionsConfig.mapping) {
|
|
199
|
+
roles = roles.map((role) => permissionsConfig.mapping[role] || role);
|
|
200
|
+
}
|
|
201
|
+
return roles;
|
|
202
|
+
});
|
|
203
|
+
const userRole = computed(() => {
|
|
204
|
+
return userRoles.value[0] || null;
|
|
205
|
+
});
|
|
206
|
+
const userRoleRaw = computed(() => {
|
|
207
|
+
if (!user.value) return null;
|
|
208
|
+
const roleField = config.public.directus?.auth?.permissions?.field || "role";
|
|
209
|
+
return user.value[roleField];
|
|
210
|
+
});
|
|
211
|
+
const hasRole = (role) => {
|
|
212
|
+
return userRoles.value.includes(role);
|
|
213
|
+
};
|
|
214
|
+
const hasAnyRole = (roles) => {
|
|
215
|
+
return roles.some((role) => userRoles.value.includes(role));
|
|
216
|
+
};
|
|
217
|
+
const hasAllRoles = (roles) => {
|
|
218
|
+
return roles.every((role) => userRoles.value.includes(role));
|
|
219
|
+
};
|
|
220
|
+
const isNotRole = (roles) => {
|
|
221
|
+
return !roles.some((role) => userRoles.value.includes(role));
|
|
222
|
+
};
|
|
223
|
+
const updatePassword = async (currentPassword, newPassword) => {
|
|
224
|
+
isLoading.value = true;
|
|
225
|
+
error.value = null;
|
|
226
|
+
try {
|
|
227
|
+
const client = $directus;
|
|
228
|
+
await client.request(
|
|
229
|
+
updateMe({
|
|
230
|
+
password: newPassword
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
error.value = e.message || "Password update failed";
|
|
236
|
+
throw e;
|
|
237
|
+
} finally {
|
|
238
|
+
isLoading.value = false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const generateTwoFactorSecret = async (password) => {
|
|
242
|
+
isLoading.value = true;
|
|
243
|
+
error.value = null;
|
|
244
|
+
try {
|
|
245
|
+
const client = $directus;
|
|
246
|
+
const generateTFACommand = () => () => ({
|
|
247
|
+
path: `/users/me/tfa/generate`,
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: JSON.stringify({ password })
|
|
250
|
+
});
|
|
251
|
+
const result = await client.request(generateTFACommand());
|
|
252
|
+
return result;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
error.value = e.message || "Failed to generate 2FA secret";
|
|
255
|
+
throw e;
|
|
256
|
+
} finally {
|
|
257
|
+
isLoading.value = false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const enableTwoFactor = async (secret, otp) => {
|
|
261
|
+
isLoading.value = true;
|
|
262
|
+
error.value = null;
|
|
263
|
+
try {
|
|
264
|
+
const client = $directus;
|
|
265
|
+
const enableTFACommand = () => () => ({
|
|
266
|
+
path: `/users/me/tfa/enable`,
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: JSON.stringify({ secret, otp })
|
|
269
|
+
});
|
|
270
|
+
await client.request(enableTFACommand());
|
|
271
|
+
await $directusAuth.checkAuthStatus();
|
|
272
|
+
return true;
|
|
273
|
+
} catch (e) {
|
|
274
|
+
error.value = e.message || "Failed to enable 2FA";
|
|
275
|
+
throw e;
|
|
276
|
+
} finally {
|
|
277
|
+
isLoading.value = false;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const disableTwoFactor = async (otp) => {
|
|
281
|
+
isLoading.value = true;
|
|
282
|
+
error.value = null;
|
|
283
|
+
try {
|
|
284
|
+
const client = $directus;
|
|
285
|
+
const disableTFACommand = () => () => ({
|
|
286
|
+
path: `/users/me/tfa/disable`,
|
|
287
|
+
method: "POST",
|
|
288
|
+
body: JSON.stringify({ otp })
|
|
289
|
+
});
|
|
290
|
+
await client.request(disableTFACommand());
|
|
291
|
+
await $directusAuth.checkAuthStatus();
|
|
292
|
+
return true;
|
|
293
|
+
} catch (e) {
|
|
294
|
+
error.value = e.message || "Failed to disable 2FA";
|
|
295
|
+
throw e;
|
|
296
|
+
} finally {
|
|
297
|
+
isLoading.value = false;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const isTwoFactorEnabled = computed(() => {
|
|
301
|
+
return user.value?.tfa_secret !== null && user.value?.tfa_secret !== void 0;
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
user,
|
|
305
|
+
isAuthenticated,
|
|
306
|
+
isLoading,
|
|
307
|
+
error,
|
|
308
|
+
login,
|
|
309
|
+
logout,
|
|
310
|
+
register,
|
|
311
|
+
verifyEmail,
|
|
312
|
+
fetchUser,
|
|
313
|
+
requestPasswordReset,
|
|
314
|
+
resetPassword,
|
|
315
|
+
refreshToken,
|
|
316
|
+
userRoles,
|
|
317
|
+
userRole,
|
|
318
|
+
userRoleRaw,
|
|
319
|
+
hasRole,
|
|
320
|
+
hasAnyRole,
|
|
321
|
+
hasAllRoles,
|
|
322
|
+
isNotRole,
|
|
323
|
+
updatePassword,
|
|
324
|
+
generateTwoFactorSecret,
|
|
325
|
+
enableTwoFactor,
|
|
326
|
+
disableTwoFactor,
|
|
327
|
+
isTwoFactorEnabled
|
|
328
|
+
};
|
|
329
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ref, onUnmounted } from "vue";
|
|
2
|
+
import { useNuxtApp } from "#app";
|
|
3
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
4
|
+
export const useDirectusRealtime = () => {
|
|
5
|
+
const { $directus, $directusWs } = useNuxtApp();
|
|
6
|
+
const client = $directus;
|
|
7
|
+
const isConnected = computed(() => unref($directusWs.isConnected));
|
|
8
|
+
const localSubscriptions = ref(/* @__PURE__ */ new Set());
|
|
9
|
+
const subscribe = async (options, callback) => {
|
|
10
|
+
const { collection, query = {}, uid, persistent = false } = options;
|
|
11
|
+
const subscriptionId = uid || `${collection}-${JSON.stringify(query)}-${Math.random()}`;
|
|
12
|
+
if (subscriptions.has(subscriptionId)) {
|
|
13
|
+
console.warn(`[Directus Realtime] Subscription ${subscriptionId} already exists`);
|
|
14
|
+
return subscriptions.get(subscriptionId);
|
|
15
|
+
}
|
|
16
|
+
console.log(`[Directus Realtime] Subscribing to ${collection} with ID: ${subscriptionId}`);
|
|
17
|
+
try {
|
|
18
|
+
const { subscription, unsubscribe: unsubscribe2 } = await client.subscribe(collection, {
|
|
19
|
+
query,
|
|
20
|
+
uid: subscriptionId
|
|
21
|
+
});
|
|
22
|
+
(async () => {
|
|
23
|
+
try {
|
|
24
|
+
for await (const message of subscription) {
|
|
25
|
+
console.log(`[Directus Realtime] Event received for ${collection}:`, message);
|
|
26
|
+
callback(message);
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`[Directus Realtime] Subscription error for ${collection}:`, error);
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
const handler = {
|
|
33
|
+
uid: subscriptionId,
|
|
34
|
+
collection,
|
|
35
|
+
unsubscribe: unsubscribe2,
|
|
36
|
+
persistent
|
|
37
|
+
};
|
|
38
|
+
subscriptions.set(subscriptionId, handler);
|
|
39
|
+
if (!persistent) {
|
|
40
|
+
localSubscriptions.value.add(subscriptionId);
|
|
41
|
+
}
|
|
42
|
+
return handler;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`[Directus Realtime] Error subscribing to ${collection}:`, error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const unsubscribe = (subscriptionId) => {
|
|
49
|
+
const handler = subscriptions.get(subscriptionId);
|
|
50
|
+
if (!handler) {
|
|
51
|
+
console.warn(`[Directus Realtime] Subscription ${subscriptionId} not found`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log(`[Directus Realtime] Unsubscribing from ${handler.collection} (${subscriptionId})`);
|
|
55
|
+
try {
|
|
56
|
+
handler.unsubscribe();
|
|
57
|
+
subscriptions.delete(subscriptionId);
|
|
58
|
+
localSubscriptions.value.delete(subscriptionId);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`[Directus Realtime] Error unsubscribing from ${subscriptionId}:`, error);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const unsubscribeCollection = (collection) => {
|
|
64
|
+
const toRemove = [];
|
|
65
|
+
subscriptions.forEach((handler, id) => {
|
|
66
|
+
if (handler.collection === collection && !handler.persistent) {
|
|
67
|
+
toRemove.push(id);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
toRemove.forEach((id) => unsubscribe(id));
|
|
71
|
+
};
|
|
72
|
+
const unsubscribeAll = () => {
|
|
73
|
+
const toRemove = [];
|
|
74
|
+
subscriptions.forEach((handler, id) => {
|
|
75
|
+
if (!handler.persistent) {
|
|
76
|
+
toRemove.push(id);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
toRemove.forEach((id) => unsubscribe(id));
|
|
80
|
+
};
|
|
81
|
+
const getActiveSubscriptions = () => {
|
|
82
|
+
return Array.from(subscriptions.values());
|
|
83
|
+
};
|
|
84
|
+
const hasSubscription = (subscriptionId) => {
|
|
85
|
+
return subscriptions.has(subscriptionId);
|
|
86
|
+
};
|
|
87
|
+
const sendMessage = (message) => {
|
|
88
|
+
try {
|
|
89
|
+
client.sendMessage(message);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("[Directus Realtime] Error sending message:", error);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const reconnect = () => {
|
|
96
|
+
try {
|
|
97
|
+
$directusWs.initialize();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("[Directus Realtime] Error reconnecting:", error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
onUnmounted(() => {
|
|
104
|
+
localSubscriptions.value.forEach((id) => {
|
|
105
|
+
unsubscribe(id);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
// Connection state
|
|
110
|
+
isConnected,
|
|
111
|
+
// Subscription management
|
|
112
|
+
subscribe,
|
|
113
|
+
unsubscribe,
|
|
114
|
+
unsubscribeCollection,
|
|
115
|
+
unsubscribeAll,
|
|
116
|
+
// Subscription info
|
|
117
|
+
getActiveSubscriptions,
|
|
118
|
+
hasSubscription,
|
|
119
|
+
// WebSocket controls
|
|
120
|
+
sendMessage,
|
|
121
|
+
reconnect,
|
|
122
|
+
// Direct client access
|
|
123
|
+
client
|
|
124
|
+
};
|
|
125
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
|
+
import { createDirectus, rest, authentication, realtime, readMe } from "@directus/sdk";
|
|
3
|
+
class NuxtCookieStorage {
|
|
4
|
+
cookie = useCookie("directus-data", {
|
|
5
|
+
default: () => null,
|
|
6
|
+
secure: process.env.NODE_ENV === "production",
|
|
7
|
+
sameSite: process.env.NODE_ENV === "production" ? "strict" : "lax",
|
|
8
|
+
httpOnly: false
|
|
9
|
+
});
|
|
10
|
+
get() {
|
|
11
|
+
return this.cookie.value;
|
|
12
|
+
}
|
|
13
|
+
set(data) {
|
|
14
|
+
this.cookie.value = data;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export default defineNuxtPlugin(async () => {
|
|
18
|
+
const config = useRuntimeConfig();
|
|
19
|
+
const apiUrl = config.public.directusUrl;
|
|
20
|
+
if (!apiUrl) {
|
|
21
|
+
throw new Error("[Directus] NUXT_PUBLIC_DIRECTUS_URL is not configured. Please set it in your nuxt.config.ts runtimeConfig.public.directusUrl");
|
|
22
|
+
}
|
|
23
|
+
if (import.meta.client) {
|
|
24
|
+
console.log("[Directus] Using API URL:", apiUrl);
|
|
25
|
+
}
|
|
26
|
+
const wsBaseUrl = config.public.directusWsUrl || config.public.directusUrl;
|
|
27
|
+
const wsUrl = wsBaseUrl.replace(/^http/, "ws") + "/websocket";
|
|
28
|
+
if (import.meta.client) {
|
|
29
|
+
console.log("[Directus] WebSocket URL:", wsUrl);
|
|
30
|
+
}
|
|
31
|
+
const storage = new NuxtCookieStorage();
|
|
32
|
+
const directusClient = createDirectus(apiUrl).with(authentication("cookie", { credentials: "include", storage })).with(rest({ credentials: "include" })).with(
|
|
33
|
+
realtime({
|
|
34
|
+
url: wsUrl,
|
|
35
|
+
debug: process.env.NODE_ENV === "development"
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
const isAuthenticatedState = useState("directus-authenticated", () => false);
|
|
39
|
+
const currentUser = useState("directus-user", () => null);
|
|
40
|
+
const isWebSocketConnected = useState("directus-ws-connected", () => false);
|
|
41
|
+
const initializeWebSocket = () => {
|
|
42
|
+
if (!import.meta.client) return;
|
|
43
|
+
try {
|
|
44
|
+
directusClient.connect();
|
|
45
|
+
directusClient.onWebSocket("open", () => {
|
|
46
|
+
console.log("[Directus] WebSocket connection established");
|
|
47
|
+
isWebSocketConnected.value = true;
|
|
48
|
+
});
|
|
49
|
+
directusClient.onWebSocket("close", () => {
|
|
50
|
+
console.log("[Directus] WebSocket connection closed");
|
|
51
|
+
isWebSocketConnected.value = false;
|
|
52
|
+
});
|
|
53
|
+
directusClient.onWebSocket("error", (error) => {
|
|
54
|
+
console.error("[Directus] WebSocket error:", error);
|
|
55
|
+
isWebSocketConnected.value = false;
|
|
56
|
+
});
|
|
57
|
+
directusClient.onWebSocket("message", (message) => {
|
|
58
|
+
console.log("[Directus] WebSocket message:", message);
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error("[Directus] WebSocket connection failed:", error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const checkAuthStatus = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const me = await directusClient.request(readMe());
|
|
67
|
+
isAuthenticatedState.value = true;
|
|
68
|
+
currentUser.value = me;
|
|
69
|
+
if (!isWebSocketConnected.value) {
|
|
70
|
+
initializeWebSocket();
|
|
71
|
+
}
|
|
72
|
+
return me;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
isAuthenticatedState.value = false;
|
|
75
|
+
currentUser.value = null;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const getRefreshToken = () => {
|
|
80
|
+
const refreshToken = useCookie("directus_refresh_token", {
|
|
81
|
+
default: () => null,
|
|
82
|
+
secure: process.env.NODE_ENV === "production",
|
|
83
|
+
sameSite: process.env.NODE_ENV === "production" ? "strict" : "lax",
|
|
84
|
+
httpOnly: false
|
|
85
|
+
});
|
|
86
|
+
return refreshToken.value;
|
|
87
|
+
};
|
|
88
|
+
if (import.meta.client) {
|
|
89
|
+
await checkAuthStatus();
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
provide: {
|
|
93
|
+
directus: directusClient,
|
|
94
|
+
directusAuth: {
|
|
95
|
+
isAuthenticated: readonly(isAuthenticatedState),
|
|
96
|
+
currentUser: readonly(currentUser),
|
|
97
|
+
checkAuthStatus,
|
|
98
|
+
getRefreshToken
|
|
99
|
+
},
|
|
100
|
+
directusWs: {
|
|
101
|
+
isConnected: readonly(isWebSocketConnected),
|
|
102
|
+
initialize: initializeWebSocket
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig, useCookie } from "#app";
|
|
2
|
+
import { useDirectusAuth } from "../composables/useDirectusAuth.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware(async (to, from) => {
|
|
4
|
+
const { isAuthenticated, fetchUser } = useDirectusAuth();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const { loginPath, registerPath, afterLoginPath } = config.public.directus?.auth || {
|
|
7
|
+
loginPath: "/login",
|
|
8
|
+
registerPath: "/register",
|
|
9
|
+
afterLoginPath: "/"
|
|
10
|
+
};
|
|
11
|
+
const enableGlobalMiddleware = config.public.directus?.enableGlobalMiddleware ?? true;
|
|
12
|
+
console.log("[directus-auth middleware] from", from.fullPath, "to", to.fullPath);
|
|
13
|
+
const authMetaRaw = to.meta?.auth;
|
|
14
|
+
let authMeta;
|
|
15
|
+
let requiresAuth;
|
|
16
|
+
if (authMetaRaw === false) {
|
|
17
|
+
requiresAuth = false;
|
|
18
|
+
} else if (authMetaRaw === true) {
|
|
19
|
+
requiresAuth = true;
|
|
20
|
+
authMeta = {};
|
|
21
|
+
} else if (typeof authMetaRaw === "object" && authMetaRaw !== null) {
|
|
22
|
+
requiresAuth = true;
|
|
23
|
+
authMeta = authMetaRaw;
|
|
24
|
+
} else {
|
|
25
|
+
requiresAuth = enableGlobalMiddleware;
|
|
26
|
+
}
|
|
27
|
+
if ([loginPath, registerPath].some((p) => to.path.startsWith(p))) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!requiresAuth) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
if (!isAuthenticated.value) {
|
|
35
|
+
try {
|
|
36
|
+
await fetchUser();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const authenticated = isAuthenticated.value;
|
|
41
|
+
if (!authenticated) {
|
|
42
|
+
if (authMeta?.unauthenticatedOnly) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const redirectCookie = useCookie("directus-auth-redirect", {
|
|
46
|
+
default: () => "",
|
|
47
|
+
maxAge: 60 * 10
|
|
48
|
+
// 10 minutes
|
|
49
|
+
});
|
|
50
|
+
if (!to.path.startsWith(loginPath) && !to.path.startsWith(registerPath)) {
|
|
51
|
+
redirectCookie.value = to.fullPath;
|
|
52
|
+
}
|
|
53
|
+
const redirectTo = authMeta?.navigateUnauthenticatedTo || loginPath;
|
|
54
|
+
return navigateTo(redirectTo);
|
|
55
|
+
}
|
|
56
|
+
if (authenticated) {
|
|
57
|
+
if (authMeta?.unauthenticatedOnly) {
|
|
58
|
+
const redirectCookie = useCookie("directus-auth-redirect", {
|
|
59
|
+
default: () => ""
|
|
60
|
+
});
|
|
61
|
+
let redirectTo = afterLoginPath;
|
|
62
|
+
if (authMeta?.navigateAuthenticatedTo) {
|
|
63
|
+
redirectTo = authMeta.navigateAuthenticatedTo;
|
|
64
|
+
}
|
|
65
|
+
if (redirectCookie.value && (from.path.startsWith(loginPath) || from.path.startsWith(registerPath))) {
|
|
66
|
+
redirectTo = redirectCookie.value;
|
|
67
|
+
redirectCookie.value = "";
|
|
68
|
+
}
|
|
69
|
+
return navigateTo(redirectTo);
|
|
70
|
+
}
|
|
71
|
+
const permissionsConfig = config.public.directus?.auth?.permissions;
|
|
72
|
+
if (permissionsConfig?.enabled && (authMeta?.roles || authMeta?.excludeRoles)) {
|
|
73
|
+
const currentUser = isAuthenticated.value ? await fetchUser() : null;
|
|
74
|
+
if (currentUser) {
|
|
75
|
+
const roleField = permissionsConfig.field || "role";
|
|
76
|
+
let userRoleValue = currentUser[roleField];
|
|
77
|
+
if (permissionsConfig.transform && typeof permissionsConfig.transform === "function") {
|
|
78
|
+
try {
|
|
79
|
+
userRoleValue = permissionsConfig.transform(userRoleValue, currentUser);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error("[directus-auth middleware] Error in transform function:", e);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let userRoles;
|
|
85
|
+
if (Array.isArray(userRoleValue)) {
|
|
86
|
+
userRoles = userRoleValue;
|
|
87
|
+
} else if (userRoleValue != null) {
|
|
88
|
+
userRoles = [userRoleValue];
|
|
89
|
+
} else {
|
|
90
|
+
userRoles = [];
|
|
91
|
+
}
|
|
92
|
+
if (permissionsConfig.mapping) {
|
|
93
|
+
userRoles = userRoles.map((role) => permissionsConfig.mapping[role] || role);
|
|
94
|
+
}
|
|
95
|
+
if (authMeta.roles && authMeta.roles.length > 0) {
|
|
96
|
+
const hasPermission = authMeta.roles.some((requiredRole) => userRoles.includes(requiredRole));
|
|
97
|
+
if (!hasPermission) {
|
|
98
|
+
console.warn(`[directus-auth middleware] User roles [${userRoles.join(", ")}] don't match required roles:`, authMeta.roles);
|
|
99
|
+
const unauthorizedRedirect = authMeta.unauthorizedRedirect || "/";
|
|
100
|
+
return navigateTo(unauthorizedRedirect);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (authMeta.excludeRoles && authMeta.excludeRoles.length > 0) {
|
|
104
|
+
const isExcluded = userRoles.some((role) => authMeta.excludeRoles.includes(role));
|
|
105
|
+
if (isExcluded) {
|
|
106
|
+
console.warn(`[directus-auth middleware] User has excluded role in [${userRoles.join(", ")}]:`, authMeta.excludeRoles);
|
|
107
|
+
const unauthorizedRedirect = authMeta.unauthorizedRedirect || "/";
|
|
108
|
+
return navigateTo(unauthorizedRedirect);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("[directus-auth middleware] Error checking authentication:", error);
|
|
116
|
+
const redirectCookie = useCookie("directus-auth-redirect", {
|
|
117
|
+
default: () => "",
|
|
118
|
+
maxAge: 60 * 10
|
|
119
|
+
// 10 minutes
|
|
120
|
+
});
|
|
121
|
+
if (!to.path.startsWith(loginPath) && !to.path.startsWith(registerPath)) {
|
|
122
|
+
redirectCookie.value = to.fullPath;
|
|
123
|
+
}
|
|
124
|
+
return navigateTo(loginPath);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
});
|
|
File without changes
|
|
File without changes
|
package/dist/types.d.mts
ADDED
package/dist/types.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@michael-nussbaumer/nuxt-directus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nuxt 4 module for Directus SDK with auth and TypeScript type generation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/module.d.ts",
|
|
10
|
+
"import": "./dist/module.mjs",
|
|
11
|
+
"require": "./dist/module.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/module.cjs",
|
|
15
|
+
"types": "./dist/module.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "pnpm run generate:types && nuxi dev playground",
|
|
21
|
+
"build": "nuxt-module-build build",
|
|
22
|
+
"prepack": "pnpm run build",
|
|
23
|
+
"generate:types": "cross-env NODE_ENV=development tsx scripts/generate-directus-types.ts",
|
|
24
|
+
"lint": "eslint .",
|
|
25
|
+
"release": "pnpm run build && npm publish"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@nuxt/kit": "^3.15.3",
|
|
29
|
+
"defu": "^6.1.4"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@directus/sdk": "^21.0.0",
|
|
33
|
+
"@nuxt/module-builder": "^0.8.4",
|
|
34
|
+
"@nuxt/schema": "^3.15.3",
|
|
35
|
+
"@types/node": "^22.10.5",
|
|
36
|
+
"cross-env": "^7.0.3",
|
|
37
|
+
"execa": "^9.5.2",
|
|
38
|
+
"nuxt": "^3.15.3",
|
|
39
|
+
"openapi-typescript": "^7.4.4",
|
|
40
|
+
"tsx": "^4.19.2",
|
|
41
|
+
"typescript": "^5.7.3",
|
|
42
|
+
"unbuild": "^2.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@directus/sdk": ">=17.0.0"
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "pnpm@9.15.4",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/Michael-Nussbaumer/nuxt-directus-module.git"
|
|
54
|
+
}
|
|
55
|
+
}
|