een-api-toolkit 0.3.16 → 0.3.22
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 +146 -0
- package/.claude/agents/een-auth-agent.md +168 -0
- package/.claude/agents/een-devices-agent.md +294 -0
- package/.claude/agents/een-events-agent.md +375 -0
- package/.claude/agents/een-media-agent.md +256 -0
- package/.claude/agents/een-setup-agent.md +126 -0
- package/.claude/agents/een-users-agent.md +239 -0
- package/.claude/agents/test-runner.md +144 -0
- package/CHANGELOG.md +151 -10
- package/README.md +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +483 -260
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +128 -1648
- package/docs/ai-reference/AI-AUTH.md +288 -0
- package/docs/ai-reference/AI-DEVICES.md +569 -0
- package/docs/ai-reference/AI-EVENTS.md +1745 -0
- package/docs/ai-reference/AI-MEDIA.md +974 -0
- package/docs/ai-reference/AI-SETUP.md +267 -0
- package/docs/ai-reference/AI-USERS.md +255 -0
- package/examples/vue-event-subscriptions/.env.example +15 -0
- package/examples/vue-event-subscriptions/README.md +103 -0
- package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
- package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
- package/examples/vue-event-subscriptions/index.html +13 -0
- package/examples/vue-event-subscriptions/package-lock.json +1726 -0
- package/examples/vue-event-subscriptions/package.json +29 -0
- package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
- package/examples/vue-event-subscriptions/src/App.vue +193 -0
- package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-event-subscriptions/src/main.ts +25 -0
- package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
- package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
- package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
- package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
- package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +901 -0
- package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
- package/examples/vue-event-subscriptions/src/views/Logout.vue +65 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +389 -0
- package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
- package/examples/vue-event-subscriptions/tsconfig.json +21 -0
- package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
- package/examples/vue-event-subscriptions/vite.config.ts +12 -0
- package/examples/vue-events/package-lock.json +8 -1
- package/examples/vue-events/package.json +1 -0
- package/examples/vue-events/src/components/EventsModal.vue +269 -47
- package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-events/src/stores/mediaSession.ts +79 -0
- package/package.json +10 -2
- package/scripts/setup-agents.ts +116 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Vue 3 Application Setup - EEN API Toolkit
|
|
2
|
+
|
|
3
|
+
> **Version:** 0.3.22
|
|
4
|
+
>
|
|
5
|
+
> Complete guide for setting up a Vue 3 application with the een-api-toolkit.
|
|
6
|
+
> Load this document when creating a new project or troubleshooting setup issues.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
| Requirement | Details |
|
|
13
|
+
|-------------|---------|
|
|
14
|
+
| **Node.js** | Version 20 LTS or later |
|
|
15
|
+
| **Vue 3.x** | The toolkit is built for Vue 3 Composition API |
|
|
16
|
+
| **Pinia** | Required peer dependency for state management |
|
|
17
|
+
| **Vite** | Recommended build tool |
|
|
18
|
+
| **OAuth Proxy** | Required for secure token management |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Create Vue 3 Project
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm create vue@latest my-een-app
|
|
28
|
+
cd my-een-app
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Install Dependencies
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install een-api-toolkit pinia
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Configure main.ts
|
|
38
|
+
|
|
39
|
+
> **⚠️ CRITICAL:** Pinia MUST be installed on the Vue app BEFORE calling
|
|
40
|
+
> `initEenToolkit()` or using `useAuthStore()`.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { createApp } from 'vue'
|
|
44
|
+
import { createPinia } from 'pinia'
|
|
45
|
+
import { initEenToolkit } from 'een-api-toolkit'
|
|
46
|
+
import App from './App.vue'
|
|
47
|
+
import router from './router'
|
|
48
|
+
|
|
49
|
+
const app = createApp(App)
|
|
50
|
+
|
|
51
|
+
// Install Pinia (required before initEenToolkit)
|
|
52
|
+
app.use(createPinia())
|
|
53
|
+
|
|
54
|
+
// Initialize EEN API Toolkit with memory storage for maximum security
|
|
55
|
+
// Note: Using 'memory' storage means tokens are lost on page refresh
|
|
56
|
+
initEenToolkit({
|
|
57
|
+
proxyUrl: import.meta.env.VITE_PROXY_URL,
|
|
58
|
+
clientId: import.meta.env.VITE_EEN_CLIENT_ID,
|
|
59
|
+
redirectUri: import.meta.env.VITE_REDIRECT_URI,
|
|
60
|
+
storageStrategy: 'memory',
|
|
61
|
+
debug: import.meta.env.VITE_DEBUG === 'true'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Install router
|
|
65
|
+
app.use(router)
|
|
66
|
+
|
|
67
|
+
app.mount('#app')
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Configure vite.config.ts
|
|
72
|
+
|
|
73
|
+
> **⚠️ IMPORTANT:** Must use `127.0.0.1:3333` for EEN OAuth callback.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { defineConfig } from 'vite'
|
|
77
|
+
import vue from '@vitejs/plugin-vue'
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
plugins: [vue()],
|
|
81
|
+
server: {
|
|
82
|
+
// IMPORTANT: Must use 127.0.0.1:3333 for EEN OAuth callback
|
|
83
|
+
// The EEN Identity Provider only permits this specific redirect URI
|
|
84
|
+
host: '127.0.0.1',
|
|
85
|
+
port: 3333
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 5. Configure Environment Variables
|
|
92
|
+
|
|
93
|
+
Create `.env` file:
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
97
|
+
VITE_EEN_CLIENT_ID=your-een-client-id
|
|
98
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
99
|
+
VITE_DEBUG=true
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Router Setup
|
|
105
|
+
|
|
106
|
+
The router must handle OAuth callbacks on the root path and protect authenticated routes.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
110
|
+
import { useAuthStore } from 'een-api-toolkit'
|
|
111
|
+
import Home from '../views/Home.vue'
|
|
112
|
+
import Login from '../views/Login.vue'
|
|
113
|
+
import Callback from '../views/Callback.vue'
|
|
114
|
+
import Users from '../views/Users.vue'
|
|
115
|
+
import Logout from '../views/Logout.vue'
|
|
116
|
+
|
|
117
|
+
const router = createRouter({
|
|
118
|
+
history: createWebHistory(),
|
|
119
|
+
routes: [
|
|
120
|
+
{
|
|
121
|
+
path: '/',
|
|
122
|
+
name: 'home',
|
|
123
|
+
component: Home,
|
|
124
|
+
// Handle OAuth callback on root path (EEN IDP redirects to http://127.0.0.1:3333)
|
|
125
|
+
beforeEnter: (to, _from, next) => {
|
|
126
|
+
// If URL has code and state params, it's an OAuth callback
|
|
127
|
+
if (to.query.code && to.query.state) {
|
|
128
|
+
next({ name: 'callback', query: to.query })
|
|
129
|
+
} else {
|
|
130
|
+
next()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
path: '/login',
|
|
136
|
+
name: 'login',
|
|
137
|
+
component: Login
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
path: '/callback',
|
|
141
|
+
name: 'callback',
|
|
142
|
+
component: Callback
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
path: '/users',
|
|
146
|
+
name: 'users',
|
|
147
|
+
component: Users,
|
|
148
|
+
meta: { requiresAuth: true }
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
path: '/logout',
|
|
152
|
+
name: 'logout',
|
|
153
|
+
component: Logout
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Navigation guard for protected routes
|
|
159
|
+
router.beforeEach((to, _from, next) => {
|
|
160
|
+
const authStore = useAuthStore()
|
|
161
|
+
|
|
162
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
163
|
+
next({ name: 'login' })
|
|
164
|
+
} else {
|
|
165
|
+
next()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export default router
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Project Structure
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
my-een-app/
|
|
179
|
+
├── src/
|
|
180
|
+
│ ├── main.ts # Pinia + toolkit initialization
|
|
181
|
+
│ ├── App.vue # Root component
|
|
182
|
+
│ ├── router/
|
|
183
|
+
│ │ └── index.ts # Routes with auth guards
|
|
184
|
+
│ └── views/
|
|
185
|
+
│ ├── Home.vue # Landing page
|
|
186
|
+
│ ├── Login.vue # OAuth login trigger
|
|
187
|
+
│ ├── Logout.vue # Token revocation
|
|
188
|
+
│ └── Callback.vue # OAuth callback handler
|
|
189
|
+
├── .env # Environment variables
|
|
190
|
+
└── vite.config.ts # Vite configuration
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Common Setup Errors
|
|
196
|
+
|
|
197
|
+
### "getActivePinia() was called but there was no active Pinia"
|
|
198
|
+
|
|
199
|
+
**Cause:** Pinia was not installed before `initEenToolkit()` was called.
|
|
200
|
+
|
|
201
|
+
**Solution:** Ensure `app.use(pinia)` is called BEFORE `initEenToolkit()`:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const app = createApp(App)
|
|
205
|
+
app.use(createPinia()) // ✅ First
|
|
206
|
+
initEenToolkit({...}) // ✅ Second
|
|
207
|
+
app.mount('#app') // ✅ Last
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### "Redirect URI mismatch"
|
|
211
|
+
|
|
212
|
+
**Cause:** OAuth redirect URI doesn't exactly match `http://127.0.0.1:3333`.
|
|
213
|
+
|
|
214
|
+
**Solution:**
|
|
215
|
+
- Use `127.0.0.1` not `localhost`
|
|
216
|
+
- Use port `3333` exactly
|
|
217
|
+
- No trailing slash
|
|
218
|
+
- No path (not `/callback`)
|
|
219
|
+
- Register at [EEN Developer Portal](https://developer.eagleeyenetworks.com/page/my-application)
|
|
220
|
+
|
|
221
|
+
### Port 3333 already in use
|
|
222
|
+
|
|
223
|
+
**Solution:** Kill existing process:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
kill $(lsof -ti :3333) 2>/dev/null || true
|
|
227
|
+
npm run dev
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Configuration Options
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface EenToolkitConfig {
|
|
236
|
+
proxyUrl?: string // OAuth proxy URL (required)
|
|
237
|
+
clientId?: string // EEN OAuth client ID (required)
|
|
238
|
+
redirectUri?: string // OAuth redirect URI (default: http://127.0.0.1:3333)
|
|
239
|
+
storageStrategy?: StorageStrategy // Token storage strategy
|
|
240
|
+
debug?: boolean // Enable debug logging
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
type StorageStrategy = 'localStorage' | 'sessionStorage' | 'memory'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Storage Strategies
|
|
247
|
+
|
|
248
|
+
| Strategy | Persistence | Use Case |
|
|
249
|
+
|----------|-------------|----------|
|
|
250
|
+
| `localStorage` | Survives browser restart | Default, good for most apps |
|
|
251
|
+
| `sessionStorage` | Per-tab, cleared on close | Multi-account scenarios |
|
|
252
|
+
| `memory` | Lost on page refresh | Maximum security |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Next Steps
|
|
257
|
+
|
|
258
|
+
After setup, proceed to:
|
|
259
|
+
- [AI-AUTH.md](./AI-AUTH.md) - Implement login/logout flows
|
|
260
|
+
- [AI-USERS.md](./AI-USERS.md) - Work with user data
|
|
261
|
+
- [AI-DEVICES.md](./AI-DEVICES.md) - Work with cameras and bridges
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Reference Example
|
|
266
|
+
|
|
267
|
+
See `examples/vue-users/` for a complete working example.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Users API - EEN API Toolkit
|
|
2
|
+
|
|
3
|
+
> **Version:** 0.3.22
|
|
4
|
+
>
|
|
5
|
+
> Complete reference for user management.
|
|
6
|
+
> Load this document when working with user data.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Types
|
|
11
|
+
|
|
12
|
+
### User
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
interface User {
|
|
16
|
+
id: string
|
|
17
|
+
email: string
|
|
18
|
+
firstName: string
|
|
19
|
+
lastName: string
|
|
20
|
+
accountId?: string
|
|
21
|
+
timeZone?: string // IANA timezone (e.g., "America/Los_Angeles")
|
|
22
|
+
language?: string // ISO 639-1 code (e.g., "en")
|
|
23
|
+
phone?: string
|
|
24
|
+
mobilePhone?: string
|
|
25
|
+
permissions?: string[] // Requires include: ['permissions']
|
|
26
|
+
lastLogin?: string // ISO 8601 timestamp
|
|
27
|
+
isActive?: boolean
|
|
28
|
+
createdAt?: string
|
|
29
|
+
updatedAt?: string
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### UserProfile
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
interface UserProfile {
|
|
37
|
+
id: string
|
|
38
|
+
email: string
|
|
39
|
+
firstName: string
|
|
40
|
+
lastName: string
|
|
41
|
+
accountId?: string
|
|
42
|
+
timeZone?: string
|
|
43
|
+
language?: string
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Parameters
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface ListUsersParams {
|
|
51
|
+
pageSize?: number // Results per page (default: 100, max: 1000)
|
|
52
|
+
pageToken?: string // Pagination token
|
|
53
|
+
include?: string[] // Additional fields (e.g., ['permissions'])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface GetUserParams {
|
|
57
|
+
include?: string[] // Additional fields to include
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Functions
|
|
64
|
+
|
|
65
|
+
### getCurrentUser()
|
|
66
|
+
|
|
67
|
+
Get the current authenticated user's profile.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { getCurrentUser } from 'een-api-toolkit'
|
|
71
|
+
|
|
72
|
+
const { data, error } = await getCurrentUser()
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
if (error.code === 'AUTH_REQUIRED') {
|
|
76
|
+
router.push('/login')
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Welcome, ${data.firstName} ${data.lastName}`)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### getUsers(params?)
|
|
85
|
+
|
|
86
|
+
List users with optional pagination.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { getUsers } from 'een-api-toolkit'
|
|
90
|
+
|
|
91
|
+
// Basic usage
|
|
92
|
+
const { data, error } = await getUsers()
|
|
93
|
+
|
|
94
|
+
// With pagination
|
|
95
|
+
const { data } = await getUsers({ pageSize: 50 })
|
|
96
|
+
|
|
97
|
+
// With include
|
|
98
|
+
const { data } = await getUsers({ include: ['permissions'] })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### getUser(userId, params?)
|
|
102
|
+
|
|
103
|
+
Get a specific user by ID.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { getUser } from 'een-api-toolkit'
|
|
107
|
+
|
|
108
|
+
const { data, error } = await getUser('user-id-123')
|
|
109
|
+
|
|
110
|
+
if (error?.code === 'NOT_FOUND') {
|
|
111
|
+
console.log('User not found')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// With permissions
|
|
116
|
+
const { data: userWithPerms } = await getUser('user-id-123', {
|
|
117
|
+
include: ['permissions']
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Pagination Example
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
async function fetchAllUsers(): Promise<User[]> {
|
|
127
|
+
const allUsers: User[] = []
|
|
128
|
+
let pageToken: string | undefined
|
|
129
|
+
|
|
130
|
+
do {
|
|
131
|
+
const { data, error } = await getUsers({ pageSize: 100, pageToken })
|
|
132
|
+
if (error) break
|
|
133
|
+
allUsers.push(...data.results)
|
|
134
|
+
pageToken = data.nextPageToken
|
|
135
|
+
} while (pageToken)
|
|
136
|
+
|
|
137
|
+
return allUsers
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Complete Vue Component
|
|
144
|
+
|
|
145
|
+
```vue
|
|
146
|
+
<script setup lang="ts">
|
|
147
|
+
import { ref, computed, onMounted } from 'vue'
|
|
148
|
+
import { getUsers, type User, type EenError, type ListUsersParams } from 'een-api-toolkit'
|
|
149
|
+
|
|
150
|
+
// Reactive state
|
|
151
|
+
const users = ref<User[]>([])
|
|
152
|
+
const loading = ref(false)
|
|
153
|
+
const error = ref<EenError | null>(null)
|
|
154
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
155
|
+
|
|
156
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
157
|
+
|
|
158
|
+
const params = ref<ListUsersParams>({ pageSize: 10 })
|
|
159
|
+
|
|
160
|
+
async function fetchUsers(fetchParams?: ListUsersParams, append = false) {
|
|
161
|
+
loading.value = true
|
|
162
|
+
error.value = null
|
|
163
|
+
|
|
164
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
165
|
+
const result = await getUsers(mergedParams)
|
|
166
|
+
|
|
167
|
+
if (result.error) {
|
|
168
|
+
error.value = result.error
|
|
169
|
+
if (!append) {
|
|
170
|
+
users.value = []
|
|
171
|
+
}
|
|
172
|
+
nextPageToken.value = undefined
|
|
173
|
+
} else {
|
|
174
|
+
if (append) {
|
|
175
|
+
users.value = [...users.value, ...result.data.results]
|
|
176
|
+
} else {
|
|
177
|
+
users.value = result.data.results
|
|
178
|
+
}
|
|
179
|
+
nextPageToken.value = result.data.nextPageToken
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
loading.value = false
|
|
183
|
+
return result
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function refresh() {
|
|
187
|
+
return fetchUsers()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fetchNextPage() {
|
|
191
|
+
if (!nextPageToken.value) return
|
|
192
|
+
return fetchUsers({ ...params.value, pageToken: nextPageToken.value }, true)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
onMounted(() => {
|
|
196
|
+
fetchUsers()
|
|
197
|
+
})
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<template>
|
|
201
|
+
<div class="users">
|
|
202
|
+
<div class="header">
|
|
203
|
+
<h2>Users</h2>
|
|
204
|
+
<button @click="refresh" :disabled="loading">
|
|
205
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div v-if="loading && users.length === 0" class="loading">
|
|
210
|
+
Loading users...
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div v-else-if="error" class="error">
|
|
214
|
+
Error: {{ error.message }}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div v-else>
|
|
218
|
+
<table v-if="users.length > 0">
|
|
219
|
+
<thead>
|
|
220
|
+
<tr>
|
|
221
|
+
<th>Name</th>
|
|
222
|
+
<th>Email</th>
|
|
223
|
+
<th>Status</th>
|
|
224
|
+
</tr>
|
|
225
|
+
</thead>
|
|
226
|
+
<tbody>
|
|
227
|
+
<tr v-for="user in users" :key="user.id">
|
|
228
|
+
<td>{{ user.firstName }} {{ user.lastName }}</td>
|
|
229
|
+
<td>{{ user.email }}</td>
|
|
230
|
+
<td>
|
|
231
|
+
<span :class="user.isActive ? 'active' : 'inactive'">
|
|
232
|
+
{{ user.isActive ? 'Active' : 'Inactive' }}
|
|
233
|
+
</span>
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
</tbody>
|
|
237
|
+
</table>
|
|
238
|
+
|
|
239
|
+
<p v-else>No users found.</p>
|
|
240
|
+
|
|
241
|
+
<div v-if="hasNextPage" class="pagination">
|
|
242
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
243
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</template>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Reference Example
|
|
254
|
+
|
|
255
|
+
See `examples/vue-users/src/views/Users.vue`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# EEN API Toolkit Configuration
|
|
2
|
+
# Copy this file to .env and fill in your values
|
|
3
|
+
|
|
4
|
+
# OAuth Proxy URL (required)
|
|
5
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
6
|
+
|
|
7
|
+
# EEN OAuth Client ID (required)
|
|
8
|
+
VITE_EEN_CLIENT_ID=your_client_id_here
|
|
9
|
+
|
|
10
|
+
# OAuth Redirect URI (required)
|
|
11
|
+
# Must match the redirect URI configured in your EEN OAuth client
|
|
12
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
13
|
+
|
|
14
|
+
# Enable debug logging (optional)
|
|
15
|
+
VITE_DEBUG=true
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Event Subscriptions Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to use the Event Subscriptions API from the EEN API Toolkit to create SSE subscriptions and receive real-time events from cameras.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Create and delete event subscriptions with SSE delivery
|
|
8
|
+
- View active subscriptions with pagination
|
|
9
|
+
- Select cameras and event types for filtering
|
|
10
|
+
- Connect to SSE streams for live events
|
|
11
|
+
- Real-time event display with auto-scrolling
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
1. Node.js 20 LTS or later
|
|
16
|
+
2. EEN OAuth credentials (client ID and secret)
|
|
17
|
+
3. Running OAuth proxy server (from `../een-oauth-proxy`)
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
1. Copy the environment file:
|
|
22
|
+
```bash
|
|
23
|
+
cp .env.example .env
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Edit `.env` with your configuration:
|
|
27
|
+
- `VITE_PROXY_URL`: URL of your OAuth proxy server
|
|
28
|
+
- `VITE_EEN_CLIENT_ID`: Your EEN OAuth client ID
|
|
29
|
+
- `VITE_REDIRECT_URI`: OAuth redirect URI (default: http://127.0.0.1:3333)
|
|
30
|
+
- `VITE_DEBUG`: Enable debug logging (optional)
|
|
31
|
+
|
|
32
|
+
3. Install dependencies:
|
|
33
|
+
```bash
|
|
34
|
+
npm install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. Start the OAuth proxy (in a separate terminal):
|
|
38
|
+
```bash
|
|
39
|
+
cd ../een-oauth-proxy
|
|
40
|
+
npm run dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
5. Start the example app:
|
|
44
|
+
```bash
|
|
45
|
+
npm run dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
6. Open http://127.0.0.1:3333 in your browser
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Creating a Subscription
|
|
53
|
+
|
|
54
|
+
1. Log in with your Eagle Eye Networks account
|
|
55
|
+
2. Go to "Subscriptions" page
|
|
56
|
+
3. Select one or more cameras from the dropdown
|
|
57
|
+
4. Select one or more event types
|
|
58
|
+
5. Click "Create Subscription"
|
|
59
|
+
|
|
60
|
+
### Viewing Live Events
|
|
61
|
+
|
|
62
|
+
1. Go to "Live Events" page
|
|
63
|
+
2. Select a subscription from the dropdown
|
|
64
|
+
3. Click "Connect"
|
|
65
|
+
4. Events will appear in real-time as they occur
|
|
66
|
+
5. Click "Disconnect" to stop receiving events
|
|
67
|
+
|
|
68
|
+
## API Functions Used
|
|
69
|
+
|
|
70
|
+
- `listEventSubscriptions()` - List all subscriptions
|
|
71
|
+
- `getEventSubscription(id)` - Get a specific subscription
|
|
72
|
+
- `createEventSubscription(params)` - Create a new subscription
|
|
73
|
+
- `deleteEventSubscription(id)` - Delete a subscription
|
|
74
|
+
- `connectToEventSubscription(sseUrl, options)` - Connect to SSE stream
|
|
75
|
+
- `getCameras()` - List cameras for filter selection
|
|
76
|
+
- `listEventTypes()` - List event types for filter selection
|
|
77
|
+
|
|
78
|
+
## Event Subscription Lifecycle
|
|
79
|
+
|
|
80
|
+
SSE subscriptions are **temporary** by default:
|
|
81
|
+
- Automatically created with a time-to-live (TTL)
|
|
82
|
+
- Deleted when no client is connected after TTL expires
|
|
83
|
+
- Can be manually deleted at any time
|
|
84
|
+
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
### No events appearing
|
|
88
|
+
|
|
89
|
+
- Ensure the camera has activity (motion, etc.)
|
|
90
|
+
- Check that the subscription has the correct event types
|
|
91
|
+
- Verify the SSE connection status shows "connected"
|
|
92
|
+
|
|
93
|
+
### Connection errors
|
|
94
|
+
|
|
95
|
+
- Verify the OAuth proxy is running
|
|
96
|
+
- Check that your token is valid (re-login if needed)
|
|
97
|
+
- Ensure the subscription still exists (may have expired)
|
|
98
|
+
|
|
99
|
+
### Cannot create subscription
|
|
100
|
+
|
|
101
|
+
- Ensure you have cameras in your account
|
|
102
|
+
- Check that event types are loaded
|
|
103
|
+
- Verify your account has permission to create subscriptions
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test.describe('Event Subscriptions 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(/Event Subscriptions/)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('header displays app name', async ({ page }) => {
|
|
13
|
+
await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN Event Subscriptions')
|
|
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
|
+
// Subscriptions, Live, and Logout should NOT be visible (requires auth)
|
|
24
|
+
await expect(page.locator('[data-testid="nav-subscriptions"]')).not.toBeVisible()
|
|
25
|
+
await expect(page.locator('[data-testid="nav-live"]')).not.toBeVisible()
|
|
26
|
+
await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('home page shows not logged in message', async ({ page }) => {
|
|
30
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
31
|
+
await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
|
|
32
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('login page displays login button', async ({ page }) => {
|
|
36
|
+
await page.goto('/login')
|
|
37
|
+
|
|
38
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
39
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('protected route (subscriptions) redirects to login', async ({ page }) => {
|
|
43
|
+
await page.goto('/subscriptions')
|
|
44
|
+
|
|
45
|
+
// Should be redirected to login page
|
|
46
|
+
await page.waitForURL('/login')
|
|
47
|
+
await expect(page).toHaveURL('/login')
|
|
48
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('protected route (live) redirects to login', async ({ page }) => {
|
|
52
|
+
await page.goto('/live')
|
|
53
|
+
|
|
54
|
+
// Should be redirected to login page
|
|
55
|
+
await page.waitForURL('/login')
|
|
56
|
+
await expect(page).toHaveURL('/login')
|
|
57
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('navigation between pages works', async ({ page }) => {
|
|
61
|
+
// Click Login link
|
|
62
|
+
await page.click('[data-testid="nav-login"]')
|
|
63
|
+
await page.waitForURL('/login')
|
|
64
|
+
await expect(page).toHaveURL('/login')
|
|
65
|
+
|
|
66
|
+
// Click Home link
|
|
67
|
+
await page.click('[data-testid="nav-home"]')
|
|
68
|
+
await page.waitForURL('/')
|
|
69
|
+
await expect(page).toHaveURL('/')
|
|
70
|
+
})
|
|
71
|
+
})
|