@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 ADDED
@@ -0,0 +1,222 @@
1
+ # @michael-nussbaumer/nuxt-directus
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@Michael-Nussbaumer/nuxt-directus.svg)](https://www.npmjs.com/package/@michael-nussbaumer/nuxt-directus)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -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 };
@@ -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 };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@michael-nussbaumer/nuxt-directus",
3
+ "configKey": "directus",
4
+ "compatibility": {
5
+ "nuxt": "^3.0.0 || ^4.0.0"
6
+ },
7
+ "version": "0.1.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "2.0.0"
11
+ }
12
+ }
@@ -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
+ };
@@ -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
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
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
+ }