een-api-toolkit 0.0.8 → 0.0.13

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,12 @@
1
+ # EEN OAuth Client ID (required)
2
+ VITE_EEN_CLIENT_ID=your-een-client-id
3
+
4
+ # OAuth Proxy URL (required)
5
+ VITE_PROXY_URL=http://localhost:8787
6
+
7
+ # OAuth Redirect URI (MUST be http://127.0.0.1:3333 - EEN IDP requirement)
8
+ # Do not change this value - the EEN Identity Provider only permits this URL
9
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
10
+
11
+ # Enable debug logging (optional)
12
+ VITE_DEBUG=true
@@ -0,0 +1,146 @@
1
+ # EEN API Toolkit - Vue 3 Example
2
+
3
+ A complete example showing how to use the een-api-toolkit in a Vue 3 application.
4
+
5
+ ## Features Demonstrated
6
+
7
+ - OAuth authentication flow (login, callback, logout)
8
+ - Protected routes with navigation guards
9
+ - useCurrentUser composable for current user profile
10
+ - useUsers composable with pagination
11
+ - Error handling
12
+ - Reactive authentication state
13
+
14
+ ## Setup
15
+
16
+ ### Prerequisites
17
+
18
+ 1. **Start the OAuth proxy** (required for authentication):
19
+
20
+ The OAuth proxy is a separate project that handles token management securely.
21
+ Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
22
+
23
+ ```bash
24
+ # In a separate terminal, from the een-oauth-proxy directory
25
+ npm install
26
+ npm run dev
27
+ ```
28
+
29
+ The proxy should be running at `http://localhost:8787`.
30
+
31
+ ### Example Setup
32
+
33
+ All commands below should be run from this example directory (`examples/vue-basic/`):
34
+
35
+ 2. Copy the environment file:
36
+ ```bash
37
+ # From examples/vue-basic/
38
+ cp .env.example .env
39
+ ```
40
+
41
+ 3. Edit `.env` with your EEN credentials:
42
+ ```env
43
+ VITE_EEN_CLIENT_ID=your-client-id
44
+ VITE_PROXY_URL=http://localhost:8787
45
+ # DO NOT change the redirect URI - EEN IDP only permits this URL
46
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
47
+ ```
48
+
49
+ 4. Install dependencies and start:
50
+ ```bash
51
+ # From examples/vue-basic/
52
+ npm install
53
+ npm run dev
54
+ ```
55
+
56
+ 5. Open http://127.0.0.1:3333 in your browser.
57
+
58
+ **Important:** The EEN Identity Provider only permits `http://127.0.0.1:3333` as the OAuth redirect URI. Do not use `localhost` or other ports.
59
+
60
+ ## Project Structure
61
+
62
+ ```
63
+ src/
64
+ ├── main.ts # App entry, toolkit initialization
65
+ ├── App.vue # Root component with navigation
66
+ ├── router/
67
+ │ └── index.ts # Vue Router with auth guards
68
+ └── views/
69
+ ├── Home.vue # Home page with user profile
70
+ ├── Login.vue # OAuth login redirect
71
+ ├── Callback.vue # OAuth callback handler
72
+ ├── Users.vue # User list with pagination
73
+ └── Logout.vue # Logout handler
74
+ ```
75
+
76
+ ## Key Code Examples
77
+
78
+ ### Initializing the Toolkit (main.ts)
79
+
80
+ ```typescript
81
+ import { initEenToolkit } from 'een-api-toolkit'
82
+
83
+ initEenToolkit({
84
+ proxyUrl: import.meta.env.VITE_PROXY_URL,
85
+ clientId: import.meta.env.VITE_EEN_CLIENT_ID,
86
+ debug: true
87
+ })
88
+ ```
89
+
90
+ ### OAuth Login (Login.vue)
91
+
92
+ ```typescript
93
+ import { getAuthUrl } from 'een-api-toolkit'
94
+
95
+ function login() {
96
+ window.location.href = getAuthUrl()
97
+ }
98
+ ```
99
+
100
+ ### OAuth Callback (Callback.vue)
101
+
102
+ ```typescript
103
+ import { handleAuthCallback } from 'een-api-toolkit'
104
+
105
+ const url = new URL(window.location.href)
106
+ const code = url.searchParams.get('code')
107
+ const state = url.searchParams.get('state')
108
+
109
+ const { error } = await handleAuthCallback(code, state)
110
+ if (error) {
111
+ // Handle error
112
+ } else {
113
+ router.push('/dashboard')
114
+ }
115
+ ```
116
+
117
+ ### Using Composables (Users.vue)
118
+
119
+ ```vue
120
+ <script setup>
121
+ import { useUsers } from 'een-api-toolkit'
122
+
123
+ const { users, loading, error, hasNextPage, fetchNextPage } = useUsers({ pageSize: 10 })
124
+ </script>
125
+
126
+ <template>
127
+ <ul v-for="user in users" :key="user.id">
128
+ <li>{{ user.email }}</li>
129
+ </ul>
130
+ <button v-if="hasNextPage" @click="fetchNextPage">Load More</button>
131
+ </template>
132
+ ```
133
+
134
+ ### Auth Guard (router/index.ts)
135
+
136
+ ```typescript
137
+ router.beforeEach((to, from, next) => {
138
+ const authStore = useAuthStore()
139
+
140
+ if (to.meta.requiresAuth && !authStore.isAuthenticated) {
141
+ next('/login')
142
+ } else {
143
+ next()
144
+ }
145
+ })
146
+ ```
@@ -0,0 +1,61 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ test.describe('Vue Basic Example - App', () => {
4
+ test.beforeEach(async ({ page }) => {
5
+ await page.goto('/')
6
+ })
7
+
8
+ test('app loads with correct title', async ({ page }) => {
9
+ await expect(page).toHaveTitle(/EEN API Toolkit/)
10
+ })
11
+
12
+ test('header displays app name', async ({ page }) => {
13
+ await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN API Toolkit Example')
14
+ })
15
+
16
+ test('navigation shows Home and Login links when not authenticated', async ({ page }) => {
17
+ // Home link should be visible
18
+ await expect(page.locator('[data-testid="nav-home"]')).toBeVisible()
19
+
20
+ // Login link should be visible (not authenticated)
21
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
22
+
23
+ // Users and Logout should NOT be visible (requires auth)
24
+ await expect(page.locator('[data-testid="nav-users"]')).not.toBeVisible()
25
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
26
+ })
27
+
28
+ test('home page shows not logged in message', async ({ page }) => {
29
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
30
+ await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
31
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
32
+ })
33
+
34
+ test('login page displays login button', async ({ page }) => {
35
+ await page.goto('/login')
36
+
37
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
38
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
39
+ })
40
+
41
+ test('protected route redirects to login', async ({ page }) => {
42
+ await page.goto('/users')
43
+
44
+ // Should be redirected to login page
45
+ await page.waitForURL('/login')
46
+ await expect(page).toHaveURL('/login')
47
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
48
+ })
49
+
50
+ test('navigation between pages works', async ({ page }) => {
51
+ // Click Login link
52
+ await page.click('[data-testid="nav-login"]')
53
+ await page.waitForURL('/login')
54
+ await expect(page).toHaveURL('/login')
55
+
56
+ // Click Home link
57
+ await page.click('[data-testid="nav-home"]')
58
+ await page.waitForURL('/')
59
+ await expect(page).toHaveURL('/')
60
+ })
61
+ })
@@ -0,0 +1,260 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+
3
+ /**
4
+ * E2E tests for the Vue Basic Example
5
+ *
6
+ * Tests the OAuth login flow through the UI:
7
+ * 1. Click login button in the example app
8
+ * 2. Enter credentials on EEN OAuth page
9
+ * 3. Complete the OAuth callback
10
+ * 4. Verify authenticated state
11
+ *
12
+ * Required environment variables:
13
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
14
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
15
+ * - TEST_USER: Test user email
16
+ * - TEST_PASSWORD: Test user password
17
+ */
18
+
19
+ // Timeout constants for consistent behavior
20
+ // Values chosen based on OAuth flow timing requirements
21
+ const TIMEOUTS = {
22
+ OAUTH_REDIRECT: 30000, // OAuth redirects can be slow on first load
23
+ ELEMENT_VISIBLE: 15000, // Wait for OAuth page elements to render
24
+ PASSWORD_VISIBLE: 10000, // Password field appears after email validation
25
+ AUTH_COMPLETE: 30000, // Full OAuth flow completion
26
+ UI_UPDATE: 10000, // UI state updates after auth changes
27
+ PROXY_CHECK: 5000 // Quick check if proxy is running
28
+ } as const
29
+
30
+ const TEST_USER = process.env.TEST_USER
31
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
32
+ const PROXY_URL = process.env.VITE_PROXY_URL
33
+
34
+ /**
35
+ * Checks if the OAuth proxy is accessible.
36
+ * Returns true if proxy responds (even with 404), false if unreachable.
37
+ */
38
+ async function isProxyAccessible(): Promise<boolean> {
39
+ if (!PROXY_URL) return false
40
+ const controller = new AbortController()
41
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
42
+
43
+ try {
44
+ const response = await fetch(PROXY_URL, {
45
+ method: 'HEAD',
46
+ signal: controller.signal
47
+ })
48
+ // 404 is ok - means proxy is running but endpoint doesn't exist
49
+ return response.ok || response.status === 404
50
+ } catch {
51
+ return false
52
+ } finally {
53
+ clearTimeout(timeoutId)
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Performs OAuth login flow through the UI.
59
+ * Starts from home page and completes full OAuth authentication.
60
+ */
61
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
62
+ // Start at home page
63
+ await page.goto('/')
64
+
65
+ // Click login button and wait for OAuth redirect
66
+ await Promise.all([
67
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
68
+ page.click('[data-testid="login-button"]')
69
+ ])
70
+
71
+ // Fill email
72
+ const emailInput = page.locator('#authentication--input__email')
73
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
74
+ await emailInput.fill(username)
75
+
76
+ // Click next
77
+ await page.getByRole('button', { name: 'Next' }).click()
78
+
79
+ // Fill password
80
+ const passwordInput = page.locator('#authentication--input__password')
81
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
82
+ await passwordInput.fill(password)
83
+
84
+ // Click sign in - use OR selector for robustness
85
+ await page.locator('#next, button:has-text("Sign in")').first().click()
86
+
87
+ // Wait for auth to complete and redirect to app
88
+ await page.waitForURL('**/', { timeout: TIMEOUTS.AUTH_COMPLETE })
89
+ }
90
+
91
+ /**
92
+ * Clears browser storage to reset auth state.
93
+ * Handles cases where localStorage isn't accessible (e.g., about:blank, cross-origin).
94
+ */
95
+ async function clearAuthState(page: Page): Promise<void> {
96
+ try {
97
+ // Only try to clear storage if we're on a page that allows it
98
+ const url = page.url()
99
+ if (url && url.startsWith('http')) {
100
+ await page.evaluate(() => {
101
+ try {
102
+ localStorage.clear()
103
+ sessionStorage.clear()
104
+ } catch {
105
+ // Ignore errors - storage may not be accessible
106
+ }
107
+ })
108
+ }
109
+ } catch {
110
+ // Ignore errors - page may be closed or in an inaccessible state
111
+ }
112
+ }
113
+
114
+ test.describe('Vue Basic Example', () => {
115
+ // Check proxy accessibility once before all tests
116
+ let proxyAccessible = false
117
+
118
+ // Helper functions to skip tests when prerequisites aren't met
119
+ function skipIfNoProxy() {
120
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
121
+ }
122
+
123
+ function skipIfNoCredentials() {
124
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
125
+ }
126
+
127
+ function skipIfNoUser() {
128
+ test.skip(!TEST_USER, 'Test user not available')
129
+ }
130
+
131
+ test.beforeAll(async () => {
132
+ proxyAccessible = await isProxyAccessible()
133
+ if (!proxyAccessible) {
134
+ console.log('OAuth proxy not accessible - OAuth tests will be skipped')
135
+ }
136
+ })
137
+
138
+ test.afterEach(async ({ page }) => {
139
+ // Clear auth state after each test to prevent state pollution
140
+ await clearAuthState(page)
141
+ })
142
+
143
+ test('shows login button when not authenticated', async ({ page }) => {
144
+ await page.goto('/')
145
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
146
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
147
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
148
+ })
149
+
150
+ test('users page shows not-authenticated state without login', async ({ page }) => {
151
+ await page.goto('/users')
152
+ await expect(
153
+ page.locator('[data-testid="not-authenticated"], [data-testid="nav-login"], .error, .auth-required').first()
154
+ ).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
155
+ })
156
+
157
+ test('login button redirects to OAuth page', async ({ page }) => {
158
+ skipIfNoProxy()
159
+ skipIfNoCredentials()
160
+
161
+ await page.goto('/')
162
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
163
+ await expect(page.locator('[data-testid="login-button"]')).toBeEnabled()
164
+
165
+ // Click login and verify redirect to OAuth page
166
+ await Promise.all([
167
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
168
+ page.click('[data-testid="login-button"]')
169
+ ])
170
+
171
+ // Verify we're on the OAuth page
172
+ const emailInput = page.locator('#authentication--input__email')
173
+ await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
174
+ })
175
+
176
+ test('complete OAuth login flow', async ({ page }) => {
177
+ skipIfNoProxy()
178
+ skipIfNoCredentials()
179
+
180
+ // Verify initially not authenticated
181
+ await page.goto('/')
182
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
183
+
184
+ // Perform login
185
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
186
+
187
+ // Verify authenticated state
188
+ await expect(page.locator('[data-testid="not-authenticated"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
189
+ await expect(page.locator('[data-testid="nav-users"]')).toBeVisible()
190
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible()
191
+ await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible()
192
+ })
193
+
194
+ test('can view users list after login', async ({ page }) => {
195
+ skipIfNoProxy()
196
+ skipIfNoCredentials()
197
+
198
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
199
+ await expect(page.locator('[data-testid="nav-users"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
200
+
201
+ // Navigate to users page
202
+ await page.click('[data-testid="nav-users"]')
203
+ await page.waitForURL('/users')
204
+
205
+ // Should see users table (not error state)
206
+ await expect(page.locator('.users table')).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
207
+ await expect(page.locator('.error')).not.toBeVisible()
208
+ })
209
+
210
+ test('can logout after login', async ({ page }) => {
211
+ skipIfNoProxy()
212
+ skipIfNoCredentials()
213
+
214
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
215
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
216
+
217
+ // Click logout
218
+ await page.click('[data-testid="nav-logout"]')
219
+
220
+ // Should show not authenticated
221
+ await page.waitForURL('**/')
222
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
223
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
224
+ })
225
+
226
+ test('invalid password shows error on OAuth page', async ({ page }) => {
227
+ skipIfNoProxy()
228
+ skipIfNoUser()
229
+
230
+ await page.goto('/')
231
+
232
+ // Click login and wait for OAuth redirect
233
+ await Promise.all([
234
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
235
+ page.click('[data-testid="login-button"]')
236
+ ])
237
+
238
+ // Fill valid email
239
+ const emailInput = page.locator('#authentication--input__email')
240
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
241
+ await emailInput.fill(TEST_USER!)
242
+ await page.getByRole('button', { name: 'Next' }).click()
243
+
244
+ // Fill invalid password
245
+ const passwordInput = page.locator('#authentication--input__password')
246
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
247
+ await passwordInput.fill('invalid-password-12345!')
248
+
249
+ // Click sign in
250
+ await page.locator('#next, button:has-text("Sign in")').first().click()
251
+
252
+ // Should show error message on OAuth page
253
+ await expect(
254
+ page.locator('.error, [class*="error"], [data-testid*="error"], #error, .alert-danger').first()
255
+ ).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
256
+
257
+ // Should still be on OAuth page
258
+ await expect(page).toHaveURL(/eagleeyenetworks\.com/)
259
+ })
260
+ })
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>EEN API Toolkit Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>