@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.
- package/.releaserc.json +37 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +6 -0
- package/README.md +326 -0
- package/app/composables/useAuth.ts +139 -0
- package/app/constants/auth.ts +26 -0
- package/app/middleware/auth.global.ts +82 -0
- package/commitlint.config.js +32 -0
- package/eslint.config.mjs +6 -0
- package/nuxt.config.ts +77 -0
- package/package.json +52 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +2 -0
- package/server/api/symfony/[...].ts +150 -0
- package/server/utils/auth-constants.ts +23 -0
- package/tsconfig.json +18 -0
- package/types/kinde-auth.d.ts +21 -0
package/.releaserc.json
ADDED
|
@@ -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
|
+
}
|
package/CHANGELOG.md
ADDED
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
|
+
}
|
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
|
+
}
|
|
Binary file
|
|
@@ -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 {}
|