een-api-toolkit 0.3.38 → 0.3.46
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/.claude/agents/docs-accuracy-reviewer.md +35 -5
- package/.claude/agents/een-auth-agent.md +28 -0
- package/.claude/agents/een-automations-agent.md +264 -0
- package/.claude/agents/een-devices-agent.md +5 -7
- package/.claude/agents/een-events-agent.md +40 -18
- package/.claude/agents/een-media-agent.md +12 -15
- package/.claude/agents/een-setup-agent.md +32 -0
- package/.claude/agents/een-users-agent.md +2 -2
- package/CHANGELOG.md +9 -75
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +815 -0
- package/dist/index.js +986 -719
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +17 -1
- package/docs/ai-reference/AI-AUTH.md +42 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +833 -0
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +2 -2
- package/docs/ai-reference/AI-GROUPING.md +128 -66
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-alerts-metrics/src/App.vue +7 -1
- package/examples/vue-automations/.env.example +11 -0
- package/examples/vue-automations/README.md +205 -0
- package/examples/vue-automations/e2e/app.spec.ts +83 -0
- package/examples/vue-automations/e2e/auth.spec.ts +468 -0
- package/examples/vue-automations/index.html +13 -0
- package/examples/vue-automations/package-lock.json +1722 -0
- package/examples/vue-automations/package.json +29 -0
- package/examples/vue-automations/playwright.config.ts +46 -0
- package/examples/vue-automations/src/App.vue +122 -0
- package/examples/vue-automations/src/main.ts +23 -0
- package/examples/vue-automations/src/router/index.ts +61 -0
- package/examples/vue-automations/src/views/Automations.vue +692 -0
- package/examples/vue-automations/src/views/Callback.vue +76 -0
- package/examples/vue-automations/src/views/Home.vue +172 -0
- package/examples/vue-automations/src/views/Login.vue +33 -0
- package/examples/vue-automations/src/views/Logout.vue +66 -0
- package/examples/vue-automations/src/vite-env.d.ts +1 -0
- package/examples/vue-automations/tsconfig.json +21 -0
- package/examples/vue-automations/tsconfig.node.json +10 -0
- package/examples/vue-automations/vite.config.ts +12 -0
- package/examples/vue-bridges/e2e/auth.spec.ts +97 -0
- package/examples/vue-bridges/src/App.vue +7 -1
- package/examples/vue-cameras/src/App.vue +7 -1
- package/examples/vue-event-subscriptions/src/App.vue +7 -1
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +1 -1
- package/examples/vue-events/src/App.vue +7 -1
- package/examples/vue-layouts/src/App.vue +7 -1
- package/examples/vue-users/package-lock.json +2 -2
- package/examples/vue-users/package.json +1 -1
- package/examples/vue-users/src/App.vue +7 -1
- package/package.json +1 -1
|
@@ -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 automations
|
|
37
|
+
router.push('/automations')
|
|
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,172 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAuthStore, getCurrentUser, getStorageStrategy, STORAGE_STRATEGY_DESCRIPTIONS, type UserProfile, type EenError } from 'een-api-toolkit'
|
|
3
|
+
import { computed, ref, onMounted } from 'vue'
|
|
4
|
+
|
|
5
|
+
const authStore = useAuthStore()
|
|
6
|
+
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
const storageStrategy = getStorageStrategy()
|
|
9
|
+
const storageDescription = STORAGE_STRATEGY_DESCRIPTIONS[storageStrategy]
|
|
10
|
+
|
|
11
|
+
// Reactive state for current user
|
|
12
|
+
const user = ref<UserProfile | null>(null)
|
|
13
|
+
const loading = ref(false)
|
|
14
|
+
const error = ref<EenError | null>(null)
|
|
15
|
+
|
|
16
|
+
async function fetchUser() {
|
|
17
|
+
if (!isAuthenticated.value) return
|
|
18
|
+
|
|
19
|
+
loading.value = true
|
|
20
|
+
error.value = null
|
|
21
|
+
|
|
22
|
+
const result = await getCurrentUser()
|
|
23
|
+
if (result.error) {
|
|
24
|
+
error.value = result.error
|
|
25
|
+
user.value = null
|
|
26
|
+
} else {
|
|
27
|
+
user.value = result.data
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loading.value = false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
if (isAuthenticated.value) {
|
|
35
|
+
fetchUser()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div class="home">
|
|
42
|
+
<h2>Welcome to the EEN Automations Example</h2>
|
|
43
|
+
|
|
44
|
+
<div v-if="isAuthenticated" data-testid="authenticated">
|
|
45
|
+
<div v-if="loading" class="loading">Loading user info...</div>
|
|
46
|
+
<div v-else-if="error" class="error">{{ error.message }}</div>
|
|
47
|
+
<div v-else-if="user" class="user-info" data-testid="user-info">
|
|
48
|
+
<p>Logged in as: <strong>{{ user.firstName }} {{ user.lastName }}</strong></p>
|
|
49
|
+
<p>Email: {{ user.email }}</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="actions">
|
|
53
|
+
<router-link to="/automations">
|
|
54
|
+
<button data-testid="view-automations-button">View Automations</button>
|
|
55
|
+
</router-link>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div v-else class="login-prompt" data-testid="not-authenticated">
|
|
60
|
+
<p>Please log in to view automation rules.</p>
|
|
61
|
+
<router-link to="/login">
|
|
62
|
+
<button data-testid="login-button">Login</button>
|
|
63
|
+
</router-link>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="description">
|
|
67
|
+
<h3>About This Example</h3>
|
|
68
|
+
<p>
|
|
69
|
+
This example demonstrates how to use the automation functions from the
|
|
70
|
+
EEN API Toolkit to display and filter automation rules.
|
|
71
|
+
</p>
|
|
72
|
+
<h4>Features</h4>
|
|
73
|
+
<ul>
|
|
74
|
+
<li>List event alert condition rules</li>
|
|
75
|
+
<li>List alert condition rules with actions and insights</li>
|
|
76
|
+
<li>List alert action rules</li>
|
|
77
|
+
<li>List alert actions (notifications, webhooks, etc.)</li>
|
|
78
|
+
<li>Filter by enabled status</li>
|
|
79
|
+
<li>Pagination support</li>
|
|
80
|
+
</ul>
|
|
81
|
+
<h4>Functions Used</h4>
|
|
82
|
+
<ul>
|
|
83
|
+
<li><code>listEventAlertConditionRules()</code></li>
|
|
84
|
+
<li><code>listAlertConditionRules()</code></li>
|
|
85
|
+
<li><code>listAlertActionRules()</code></li>
|
|
86
|
+
<li><code>listAlertActions()</code></li>
|
|
87
|
+
</ul>
|
|
88
|
+
<p class="storage-note" data-testid="storage-strategy">
|
|
89
|
+
Storage strategy: <strong>{{ storageStrategy }}</strong> ({{ storageDescription }})
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.home {
|
|
97
|
+
max-width: 600px;
|
|
98
|
+
margin: 0 auto;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
h2 {
|
|
102
|
+
margin-bottom: 20px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.user-info {
|
|
106
|
+
background: #f5f5f5;
|
|
107
|
+
padding: 15px;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
margin-bottom: 20px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.user-info p {
|
|
113
|
+
margin: 5px 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.actions {
|
|
117
|
+
margin: 20px 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.login-prompt {
|
|
121
|
+
text-align: center;
|
|
122
|
+
padding: 20px;
|
|
123
|
+
background: #f5f5f5;
|
|
124
|
+
border-radius: 4px;
|
|
125
|
+
margin-bottom: 20px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.login-prompt p {
|
|
129
|
+
margin-bottom: 15px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.description {
|
|
133
|
+
margin-top: 40px;
|
|
134
|
+
padding-top: 20px;
|
|
135
|
+
border-top: 1px solid #eee;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.description h3 {
|
|
139
|
+
margin-bottom: 10px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.description h4 {
|
|
143
|
+
margin-top: 15px;
|
|
144
|
+
margin-bottom: 10px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.description p {
|
|
148
|
+
color: #666;
|
|
149
|
+
margin-bottom: 15px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.description ul {
|
|
153
|
+
list-style: disc;
|
|
154
|
+
padding-left: 20px;
|
|
155
|
+
color: #666;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.description code {
|
|
159
|
+
background: #f0f0f0;
|
|
160
|
+
padding: 2px 6px;
|
|
161
|
+
border-radius: 3px;
|
|
162
|
+
font-size: 0.9em;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.storage-note {
|
|
166
|
+
margin-top: 20px;
|
|
167
|
+
padding-top: 15px;
|
|
168
|
+
border-top: 1px solid #ddd;
|
|
169
|
+
font-size: 0.85em;
|
|
170
|
+
color: #888;
|
|
171
|
+
}
|
|
172
|
+
</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 @click="login" data-testid="login-button">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,66 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { revokeToken, useAuthStore } from 'een-api-toolkit'
|
|
5
|
+
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
const authStore = useAuthStore()
|
|
8
|
+
const error = ref<string | null>(null)
|
|
9
|
+
const processing = ref(true)
|
|
10
|
+
|
|
11
|
+
onMounted(async () => {
|
|
12
|
+
if (!authStore.isAuthenticated) {
|
|
13
|
+
// Already logged out
|
|
14
|
+
router.push('/')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = await revokeToken()
|
|
19
|
+
|
|
20
|
+
if (result.error) {
|
|
21
|
+
error.value = result.error.message
|
|
22
|
+
processing.value = false
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Success - redirect to home
|
|
27
|
+
router.push('/')
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="logout">
|
|
33
|
+
<div v-if="processing" class="loading">
|
|
34
|
+
<h2>Logging out...</h2>
|
|
35
|
+
<p>Please wait while we complete the logout process.</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div v-else-if="error" class="error-state">
|
|
39
|
+
<h2>Logout Failed</h2>
|
|
40
|
+
<p class="error">{{ error }}</p>
|
|
41
|
+
<router-link to="/">
|
|
42
|
+
<button>Return Home</button>
|
|
43
|
+
</router-link>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.logout {
|
|
50
|
+
text-align: center;
|
|
51
|
+
max-width: 400px;
|
|
52
|
+
margin: 0 auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h2 {
|
|
56
|
+
margin-bottom: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.loading p {
|
|
60
|
+
color: #666;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.error-state .error {
|
|
64
|
+
margin-bottom: 20px;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -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,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [vue()],
|
|
6
|
+
server: {
|
|
7
|
+
// IMPORTANT: Must use 127.0.0.1:3333 for EEN OAuth callback
|
|
8
|
+
// The EEN Identity Provider only permits this specific redirect URI
|
|
9
|
+
host: '127.0.0.1',
|
|
10
|
+
port: 3333
|
|
11
|
+
}
|
|
12
|
+
})
|
|
@@ -203,4 +203,101 @@ test.describe('Vue Bridges Example - Auth', () => {
|
|
|
203
203
|
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
204
204
|
await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
|
|
205
205
|
})
|
|
206
|
+
|
|
207
|
+
test('after logout, localStorage is cleared and new login flow is required', async ({ page }) => {
|
|
208
|
+
skipIfNoProxy()
|
|
209
|
+
skipIfNoCredentials()
|
|
210
|
+
|
|
211
|
+
// First login
|
|
212
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
213
|
+
await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
214
|
+
|
|
215
|
+
// Verify we're authenticated
|
|
216
|
+
await expect(page.locator('[data-testid="nav-bridges"]')).toBeVisible()
|
|
217
|
+
|
|
218
|
+
// Logout
|
|
219
|
+
await page.click('[data-testid="nav-logout"]')
|
|
220
|
+
await page.waitForURL('**/')
|
|
221
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
222
|
+
|
|
223
|
+
// Verify localStorage is cleared (session data removed)
|
|
224
|
+
const tokenAfterLogout = await page.evaluate(() => localStorage.getItem('een_token'))
|
|
225
|
+
expect(tokenAfterLogout).toBeNull()
|
|
226
|
+
|
|
227
|
+
const sessionIdAfterLogout = await page.evaluate(() => localStorage.getItem('een_sessionId'))
|
|
228
|
+
expect(sessionIdAfterLogout).toBeNull()
|
|
229
|
+
|
|
230
|
+
const hostnameAfterLogout = await page.evaluate(() => localStorage.getItem('een_hostname'))
|
|
231
|
+
expect(hostnameAfterLogout).toBeNull()
|
|
232
|
+
|
|
233
|
+
// Verify the app shows not-authenticated state (proves our session is cleared)
|
|
234
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
235
|
+
await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
|
|
236
|
+
await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
|
|
237
|
+
|
|
238
|
+
// Click login to start new OAuth flow
|
|
239
|
+
await page.click('[data-testid="login-button"]')
|
|
240
|
+
await page.waitForURL('/login')
|
|
241
|
+
|
|
242
|
+
// Click OAuth login button - this initiates the OAuth redirect
|
|
243
|
+
await page.click('button:has-text("Login with Eagle Eye Networks")')
|
|
244
|
+
|
|
245
|
+
// Wait for either:
|
|
246
|
+
// 1. OAuth page (if IDP session expired) - user needs to enter credentials
|
|
247
|
+
// 2. Callback/bridges page (if IDP remembers user via cookies) - auto-login
|
|
248
|
+
// Either way, our localStorage was cleared and user had to initiate a new login
|
|
249
|
+
await page.waitForURL(/eagleeyenetworks\.com|127\.0\.0\.1:3333/, { timeout: TIMEOUTS.AUTH_COMPLETE })
|
|
250
|
+
|
|
251
|
+
// If we ended up back at our app (auto-login via IDP cookies), verify we're authenticated again
|
|
252
|
+
const currentUrl = page.url()
|
|
253
|
+
if (currentUrl.includes('127.0.0.1:3333')) {
|
|
254
|
+
// Auto-logged in - wait for the full flow to complete
|
|
255
|
+
await page.waitForURL('**/bridges', { timeout: TIMEOUTS.AUTH_COMPLETE })
|
|
256
|
+
await expect(page.locator('[data-testid="nav-bridges"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
257
|
+
await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible()
|
|
258
|
+
|
|
259
|
+
// Verify localStorage has new token (proves it was cleared and refilled)
|
|
260
|
+
const newToken = await page.evaluate(() => localStorage.getItem('een_token'))
|
|
261
|
+
expect(newToken).not.toBeNull()
|
|
262
|
+
} else {
|
|
263
|
+
// At OAuth page - user needs to enter credentials
|
|
264
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
265
|
+
await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('localStorage: second tab shares session without re-login', async ({ page, context }) => {
|
|
270
|
+
skipIfNoProxy()
|
|
271
|
+
skipIfNoCredentials()
|
|
272
|
+
|
|
273
|
+
// First tab: login
|
|
274
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
275
|
+
await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
276
|
+
|
|
277
|
+
// Verify localStorage has token
|
|
278
|
+
const tokenInFirstTab = await page.evaluate(() => localStorage.getItem('een_token'))
|
|
279
|
+
expect(tokenInFirstTab).not.toBeNull()
|
|
280
|
+
|
|
281
|
+
// Open second tab in same browser context (shares localStorage)
|
|
282
|
+
const secondTab = await context.newPage()
|
|
283
|
+
await secondTab.goto('/')
|
|
284
|
+
|
|
285
|
+
// Second tab should be authenticated immediately (no login needed)
|
|
286
|
+
// because localStorage is shared and App.vue calls initialize()
|
|
287
|
+
await expect(secondTab.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
288
|
+
await expect(secondTab.locator('[data-testid="nav-bridges"]')).toBeVisible()
|
|
289
|
+
await expect(secondTab.locator('[data-testid="nav-login"]')).not.toBeVisible()
|
|
290
|
+
|
|
291
|
+
// Verify second tab has the same token from localStorage
|
|
292
|
+
const tokenInSecondTab = await secondTab.evaluate(() => localStorage.getItem('een_token'))
|
|
293
|
+
expect(tokenInSecondTab).toBe(tokenInFirstTab)
|
|
294
|
+
|
|
295
|
+
// Navigate to bridges in second tab - should work without login
|
|
296
|
+
await secondTab.click('[data-testid="nav-bridges"]')
|
|
297
|
+
await secondTab.waitForURL('**/bridges')
|
|
298
|
+
await expect(secondTab.locator('.bridge-grid, .no-bridges')).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
299
|
+
|
|
300
|
+
// Clean up second tab
|
|
301
|
+
await secondTab.close()
|
|
302
|
+
})
|
|
206
303
|
})
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
const authStore = useAuthStore()
|
|
6
6
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
// Initialize auth store from storage on app mount
|
|
9
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
authStore.initialize()
|
|
12
|
+
})
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
const authStore = useAuthStore()
|
|
6
6
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
// Initialize auth store from storage on app mount
|
|
9
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
authStore.initialize()
|
|
12
|
+
})
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
import { useRouter } from 'vue-router'
|
|
5
5
|
|
|
6
6
|
const authStore = useAuthStore()
|
|
7
7
|
const router = useRouter()
|
|
8
8
|
|
|
9
9
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
10
|
+
|
|
11
|
+
// Initialize auth store from storage on app mount
|
|
12
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
authStore.initialize()
|
|
15
|
+
})
|
|
10
16
|
const refreshFailed = computed(() => authStore.refreshFailed)
|
|
11
17
|
const refreshFailedMessage = computed(() => authStore.refreshFailedMessage)
|
|
12
18
|
const isRefreshing = computed(() => authStore.isRefreshing)
|
|
@@ -380,7 +380,7 @@ watch(selectedSubscriptionId, (newId) => {
|
|
|
380
380
|
<ul class="warning-list">
|
|
381
381
|
<li>SSE URLs are single-use. Once disconnected, the subscription cannot be reconnected.</li>
|
|
382
382
|
<li>To receive events again after disconnecting, create a new subscription.</li>
|
|
383
|
-
<li>
|
|
383
|
+
<li>SSE subscriptions have a server-determined 15-minute TTL (not configurable) and expire if not connected.</li>
|
|
384
384
|
</ul>
|
|
385
385
|
</div>
|
|
386
386
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
const authStore = useAuthStore()
|
|
6
6
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
// Initialize auth store from storage on app mount
|
|
9
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
authStore.initialize()
|
|
12
|
+
})
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
const authStore = useAuthStore()
|
|
6
6
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
// Initialize auth store from storage on app mount
|
|
9
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
authStore.initialize()
|
|
12
|
+
})
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "een-api-toolkit-example",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "een-api-toolkit-example",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.26",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"een-api-toolkit": "file:../..",
|
|
12
12
|
"pinia": "^3.0.4",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { onMounted, computed } from 'vue'
|
|
2
3
|
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
4
|
|
|
5
5
|
const authStore = useAuthStore()
|
|
6
6
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
|
|
8
|
+
// Initialize auth store from storage on app mount
|
|
9
|
+
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
authStore.initialize()
|
|
12
|
+
})
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|