@habityzer/nuxt-symfony-kinde-layer 1.0.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.
@@ -0,0 +1,37 @@
1
+ {
2
+ "branches": ["master"],
3
+ "plugins": [
4
+ ["@semantic-release/commit-analyzer", {
5
+ "preset": "conventionalcommits",
6
+ "releaseRules": [
7
+ {"type": "feat", "release": "minor"},
8
+ {"type": "fix", "release": "patch"},
9
+ {"type": "docs", "release": "patch"},
10
+ {"type": "style", "release": "patch"},
11
+ {"type": "refactor", "release": "patch"},
12
+ {"type": "perf", "release": "patch"},
13
+ {"type": "test", "release": "patch"},
14
+ {"type": "build", "release": "patch"},
15
+ {"type": "ci", "release": "patch"},
16
+ {"type": "chore", "scope": "deps", "release": "patch"},
17
+ {"type": "chore", "scope": "release", "release": "patch"},
18
+ {"scope": "breaking", "release": "major"},
19
+ {"type": "BREAKING CHANGE", "release": "major"}
20
+ ]
21
+ }],
22
+ "@semantic-release/release-notes-generator",
23
+ ["@semantic-release/changelog", {
24
+ "changelogFile": "CHANGELOG.md"
25
+ }],
26
+ ["@semantic-release/npm", {
27
+ "npmPublish": false
28
+ }],
29
+ ["@semantic-release/git", {
30
+ "assets": ["package.json", "CHANGELOG.md"],
31
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
32
+ "gitArgs": ["--no-verify"]
33
+ }]
34
+ ],
35
+ "dryRun": false,
36
+ "ci": false
37
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "cSpell.words": [
3
+ "habityzer",
4
+ "iconify",
5
+ "kinde",
6
+ "nuxi",
7
+ "symfony",
8
+ "vueuse"
9
+ ]
10
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # 1.0.0 (2025-10-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * Update semantic-release to version 24.2.9 and add it to package.json for improved release management ([76270ec](https://github.com/Habityzer/nuxt-symfony-kinde-layer/commit/76270ecc4f42276cf9d0547095bf75988c196441))
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # @habityzer/nuxt-symfony-kinde-layer
2
+
3
+ Shared Nuxt layer for Symfony + Kinde authentication integration. This layer provides common authentication logic, API proxying, and pre-configured modules for Nuxt projects using Symfony backends with Kinde authentication.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Symfony API Proxy** - Automatically forwards requests with Kinde auth tokens
8
+ - ✅ **Accept Header Fix** - Properly forwards Accept header for content negotiation (JSON vs Hydra)
9
+ - ✅ **Auth Composable** - Unified authentication state management
10
+ - ✅ **Global Auth Middleware** - Configurable route protection
11
+ - ✅ **E2E Testing Support** - Built-in support for automated testing
12
+ - ✅ **Pre-configured Modules** - Includes @nuxt/ui, @nuxt/image, Pinia, ESLint, and more
13
+ - ✅ **Type-safe** - Full TypeScript support with OpenAPI integration
14
+
15
+ ## Installation
16
+
17
+ ### Using pnpm workspace (recommended for monorepos)
18
+
19
+ ```bash
20
+ # In your project root
21
+ pnpm add @habityzer/nuxt-symfony-kinde-layer
22
+ ```
23
+
24
+ ### Or link locally
25
+
26
+ ```bash
27
+ cd /path/to/@habityzer/nuxt-symfony-kinde-layer
28
+ pnpm install
29
+ pnpm link --global
30
+
31
+ cd /path/to/your-project
32
+ pnpm link --global @habityzer/nuxt-symfony-kinde-layer
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### 1. Extend the layer in your `nuxt.config.ts`
38
+
39
+ ```typescript
40
+ export default defineNuxtConfig({
41
+ extends: ['@habityzer/nuxt-symfony-kinde-layer'],
42
+
43
+ // Runtime config - expose auth settings for middleware
44
+ runtimeConfig: {
45
+ apiBaseUrl: process.env.API_BASE_URL,
46
+
47
+ public: {
48
+ apiBaseUrl: process.env.API_BASE_URL,
49
+
50
+ // IMPORTANT: Expose auth config for middleware (must match kindeAuth below)
51
+ kindeAuth: {
52
+ cookie: {
53
+ prefix: 'myapp_' // Must match prefix in kindeAuth
54
+ },
55
+ middleware: {
56
+ publicRoutes: ['/', '/blog', '/help']
57
+ }
58
+ }
59
+ }
60
+ },
61
+
62
+ // Configure Kinde authentication module
63
+ kindeAuth: {
64
+ authDomain: process.env.NUXT_KINDE_AUTH_DOMAIN,
65
+ clientId: process.env.NUXT_KINDE_CLIENT_ID,
66
+ clientSecret: process.env.NUXT_KINDE_CLIENT_SECRET,
67
+ redirectURL: process.env.NUXT_KINDE_REDIRECT_URL,
68
+ logoutRedirectURL: process.env.NUXT_KINDE_LOGOUT_REDIRECT_URL,
69
+ postLoginRedirectURL: '/dashboard',
70
+ cookie: {
71
+ prefix: 'myapp_' // IMPORTANT: Must be unique per project to avoid cookie conflicts
72
+ },
73
+ middleware: {
74
+ publicRoutes: ['/', '/blog', '/help'] // Must match publicRoutes in runtimeConfig.public
75
+ }
76
+ }
77
+ })
78
+ ```
79
+
80
+ ### 2. Environment Variables
81
+
82
+ Create a `.env` file:
83
+
84
+ ```bash
85
+ # Symfony Backend
86
+ API_BASE_URL=http://localhost:8000
87
+
88
+ # Kinde Authentication
89
+ NUXT_KINDE_AUTH_DOMAIN=https://your-domain.kinde.com
90
+ NUXT_KINDE_CLIENT_ID=your-client-id
91
+ NUXT_KINDE_CLIENT_SECRET=your-client-secret
92
+ NUXT_KINDE_REDIRECT_URL=http://localhost:3000/api/kinde/callback
93
+ NUXT_KINDE_LOGOUT_REDIRECT_URL=http://localhost:3000
94
+ NUXT_KINDE_POST_LOGIN_REDIRECT_URL=/dashboard
95
+ ```
96
+
97
+ ### 3. Use the Auth Composable
98
+
99
+ ```vue
100
+ <script setup lang="ts">
101
+ const {
102
+ isAuthenticated,
103
+ currentUser,
104
+ userDisplayName,
105
+ userEmail,
106
+ isPremium,
107
+ login,
108
+ logout,
109
+ fetchUserProfile
110
+ } = useAuth()
111
+
112
+ // Fetch user profile on mount
113
+ onMounted(async () => {
114
+ if (isAuthenticated.value) {
115
+ await fetchUserProfile()
116
+ }
117
+ })
118
+ </script>
119
+
120
+ <template>
121
+ <div>
122
+ <template v-if="isAuthenticated">
123
+ <p>Welcome, {{ userDisplayName }}!</p>
124
+ <p v-if="isPremium">Premium user</p>
125
+ <button @click="logout">Logout</button>
126
+ </template>
127
+ <template v-else>
128
+ <button @click="login">Login</button>
129
+ </template>
130
+ </div>
131
+ </template>
132
+ ```
133
+
134
+ ### 4. Call Symfony APIs
135
+
136
+ The layer automatically proxies requests to `/api/symfony/*`:
137
+
138
+ ```typescript
139
+ // This calls your Symfony backend at /api/users
140
+ const users = await $fetch('/api/symfony/api/users')
141
+
142
+ // With generated OpenAPI composables
143
+ const { getUsersApi } = useUsersApi()
144
+ const response = await getUsersApi()
145
+ ```
146
+
147
+ ## What's Included
148
+
149
+ ### Modules
150
+
151
+ - `@nuxt/ui` - UI component library
152
+ - `@nuxt/image` - Image optimization
153
+ - `@nuxt/eslint` - Linting
154
+ - `@pinia/nuxt` - State management
155
+ - `@habityzer/nuxt-kinde-auth` - Kinde authentication
156
+ - `@vueuse/core` - Vue composition utilities
157
+
158
+ ### Files
159
+
160
+ - `server/api/symfony/[...].ts` - Symfony API proxy with auth
161
+ - `app/composables/useAuth.ts` - Authentication composable
162
+ - `app/constants/auth.ts` - Auth constants
163
+ - `app/middleware/auth.global.ts` - Global route protection
164
+
165
+ ## Configuration Options
166
+
167
+ ### Cookie Prefix
168
+
169
+ **CRITICAL:** Always set a unique cookie prefix per project to avoid cookie conflicts when running multiple projects locally:
170
+
171
+ ```typescript
172
+ // MUST be set in BOTH places:
173
+ runtimeConfig: {
174
+ public: {
175
+ kindeAuth: {
176
+ cookie: {
177
+ prefix: 'myproject_' // For middleware to read
178
+ }
179
+ }
180
+ }
181
+ },
182
+
183
+ kindeAuth: {
184
+ cookie: {
185
+ prefix: 'myproject_' // For Kinde module (must match above)
186
+ }
187
+ }
188
+ ```
189
+
190
+ **Why both?**
191
+ - `kindeAuth.cookie.prefix` - Used by the Kinde auth module to set/read cookies
192
+ - `runtimeConfig.public.kindeAuth.cookie.prefix` - Used by the layer's middleware to check authentication
193
+
194
+ **Without unique prefixes:** If you run `ew-nuxt` and `habityzer-nuxt` locally at the same time, they'll share cookies and cause auth conflicts!
195
+
196
+ ### Public Routes
197
+
198
+ Configure which routes don't require authentication:
199
+
200
+ ```typescript
201
+ kindeAuth: {
202
+ middleware: {
203
+ publicRoutes: [
204
+ '/',
205
+ '/blog',
206
+ '/about',
207
+ '/legal'
208
+ ]
209
+ }
210
+ }
211
+ ```
212
+
213
+ ## E2E Testing
214
+
215
+ The layer supports E2E testing with app tokens:
216
+
217
+ 1. Generate an app token in Symfony:
218
+ ```bash
219
+ php bin/console app:token:manage create
220
+ ```
221
+
222
+ 2. Set the token in your E2E tests:
223
+ ```typescript
224
+ // Set cookie
225
+ await page.context().addCookies([{
226
+ name: 'kinde_token',
227
+ value: 'app_your_token_here',
228
+ domain: 'localhost',
229
+ path: '/'
230
+ }])
231
+ ```
232
+
233
+ ## API Schema Generation
234
+
235
+ The layer includes OpenAPI tools for generating typed API composables:
236
+
237
+ ```bash
238
+ # Generate TypeScript types from OpenAPI schema
239
+ pnpm generate:types
240
+
241
+ # Generate API composables
242
+ pnpm generate:api
243
+
244
+ # Or do both
245
+ pnpm sync:api
246
+ ```
247
+
248
+ Add these scripts to your project's `package.json`:
249
+
250
+ ```json
251
+ {
252
+ "scripts": {
253
+ "generate:types": "openapi-typescript ./schema/api.json -o ./app/types/api.ts --default-non-nullable false && eslint ./app/types/api.ts --fix",
254
+ "generate:api": "pnpm generate:types && nuxt-openapi-composables generate -s ./schema/api.json -o ./app/composables/api --types-import '~/types/api'",
255
+ "sync:api": "pnpm update:schema && pnpm generate:api"
256
+ }
257
+ }
258
+ ```
259
+
260
+ ## Troubleshooting
261
+
262
+ ### Cookie Name Conflicts
263
+
264
+ If you see authentication issues, ensure each project has a unique cookie prefix:
265
+
266
+ ```typescript
267
+ // Project A
268
+ kindeAuth: { cookie: { prefix: 'projecta_' } }
269
+
270
+ // Project B
271
+ kindeAuth: { cookie: { prefix: 'projectb_' } }
272
+ ```
273
+
274
+ ### TypeScript Errors with API Responses
275
+
276
+ If you get type mismatches between expected Hydra collections and plain arrays, the proxy is correctly forwarding the Accept header. Make sure your OpenAPI schema matches what the API actually returns.
277
+
278
+ ## Architecture & Design Decisions
279
+
280
+ ### Why Constants Are Defined Inline in Server Code
281
+
282
+ You'll notice that auth constants (`E2E_TOKEN_COOKIE_NAME`, `APP_TOKEN_PREFIX`, `KINDE_ID_TOKEN_COOKIE_NAME`) are defined directly in the server files (`server/api/symfony/[...].ts`) rather than imported from a shared constants file.
283
+
284
+ **Reason**: Nitro's bundling process for server-side code doesn't support:
285
+ - App aliases like `~` or `@` (these resolve to the consuming project's app directory, not the layer's)
286
+ - Relative imports from external layers during the rollup bundling phase
287
+ - The `#build` alias for accessing layer exports
288
+
289
+ **Solution**: We define these constants inline in server files while maintaining the shared `app/constants/auth.ts` for client-side code. This is a deliberate architectural choice to ensure reliable builds across all consuming projects.
290
+
291
+ ### Cookie Prefix Configuration
292
+
293
+ The layer uses a project-specific cookie prefix (e.g., `ew-`, `habityzer_`) to prevent cookie conflicts when running multiple projects locally.
294
+
295
+ **Implementation**:
296
+ 1. Base cookie names are defined without prefixes (`id_token`, `access_token`)
297
+ 2. Projects configure their prefix in `nuxt.config.ts`
298
+ 3. The prefix is applied dynamically at runtime by middleware and composables
299
+ 4. Projects should NOT redefine the cookie constant names - they inherit from the layer
300
+
301
+ ### TypeScript Type Suppressions
302
+
303
+ You may see `@ts-expect-error` comments for the `cookie` property in configuration files. This is expected and safe.
304
+
305
+ **Reason**: The `@habityzer/nuxt-kinde-auth` module's TypeScript definitions don't include our custom `cookie.prefix` configuration property, but it works correctly at runtime.
306
+
307
+ **Solution**: We use `@ts-expect-error` comments to suppress TypeScript errors without compromising type safety elsewhere in the codebase.
308
+
309
+ ### Cache Clearing
310
+
311
+ If you encounter auto-import issues after updating the layer (especially for composables), clear your Nuxt cache:
312
+
313
+ ```bash
314
+ rm -rf .nuxt node_modules/.cache
315
+ pnpm build
316
+ ```
317
+
318
+ This forces Nuxt to regenerate its auto-import registry and pick up changes from the layer.
319
+
320
+ ## License
321
+
322
+ MIT
323
+
324
+ ## Contributing
325
+
326
+ This is a private layer for Habityzer projects. For issues or improvements, contact the team.
@@ -0,0 +1,139 @@
1
+ import { computed, ref, readonly } from 'vue'
2
+ import { E2E_TOKEN_COOKIE_NAME } from '../constants/auth'
3
+
4
+ interface SymfonyUser {
5
+ id: number
6
+ email: string
7
+ name: string
8
+ picture?: string | null
9
+ subscription_tier?: 'free' | 'pro' | 'teams' | 'enterprise'
10
+ tier?: 'free' | 'pro' | 'teams' | 'enterprise' // Handle potential API variation
11
+ is_premium: boolean
12
+ roles?: string[]
13
+ kinde_id: string
14
+ google_id?: string | null
15
+ created_at: string
16
+ updated_at: string
17
+ }
18
+
19
+ // Singleton state - shared across all useAuth() calls
20
+ const userProfile = ref<SymfonyUser | null>(null)
21
+ const isLoading = ref(false)
22
+
23
+ export const useAuth = () => {
24
+ // Use the base Kinde auth composable from the module
25
+ const kindeAuth = useKindeAuth()
26
+
27
+ // Primary user data (from Symfony API)
28
+ const currentUser = computed(() => userProfile.value)
29
+
30
+ // Get user display name
31
+ const userDisplayName = computed(() => {
32
+ if (userProfile.value?.name) return userProfile.value.name
33
+ if (userProfile.value?.email) return userProfile.value.email
34
+ return 'User'
35
+ })
36
+
37
+ // Get user email
38
+ const userEmail = computed(() => {
39
+ return userProfile.value?.email || null
40
+ })
41
+
42
+ // Get user picture URL
43
+ const userPicture = computed(() => {
44
+ return userProfile.value?.picture || null
45
+ })
46
+
47
+ // Get user tier/subscription info
48
+ const userTier = computed(() => {
49
+ return userProfile.value?.subscription_tier || userProfile.value?.tier || 'free'
50
+ })
51
+
52
+ const isPremium = computed(() => {
53
+ return userProfile.value?.is_premium || false
54
+ })
55
+
56
+ // Use login from Kinde module
57
+ const login = kindeAuth.login
58
+
59
+ // Fetch user profile from Symfony API via Nuxt proxy
60
+ const fetchUserProfile = async (): Promise<SymfonyUser | null> => {
61
+ // Prevent multiple simultaneous calls
62
+ if (isLoading.value) {
63
+ return userProfile.value
64
+ }
65
+
66
+ // If we already have a valid profile, return it
67
+ if (userProfile.value) {
68
+ return userProfile.value
69
+ }
70
+
71
+ try {
72
+ isLoading.value = true
73
+
74
+ // Call Symfony API via Nuxt proxy (include /api/ prefix for Symfony routes)
75
+ const response = await $fetch<SymfonyUser>('/api/symfony/api/authentication', {
76
+ method: 'GET',
77
+ // Add retry and error handling options
78
+ retry: 0, // Don't retry on failure
79
+ onResponseError({ response }) {
80
+ // Don't throw on 401 - just log it
81
+ if (response.status === 401) {
82
+ console.warn('User not authenticated in Symfony')
83
+ }
84
+ }
85
+ })
86
+
87
+ userProfile.value = response
88
+ return response
89
+ } catch (error) {
90
+ // Silently handle auth errors on public pages
91
+ if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 401) {
92
+ console.debug('Auth check failed - user not logged in')
93
+ } else {
94
+ console.error('Failed to fetch user profile:', error)
95
+ }
96
+ userProfile.value = null
97
+ return null
98
+ } finally {
99
+ isLoading.value = false
100
+ }
101
+ }
102
+
103
+ // Logout - clear Symfony profile and use Kinde logout
104
+ const logout = () => {
105
+ // Clear local Symfony state
106
+ userProfile.value = null
107
+
108
+ // Clear E2E test token if exists
109
+ if (import.meta.client) {
110
+ localStorage.removeItem('e2e_app_token')
111
+ document.cookie = `${E2E_TOKEN_COOKIE_NAME}=; path=/; max-age=0`
112
+ }
113
+
114
+ // Use Kinde module logout
115
+ kindeAuth.logout()
116
+ }
117
+
118
+ return {
119
+ // Core auth state (from Kinde module)
120
+ isAuthenticated: kindeAuth.isAuthenticated,
121
+ isLoading: readonly(isLoading),
122
+
123
+ // Symfony user data
124
+ currentUser: readonly(currentUser),
125
+ userProfile: readonly(userProfile),
126
+
127
+ // User info
128
+ userDisplayName: readonly(userDisplayName),
129
+ userEmail: readonly(userEmail),
130
+ userPicture: readonly(userPicture),
131
+ userTier: readonly(userTier),
132
+ isPremium: readonly(isPremium),
133
+
134
+ // Authentication methods
135
+ login,
136
+ logout,
137
+ fetchUserProfile
138
+ }
139
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Authentication constants shared across the application
3
+ */
4
+
5
+ /**
6
+ * Cookie name for E2E test authentication token
7
+ * Used by automated tests to bypass Kinde OAuth flow
8
+ */
9
+ export const E2E_TOKEN_COOKIE_NAME = 'kinde_token'
10
+
11
+ /**
12
+ * Prefix for Symfony app tokens (used in E2E tests)
13
+ * These are long-lived tokens generated by `php bin/console app:token:manage create`
14
+ */
15
+ export const APP_TOKEN_PREFIX = 'app_'
16
+
17
+ /**
18
+ * Kinde authentication cookie names
19
+ * These cookies are managed by the @habityzer/nuxt-kinde-auth module
20
+ * The prefix is configured per-project in nuxt.config.ts
21
+ *
22
+ * Note: These constants use placeholder names. The actual cookie names
23
+ * will have the project-specific prefix (e.g., 'ew-id_token', 'habityzer_id_token')
24
+ */
25
+ export const KINDE_ID_TOKEN_COOKIE_NAME = 'id_token'
26
+ export const KINDE_ACCESS_TOKEN_COOKIE_NAME = 'access_token'
@@ -0,0 +1,82 @@
1
+ import { E2E_TOKEN_COOKIE_NAME, KINDE_ID_TOKEN_COOKIE_NAME, KINDE_ACCESS_TOKEN_COOKIE_NAME } from '../constants/auth'
2
+
3
+ /**
4
+ * Global auth middleware - checks authentication on route navigation
5
+ * Redirects to login if accessing protected routes without authentication
6
+ *
7
+ * Note: This middleware works alongside the nuxt-kinde-auth module.
8
+ * It handles E2E testing tokens and uses the module's login endpoints.
9
+ *
10
+ * Projects should configure publicRoutes in their nuxt.config.ts:
11
+ * kindeAuth: {
12
+ * middleware: {
13
+ * publicRoutes: ['/', '/blog', '/help']
14
+ * }
15
+ * }
16
+ */
17
+ export default defineNuxtRouteMiddleware(async (to) => {
18
+ // Get public routes from runtime config (configured per-project)
19
+ const config = useRuntimeConfig()
20
+ const kindeConfig = config.public.kindeAuth || {}
21
+ const publicRoutes: string[] = kindeConfig.middleware?.publicRoutes || ['/']
22
+
23
+ // Check if the route is public or a child of public routes
24
+ const isPublicRoute = publicRoutes.some(route =>
25
+ to.path === route || to.path.startsWith(`${route}/`)
26
+ )
27
+
28
+ // If it's a public route, allow access
29
+ if (isPublicRoute) {
30
+ return
31
+ }
32
+
33
+ // For protected routes, check authentication
34
+ if (import.meta.server) {
35
+ // Server-side: Check for auth cookies using Nuxt's useCookie
36
+ // Note: Cookie names include the project-specific prefix
37
+ const config = useRuntimeConfig()
38
+ // @ts-expect-error - cookie property exists in runtime config but not in Kinde module types
39
+ const cookiePrefix = config.public.kindeAuth?.cookie?.prefix || 'app_'
40
+
41
+ const idTokenName = `${cookiePrefix}${KINDE_ID_TOKEN_COOKIE_NAME}`
42
+ const accessTokenName = `${cookiePrefix}${KINDE_ACCESS_TOKEN_COOKIE_NAME}`
43
+
44
+ const idToken = useCookie(idTokenName)
45
+ const accessToken = useCookie(accessTokenName)
46
+ const e2eToken = useCookie(E2E_TOKEN_COOKIE_NAME) // E2E test token
47
+
48
+ // Allow access if any valid auth token exists
49
+ if (!idToken.value && !accessToken.value && !e2eToken.value) {
50
+ // Redirect to module's login endpoint
51
+ return navigateTo('/api/kinde/login', { external: true })
52
+ }
53
+ } else {
54
+ // Client-side: Check for E2E token first (for tests)
55
+ const e2eToken = useCookie(E2E_TOKEN_COOKIE_NAME)
56
+
57
+ // If E2E token exists, allow access (for automated tests)
58
+ if (e2eToken.value) {
59
+ return
60
+ }
61
+
62
+ // Check for auth cookies directly (more reliable than reactive state)
63
+ const config = useRuntimeConfig()
64
+ // @ts-expect-error - cookie property exists in runtime config but not in Kinde module types
65
+ const cookiePrefix = config.public.kindeAuth?.cookie?.prefix || 'app_'
66
+
67
+ const idTokenName = `${cookiePrefix}${KINDE_ID_TOKEN_COOKIE_NAME}`
68
+ const accessTokenName = `${cookiePrefix}${KINDE_ACCESS_TOKEN_COOKIE_NAME}`
69
+
70
+ const idToken = useCookie(idTokenName)
71
+ const accessToken = useCookie(accessTokenName)
72
+
73
+ // Allow access if any valid auth token cookie exists
74
+ if (!idToken.value && !accessToken.value) {
75
+ // Redirect to module's login endpoint
76
+ if (import.meta.client) {
77
+ window.location.href = '/api/kinde/login'
78
+ }
79
+ return
80
+ }
81
+ }
82
+ })
@@ -0,0 +1,32 @@
1
+ export default {
2
+ extends: ['@commitlint/config-conventional'],
3
+ rules: {
4
+ 'body-leading-blank': [1, 'always'],
5
+ 'body-max-line-length': [2, 'always', 500],
6
+ 'footer-leading-blank': [1, 'always'],
7
+ 'footer-max-line-length': [2, 'always', 500],
8
+ 'header-max-length': [2, 'always', 100],
9
+ 'subject-case': [0, 'never'],
10
+ 'subject-empty': [2, 'never'],
11
+ 'subject-full-stop': [2, 'never', '.'],
12
+ 'type-case': [2, 'always', 'lower-case'],
13
+ 'type-empty': [2, 'never'],
14
+ 'type-enum': [
15
+ 2,
16
+ 'always',
17
+ [
18
+ 'build',
19
+ 'chore',
20
+ 'ci',
21
+ 'docs',
22
+ 'feat',
23
+ 'fix',
24
+ 'perf',
25
+ 'refactor',
26
+ 'revert',
27
+ 'style',
28
+ 'test'
29
+ ]
30
+ ]
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+ import withNuxt from './node_modules/.cache/nuxt/.nuxt/eslint.config.mjs'
3
+
4
+ export default withNuxt(
5
+ // Your custom configs here
6
+ )
package/nuxt.config.ts ADDED
@@ -0,0 +1,77 @@
1
+ // https://nuxt.com/docs/guide/going-further/layers
2
+ export default defineNuxtConfig({
3
+
4
+ // Pre-configure shared modules that all projects will use
5
+ modules: [
6
+ '@nuxt/eslint',
7
+ '@nuxt/ui',
8
+ '@nuxt/image',
9
+ '@habityzer/nuxt-kinde-auth',
10
+ '@pinia/nuxt'
11
+ ],
12
+
13
+ devtools: { enabled: true },
14
+
15
+ // Default runtime config (projects can override)
16
+ runtimeConfig: {
17
+ // Server-only (private) runtime config
18
+ apiBaseUrl: '',
19
+
20
+ public: {
21
+ // Public runtime config (exposed to client-side)
22
+ apiBaseUrl: '',
23
+ apiPrefix: '/api/symfony', // API endpoint prefix for useOpenApi
24
+
25
+ // Expose kindeAuth config for middleware (will be merged with project config)
26
+ kindeAuth: {
27
+ // @ts-expect-error - cookie property exists in runtime but not in Kinde module types
28
+ cookie: {
29
+ prefix: 'app_' // Default, projects override this
30
+ },
31
+ middleware: {
32
+ publicRoutes: [] // Default, projects override this
33
+ }
34
+ }
35
+ }
36
+ },
37
+ compatibilityDate: '2025-01-17',
38
+
39
+ // ESLint configuration
40
+ eslint: {
41
+ config: {
42
+ stylistic: {
43
+ commaDangle: 'never',
44
+ braceStyle: '1tbs'
45
+ }
46
+ }
47
+ },
48
+
49
+ // Default Kinde configuration (projects MUST override with their credentials)
50
+ kindeAuth: {
51
+ authDomain: '', // Project must provide
52
+ clientId: '', // Project must provide
53
+ clientSecret: '', // Project must provide
54
+ redirectURL: '', // Project must provide
55
+ logoutRedirectURL: '', // Project must provide
56
+ postLoginRedirectURL: '/dashboard', // Default, can be overridden
57
+ cookie: {
58
+ prefix: 'app_', // Projects MUST override this
59
+ httpOnly: false, // Allow client-side deletion for logout
60
+ secure: process.env.NODE_ENV === 'production',
61
+ sameSite: 'lax' as const,
62
+ path: '/',
63
+ maxAge: 60 * 60 * 24 * 7 // 7 days
64
+ },
65
+ middleware: {
66
+ enabled: false, // Disabled - using custom middleware from layer
67
+ global: false,
68
+ publicRoutes: [] // Projects can override
69
+ },
70
+ debug: {
71
+ enabled: process.env.NODE_ENV !== 'production'
72
+ }
73
+ },
74
+
75
+ // Pinia configuration
76
+ pinia: {}
77
+ })
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@habityzer/nuxt-symfony-kinde-layer",
3
+ "version": "1.0.0",
4
+ "description": "Shared Nuxt layer for Symfony + Kinde authentication integration",
5
+ "type": "module",
6
+ "main": "./nuxt.config.ts",
7
+ "keywords": [
8
+ "nuxt",
9
+ "nuxt-layer",
10
+ "symfony",
11
+ "kinde",
12
+ "authentication"
13
+ ],
14
+ "author": "Habityzer",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@habityzer/nuxt-kinde-auth": "^1.2.0",
18
+ "@pinia/nuxt": "^0.11.2",
19
+ "@nuxt/ui": "^4.0.1",
20
+ "@nuxt/image": "^1.11.0",
21
+ "@nuxt/eslint": "^1.9.0",
22
+ "@vueuse/core": "^13.9.0",
23
+ "nuxt": "^4.1.3",
24
+ "vue": "^3.5.22",
25
+ "vue-router": "^4.6.0"
26
+ },
27
+ "devDependencies": {
28
+ "@commitlint/config-conventional": "^19.8.1",
29
+ "commitlint": "^19.8.1",
30
+ "conventional-changelog-conventionalcommits": "^9.1.0",
31
+ "@semantic-release/changelog": "^6.0.3",
32
+ "@semantic-release/commit-analyzer": "^13.0.1",
33
+ "@semantic-release/git": "^10.0.1",
34
+ "@semantic-release/release-notes-generator": "^14.1.0",
35
+ "@habityzer/nuxt-openapi-composables": "^1.1.0",
36
+ "@iconify-json/heroicons": "^1.2.3",
37
+ "openapi-typescript": "^7.10.0",
38
+ "typescript": "^5.9.3",
39
+ "eslint": "^9.37.0",
40
+ "semantic-release": "^24.2.9"
41
+ },
42
+ "peerDependencies": {
43
+ "nuxt": "^3.0.0 || ^4.0.0"
44
+ },
45
+ "scripts": {
46
+ "dev": "nuxi dev --dotenv .env.example",
47
+ "build": "nuxt build",
48
+ "lint": "eslint .",
49
+ "lint:fix": "eslint . --fix",
50
+ "release": " HUSKY=0 semantic-release"
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ onlyBuiltDependencies:
2
+ - '@tailwindcss/oxide'
3
+ - sharp
4
+ - unrs-resolver
5
+ - vue-demi
Binary file
@@ -0,0 +1,2 @@
1
+ User-Agent: *
2
+ Disallow:
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Symfony API Proxy - Forwards requests to Symfony backend with authentication
3
+ *
4
+ * Usage: /api/symfony/* -> proxies to Symfony backend
5
+ *
6
+ * Features:
7
+ * - Forwards Kinde authentication tokens
8
+ * - Supports E2E testing with app tokens
9
+ * - Properly forwards Accept and Content-Type headers for API negotiation
10
+ * - Handles query parameters
11
+ *
12
+ * @see .cursorrules for proxy best practices
13
+ */
14
+
15
+ // Auth constants (defined inline to avoid import issues during Nitro bundling)
16
+ const E2E_TOKEN_COOKIE_NAME = 'kinde_token'
17
+ const APP_TOKEN_PREFIX = 'app_'
18
+ const KINDE_ID_TOKEN_COOKIE_NAME = 'id_token'
19
+ export default defineEventHandler(async (event) => {
20
+ const config = useRuntimeConfig()
21
+
22
+ // Get the path (remove /api/symfony prefix)
23
+ let path = event.context.params?.path || event.path.replace('/api/symfony', '')
24
+
25
+ // Ensure path starts with / (for catch-all routes it might not)
26
+ if (!path.startsWith('/')) {
27
+ path = `/${path}`
28
+ }
29
+
30
+ let token: string | undefined
31
+
32
+ // Check for E2E test token first (from cookie)
33
+ // Only use E2E token if it's a valid app token (starts with APP_TOKEN_PREFIX)
34
+ const e2eToken = getCookie(event, E2E_TOKEN_COOKIE_NAME)
35
+ if (e2eToken && e2eToken.startsWith(APP_TOKEN_PREFIX)) {
36
+ token = e2eToken
37
+ } else {
38
+ // Use Kinde authentication from the module
39
+ const kinde = event.context.kinde
40
+
41
+ if (!kinde?.client || !kinde?.sessionManager) {
42
+ throw createError({
43
+ statusCode: 500,
44
+ statusMessage: 'Kinde authentication not initialized. Module may not be loaded correctly.'
45
+ })
46
+ }
47
+
48
+ const { client, sessionManager } = kinde
49
+
50
+ try {
51
+ // Try to get access token first
52
+ let accessToken: string | null = null
53
+ try {
54
+ accessToken = await client.getToken(sessionManager)
55
+ } catch {
56
+ // Silent - will try id_token fallback
57
+ }
58
+
59
+ // If access token is not available, try id_token as fallback
60
+ if (!accessToken || accessToken.trim() === '') {
61
+ const idToken = await sessionManager.getSessionItem(KINDE_ID_TOKEN_COOKIE_NAME) as string | undefined
62
+
63
+ if (idToken) {
64
+ token = idToken
65
+ }
66
+ } else {
67
+ token = accessToken
68
+ }
69
+
70
+ if (!token || token.trim() === '') {
71
+ throw createError({
72
+ statusCode: 401,
73
+ statusMessage: 'Unauthorized - Please log in'
74
+ })
75
+ }
76
+ } catch (error) {
77
+ console.error('❌ [SYMFONY PROXY] Auth error:', error)
78
+ throw createError({
79
+ statusCode: 401,
80
+ statusMessage: error instanceof Error ? error.message : 'Authentication failed'
81
+ })
82
+ }
83
+ }
84
+
85
+ if (!token) {
86
+ throw createError({
87
+ statusCode: 401,
88
+ statusMessage: 'No authentication token available'
89
+ })
90
+ }
91
+
92
+ try {
93
+ // Get request method and body
94
+ const method = event.method
95
+ const body = method !== 'GET' && method !== 'HEAD' ? await readBody(event) : undefined
96
+
97
+ // Get query parameters from original request
98
+ const query = getQuery(event)
99
+
100
+ // Prepare headers for Symfony
101
+ // IMPORTANT: Forward Content-Type and Accept headers for proper API negotiation
102
+ const headers: Record<string, string> = {
103
+ Authorization: `Bearer ${token}`
104
+ }
105
+
106
+ // Forward Content-Type header
107
+ const contentType = getHeader(event, 'content-type')
108
+ if (contentType) {
109
+ headers['Content-Type'] = contentType
110
+ }
111
+
112
+ // Forward Accept header (CRITICAL for content negotiation)
113
+ // Without this, backend returns default format instead of requested format (e.g., JSON vs Hydra)
114
+ const accept = getHeader(event, 'accept')
115
+ if (accept) {
116
+ headers['Accept'] = accept
117
+ }
118
+
119
+ // Forward request to Symfony with Kinde token
120
+ const response = await $fetch(path, {
121
+ baseURL: config.apiBaseUrl as string,
122
+ method,
123
+ headers,
124
+ body,
125
+ query
126
+ })
127
+
128
+ return response
129
+ } catch (error) {
130
+ console.error('❌ [SYMFONY PROXY] Symfony API error:', {
131
+ path,
132
+ statusCode: error && typeof error === 'object' && 'statusCode' in error ? error.statusCode : 'unknown',
133
+ message: error instanceof Error ? error.message : 'unknown'
134
+ })
135
+ // Handle Symfony API errors
136
+ const statusCode = error && typeof error === 'object' && 'statusCode' in error ? (error.statusCode as number) : 500
137
+ const statusMessage = error && typeof error === 'object' && 'statusMessage' in error
138
+ ? (error.statusMessage as string)
139
+ : error instanceof Error
140
+ ? error.message
141
+ : 'Symfony API error'
142
+ const data = error && typeof error === 'object' && 'data' in error ? error.data : undefined
143
+
144
+ throw createError({
145
+ statusCode,
146
+ statusMessage,
147
+ data
148
+ })
149
+ }
150
+ })
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Authentication constants for server-side code
3
+ * These are duplicated from app/constants/auth.ts to avoid import issues in Nitro bundling
4
+ */
5
+
6
+ /**
7
+ * Cookie name for E2E test authentication token
8
+ * Used by automated tests to bypass Kinde OAuth flow
9
+ */
10
+ export const E2E_TOKEN_COOKIE_NAME = 'kinde_token'
11
+
12
+ /**
13
+ * Prefix for Symfony app tokens (used in E2E tests)
14
+ * These are long-lived tokens generated by `php bin/console app:token:manage create`
15
+ */
16
+ export const APP_TOKEN_PREFIX = 'app_'
17
+
18
+ /**
19
+ * Kinde authentication cookie names (base names without prefix)
20
+ * The prefix is configured per-project in nuxt.config.ts and applied dynamically
21
+ */
22
+ export const KINDE_ID_TOKEN_COOKIE_NAME = 'id_token'
23
+ export const KINDE_ACCESS_TOKEN_COOKIE_NAME = 'access_token'
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ // https://nuxt.com/docs/guide/concepts/typescript
3
+ "files": [],
4
+ "references": [
5
+ {
6
+ "path": "./.nuxt/tsconfig.app.json"
7
+ },
8
+ {
9
+ "path": "./.nuxt/tsconfig.server.json"
10
+ },
11
+ {
12
+ "path": "./.nuxt/tsconfig.shared.json"
13
+ },
14
+ {
15
+ "path": "./.nuxt/tsconfig.node.json"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Type augmentation for @habityzer/nuxt-kinde-auth module
3
+ * Adds custom configuration options we use in the layer
4
+ */
5
+ declare module '@habityzer/nuxt-kinde-auth' {
6
+ interface ModuleOptions {
7
+ cookie?: {
8
+ prefix?: string
9
+ }
10
+ middleware?: {
11
+ enabled?: boolean
12
+ global?: boolean
13
+ publicRoutes?: string[]
14
+ }
15
+ debug?: {
16
+ enabled?: boolean
17
+ }
18
+ }
19
+ }
20
+
21
+ export {}