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,28 @@
1
+ {
2
+ "name": "een-api-toolkit-example",
3
+ "version": "0.0.8",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "stop": "npx kill-port 3333",
8
+ "dev": "npm run stop && vite",
9
+ "build": "vue-tsc && vite build",
10
+ "preview": "vite preview",
11
+ "test:e2e": "playwright test",
12
+ "test:e2e:ui": "playwright test --ui"
13
+ },
14
+ "dependencies": {
15
+ "een-api-toolkit": "file:../..",
16
+ "pinia": "^2.1.7",
17
+ "vue": "^3.4.0",
18
+ "vue-router": "^4.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@playwright/test": "^1.57.0",
22
+ "@vitejs/plugin-vue": "^6.0.0",
23
+ "dotenv": "^17.2.3",
24
+ "typescript": "~5.8.0",
25
+ "vite": "^7.3.0",
26
+ "vue-tsc": "^3.2.1"
27
+ }
28
+ }
@@ -0,0 +1,47 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+ import dotenv from 'dotenv'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ // Load .env files: parent first, then local with override to replace any conflicts
9
+ // In CI, env vars are passed directly via workflow
10
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') })
11
+ dotenv.config({ path: path.resolve(__dirname, '.env'), override: true })
12
+
13
+ const redirectUri = process.env.VITE_REDIRECT_URI || 'http://127.0.0.1:3333'
14
+ if (!redirectUri.startsWith('http://127.0.0.1:') && !redirectUri.startsWith('http://localhost:')) {
15
+ throw new Error('VITE_REDIRECT_URI must use localhost or 127.0.0.1 for security')
16
+ }
17
+ const baseURL = redirectUri
18
+
19
+ export default defineConfig({
20
+ testDir: './e2e',
21
+ testMatch: '**/*.spec.ts',
22
+ fullyParallel: false, // Run tests sequentially for predictable order
23
+ forbidOnly: !!process.env.CI,
24
+ retries: 0, // No retries - fail fast
25
+ maxFailures: 1, // Stop on first failure
26
+ workers: 1,
27
+ reporter: [['html', { open: 'never' }]],
28
+ timeout: 30000,
29
+ use: {
30
+ baseURL,
31
+ trace: 'on-first-retry',
32
+ video: 'retain-on-failure'
33
+ },
34
+ outputDir: './e2e-results/',
35
+ projects: [
36
+ {
37
+ name: 'chromium',
38
+ use: { ...devices['Desktop Chrome'] }
39
+ }
40
+ ],
41
+ webServer: {
42
+ command: 'npm run dev',
43
+ url: baseURL,
44
+ reuseExistingServer: !process.env.CI,
45
+ timeout: 30000
46
+ }
47
+ })
@@ -0,0 +1,108 @@
1
+ <script setup lang="ts">
2
+ import { useAuthStore } from 'een-api-toolkit'
3
+ import { computed } from 'vue'
4
+
5
+ const authStore = useAuthStore()
6
+ const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+ </script>
8
+
9
+ <template>
10
+ <div class="app">
11
+ <header>
12
+ <h1 data-testid="app-title">EEN API Toolkit Example</h1>
13
+ <nav>
14
+ <router-link data-testid="nav-home" to="/">Home</router-link>
15
+ <router-link data-testid="nav-users" v-if="isAuthenticated" to="/users">Users</router-link>
16
+ <router-link data-testid="nav-login" v-if="!isAuthenticated" to="/login">Login</router-link>
17
+ <router-link data-testid="nav-logout" v-if="isAuthenticated" to="/logout">Logout</router-link>
18
+ </nav>
19
+ </header>
20
+ <main>
21
+ <router-view />
22
+ </main>
23
+ </div>
24
+ </template>
25
+
26
+ <style>
27
+ * {
28
+ box-sizing: border-box;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
35
+ Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
36
+ line-height: 1.6;
37
+ color: #333;
38
+ }
39
+
40
+ .app {
41
+ max-width: 1200px;
42
+ margin: 0 auto;
43
+ padding: 20px;
44
+ }
45
+
46
+ header {
47
+ display: flex;
48
+ justify-content: space-between;
49
+ align-items: center;
50
+ margin-bottom: 30px;
51
+ padding-bottom: 20px;
52
+ border-bottom: 1px solid #eee;
53
+ }
54
+
55
+ header h1 {
56
+ font-size: 1.5rem;
57
+ }
58
+
59
+ nav {
60
+ display: flex;
61
+ gap: 20px;
62
+ }
63
+
64
+ nav a {
65
+ color: #42b883;
66
+ text-decoration: none;
67
+ }
68
+
69
+ nav a:hover {
70
+ text-decoration: underline;
71
+ }
72
+
73
+ nav a.router-link-active {
74
+ font-weight: bold;
75
+ }
76
+
77
+ button {
78
+ background: #42b883;
79
+ color: white;
80
+ border: none;
81
+ padding: 10px 20px;
82
+ border-radius: 4px;
83
+ cursor: pointer;
84
+ font-size: 1rem;
85
+ }
86
+
87
+ button:hover {
88
+ background: #3aa876;
89
+ }
90
+
91
+ button:disabled {
92
+ background: #ccc;
93
+ cursor: not-allowed;
94
+ }
95
+
96
+ .error {
97
+ color: #e74c3c;
98
+ padding: 10px;
99
+ background: #fdf2f2;
100
+ border-radius: 4px;
101
+ margin: 10px 0;
102
+ }
103
+
104
+ .loading {
105
+ color: #666;
106
+ font-style: italic;
107
+ }
108
+ </style>
@@ -0,0 +1,23 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import { initEenToolkit } from 'een-api-toolkit'
4
+ import App from './App.vue'
5
+ import router from './router'
6
+
7
+ const app = createApp(App)
8
+
9
+ // Install Pinia (required before initEenToolkit)
10
+ app.use(createPinia())
11
+
12
+ // Initialize EEN API Toolkit
13
+ initEenToolkit({
14
+ proxyUrl: import.meta.env.VITE_PROXY_URL,
15
+ clientId: import.meta.env.VITE_EEN_CLIENT_ID,
16
+ redirectUri: import.meta.env.VITE_REDIRECT_URI,
17
+ debug: import.meta.env.VITE_DEBUG === 'true'
18
+ })
19
+
20
+ // Install router
21
+ app.use(router)
22
+
23
+ app.mount('#app')
@@ -0,0 +1,61 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import { useAuthStore } from 'een-api-toolkit'
3
+ import Home from '../views/Home.vue'
4
+ import Login from '../views/Login.vue'
5
+ import Callback from '../views/Callback.vue'
6
+ import Users from '../views/Users.vue'
7
+ import Logout from '../views/Logout.vue'
8
+
9
+ const router = createRouter({
10
+ history: createWebHistory(),
11
+ routes: [
12
+ {
13
+ path: '/',
14
+ name: 'home',
15
+ component: Home,
16
+ // Handle OAuth callback on root path (EEN IDP redirects to http://127.0.0.1:3333)
17
+ beforeEnter: (to, _from, next) => {
18
+ // If URL has code and state params, it's an OAuth callback
19
+ if (to.query.code && to.query.state) {
20
+ next({ name: 'callback', query: to.query })
21
+ } else {
22
+ next()
23
+ }
24
+ }
25
+ },
26
+ {
27
+ path: '/login',
28
+ name: 'login',
29
+ component: Login
30
+ },
31
+ {
32
+ path: '/callback',
33
+ name: 'callback',
34
+ component: Callback
35
+ },
36
+ {
37
+ path: '/users',
38
+ name: 'users',
39
+ component: Users,
40
+ meta: { requiresAuth: true }
41
+ },
42
+ {
43
+ path: '/logout',
44
+ name: 'logout',
45
+ component: Logout
46
+ }
47
+ ]
48
+ })
49
+
50
+ // Navigation guard for protected routes
51
+ router.beforeEach((to, _from, next) => {
52
+ const authStore = useAuthStore()
53
+
54
+ if (to.meta.requiresAuth && !authStore.isAuthenticated) {
55
+ next({ name: 'login' })
56
+ } else {
57
+ next()
58
+ }
59
+ })
60
+
61
+ export default router
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { handleAuthCallback } from 'een-api-toolkit'
5
+
6
+ const router = useRouter()
7
+ const error = ref<string | null>(null)
8
+ const processing = ref(true)
9
+
10
+ onMounted(async () => {
11
+ const url = new URL(window.location.href)
12
+ const code = url.searchParams.get('code')
13
+ const state = url.searchParams.get('state')
14
+ const errorParam = url.searchParams.get('error')
15
+
16
+ if (errorParam) {
17
+ error.value = `OAuth error: ${errorParam}`
18
+ processing.value = false
19
+ return
20
+ }
21
+
22
+ if (!code || !state) {
23
+ error.value = 'Missing authorization code or state parameter'
24
+ processing.value = false
25
+ return
26
+ }
27
+
28
+ const result = await handleAuthCallback(code, state)
29
+
30
+ if (result.error) {
31
+ error.value = result.error.message
32
+ processing.value = false
33
+ return
34
+ }
35
+
36
+ // Success - redirect to home
37
+ router.push('/')
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <div class="callback">
43
+ <div v-if="processing" class="loading">
44
+ <h2>Authenticating...</h2>
45
+ <p>Please wait while we complete the login process.</p>
46
+ </div>
47
+
48
+ <div v-else-if="error" class="error-state">
49
+ <h2>Authentication Failed</h2>
50
+ <p class="error">{{ error }}</p>
51
+ <router-link to="/login">
52
+ <button>Try Again</button>
53
+ </router-link>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <style scoped>
59
+ .callback {
60
+ text-align: center;
61
+ max-width: 400px;
62
+ margin: 0 auto;
63
+ }
64
+
65
+ h2 {
66
+ margin-bottom: 20px;
67
+ }
68
+
69
+ .loading p {
70
+ color: #666;
71
+ }
72
+
73
+ .error-state .error {
74
+ margin-bottom: 20px;
75
+ }
76
+ </style>
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ import { useAuthStore, useCurrentUser, getAuthUrl } from 'een-api-toolkit'
3
+ import { computed, ref, watch } from 'vue'
4
+
5
+ const authStore = useAuthStore()
6
+ const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+ const loginError = ref<string | null>(null)
8
+
9
+ function login() {
10
+ try {
11
+ loginError.value = null
12
+ const authUrl = getAuthUrl()
13
+ window.location.href = authUrl
14
+ } catch (err) {
15
+ loginError.value = err instanceof Error ? err.message : 'Failed to initiate login'
16
+ console.error('Login error:', err)
17
+ }
18
+ }
19
+
20
+ // Don't fetch on mount - we'll handle it reactively
21
+ const { user, loading, error, fetch } = useCurrentUser({
22
+ immediate: false
23
+ })
24
+
25
+ // Guard to prevent concurrent fetch calls
26
+ let fetchInProgress = false
27
+
28
+ // Fetch user when authentication state changes
29
+ watch(
30
+ isAuthenticated,
31
+ async (isAuth) => {
32
+ if (isAuth && !user.value && !fetchInProgress) {
33
+ fetchInProgress = true
34
+ try {
35
+ await fetch()
36
+ } finally {
37
+ fetchInProgress = false
38
+ }
39
+ }
40
+ },
41
+ { immediate: true }
42
+ )
43
+ </script>
44
+
45
+ <template>
46
+ <div class="home">
47
+ <h2>Welcome to the EEN API Toolkit Example</h2>
48
+
49
+ <div v-if="!isAuthenticated" class="not-authenticated" data-testid="not-authenticated">
50
+ <p data-testid="not-authenticated-message">You are not logged in.</p>
51
+ <p v-if="loginError" class="error" data-testid="login-error">{{ loginError }}</p>
52
+ <button data-testid="login-button" @click="login">Login with Eagle Eye Networks</button>
53
+ </div>
54
+
55
+ <div v-else class="authenticated">
56
+ <div v-if="loading" class="loading">Loading user profile...</div>
57
+ <div v-else-if="error" class="error">Error: {{ error.message }}</div>
58
+ <div v-else-if="user" class="user-info">
59
+ <h3>Hello, {{ user.firstName }} {{ user.lastName }}!</h3>
60
+ <p>Email: {{ user.email }}</p>
61
+ <p>Account ID: {{ user.accountId }}</p>
62
+ </div>
63
+
64
+ <div class="actions">
65
+ <router-link to="/users">
66
+ <button>View Users</button>
67
+ </router-link>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <style scoped>
74
+ .home {
75
+ text-align: center;
76
+ }
77
+
78
+ h2 {
79
+ margin-bottom: 30px;
80
+ }
81
+
82
+ .not-authenticated,
83
+ .authenticated {
84
+ margin-top: 20px;
85
+ }
86
+
87
+ .user-info {
88
+ background: #f5f5f5;
89
+ padding: 20px;
90
+ border-radius: 8px;
91
+ margin-bottom: 20px;
92
+ }
93
+
94
+ .user-info h3 {
95
+ margin-bottom: 10px;
96
+ }
97
+
98
+ .user-info p {
99
+ color: #666;
100
+ }
101
+
102
+ .actions {
103
+ margin-top: 20px;
104
+ }
105
+ </style>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { getAuthUrl } from 'een-api-toolkit'
3
+
4
+ function login() {
5
+ // Redirect to EEN OAuth login
6
+ window.location.href = getAuthUrl()
7
+ }
8
+ </script>
9
+
10
+ <template>
11
+ <div class="login">
12
+ <h2 data-testid="login-title">Login</h2>
13
+ <p>Click the button below to login with your Eagle Eye Networks account.</p>
14
+ <button data-testid="login-button" @click="login">Login with Eagle Eye Networks</button>
15
+ </div>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .login {
20
+ text-align: center;
21
+ max-width: 400px;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ h2 {
26
+ margin-bottom: 20px;
27
+ }
28
+
29
+ p {
30
+ margin-bottom: 20px;
31
+ color: #666;
32
+ }
33
+ </style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { revokeToken } from 'een-api-toolkit'
5
+
6
+ const router = useRouter()
7
+ const processing = ref(true)
8
+ const error = ref<string | null>(null)
9
+
10
+ onMounted(async () => {
11
+ const result = await revokeToken()
12
+
13
+ if (result.error) {
14
+ // Even if revoke fails, the local state is cleared
15
+ console.warn('Token revocation failed:', result.error.message)
16
+ }
17
+
18
+ processing.value = false
19
+
20
+ // Redirect to home after a short delay
21
+ setTimeout(() => {
22
+ router.push('/')
23
+ }, 2000)
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <div class="logout">
29
+ <div v-if="processing">
30
+ <h2>Logging out...</h2>
31
+ <p class="loading">Please wait.</p>
32
+ </div>
33
+
34
+ <div v-else>
35
+ <h2>Logged Out</h2>
36
+ <p>You have been successfully logged out.</p>
37
+ <p v-if="error" class="error">Note: {{ error }}</p>
38
+ <p class="redirect">Redirecting to home page...</p>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .logout {
45
+ text-align: center;
46
+ max-width: 400px;
47
+ margin: 0 auto;
48
+ }
49
+
50
+ h2 {
51
+ margin-bottom: 20px;
52
+ }
53
+
54
+ .redirect {
55
+ color: #666;
56
+ font-style: italic;
57
+ margin-top: 20px;
58
+ }
59
+ </style>
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts">
2
+ import { useUsers } from 'een-api-toolkit'
3
+
4
+ const {
5
+ users,
6
+ loading,
7
+ error,
8
+ hasNextPage,
9
+ fetchNextPage,
10
+ refresh
11
+ } = useUsers({ pageSize: 10 })
12
+ </script>
13
+
14
+ <template>
15
+ <div class="users">
16
+ <div class="header">
17
+ <h2>Users</h2>
18
+ <button @click="refresh" :disabled="loading">
19
+ {{ loading ? 'Loading...' : 'Refresh' }}
20
+ </button>
21
+ </div>
22
+
23
+ <div v-if="loading && users.length === 0" class="loading">
24
+ Loading users...
25
+ </div>
26
+
27
+ <div v-else-if="error" class="error">
28
+ Error: {{ error.message }}
29
+ </div>
30
+
31
+ <div v-else>
32
+ <table v-if="users.length > 0">
33
+ <thead>
34
+ <tr>
35
+ <th>Name</th>
36
+ <th>Email</th>
37
+ <th>Status</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <tr v-for="user in users" :key="user.id">
42
+ <td>{{ user.firstName }} {{ user.lastName }}</td>
43
+ <td>{{ user.email }}</td>
44
+ <td>
45
+ <span :class="user.isActive ? 'active' : 'inactive'">
46
+ {{ user.isActive ? 'Active' : 'Inactive' }}
47
+ </span>
48
+ </td>
49
+ </tr>
50
+ </tbody>
51
+ </table>
52
+
53
+ <p v-else>No users found.</p>
54
+
55
+ <div v-if="hasNextPage" class="pagination">
56
+ <button @click="fetchNextPage" :disabled="loading">
57
+ {{ loading ? 'Loading...' : 'Load More' }}
58
+ </button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </template>
63
+
64
+ <style scoped>
65
+ .users {
66
+ max-width: 800px;
67
+ margin: 0 auto;
68
+ }
69
+
70
+ .header {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: center;
74
+ margin-bottom: 20px;
75
+ }
76
+
77
+ table {
78
+ width: 100%;
79
+ border-collapse: collapse;
80
+ }
81
+
82
+ th,
83
+ td {
84
+ padding: 12px;
85
+ text-align: left;
86
+ border-bottom: 1px solid #eee;
87
+ }
88
+
89
+ th {
90
+ background: #f5f5f5;
91
+ font-weight: 600;
92
+ }
93
+
94
+ .active {
95
+ color: #27ae60;
96
+ }
97
+
98
+ .inactive {
99
+ color: #e74c3c;
100
+ }
101
+
102
+ .pagination {
103
+ margin-top: 20px;
104
+ text-align: center;
105
+ }
106
+ </style>
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_EEN_CLIENT_ID: string
5
+ readonly VITE_PROXY_URL: string
6
+ readonly VITE_REDIRECT_URI?: string
7
+ readonly VITE_DEBUG?: string
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv
12
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "preserve",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }