een-api-toolkit 0.3.47 → 0.3.49
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/een-jobs-agent.md +676 -0
- package/CHANGELOG.md +7 -8
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1172 -28
- package/dist/index.js +796 -333
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +22 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1084 -0
- 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-jobs/.env.example +11 -0
- package/examples/vue-jobs/README.md +245 -0
- package/examples/vue-jobs/e2e/app.spec.ts +79 -0
- package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
- package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
- package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
- package/examples/vue-jobs/index.html +13 -0
- package/examples/vue-jobs/package-lock.json +1722 -0
- package/examples/vue-jobs/package.json +28 -0
- package/examples/vue-jobs/playwright.config.ts +47 -0
- package/examples/vue-jobs/src/App.vue +154 -0
- package/examples/vue-jobs/src/main.ts +25 -0
- package/examples/vue-jobs/src/router/index.ts +82 -0
- package/examples/vue-jobs/src/views/Callback.vue +76 -0
- package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
- package/examples/vue-jobs/src/views/Files.vue +424 -0
- package/examples/vue-jobs/src/views/Home.vue +195 -0
- package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
- package/examples/vue-jobs/src/views/Jobs.vue +297 -0
- package/examples/vue-jobs/src/views/Login.vue +33 -0
- package/examples/vue-jobs/src/views/Logout.vue +59 -0
- package/examples/vue-jobs/src/vite-env.d.ts +1 -0
- package/examples/vue-jobs/tsconfig.json +25 -0
- package/examples/vue-jobs/vite.config.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OAuth Proxy URL (required)
|
|
2
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
3
|
+
|
|
4
|
+
# EEN OAuth Client ID (required)
|
|
5
|
+
VITE_EEN_CLIENT_ID=your-client-id
|
|
6
|
+
|
|
7
|
+
# OAuth Redirect URI (must match exactly)
|
|
8
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
9
|
+
|
|
10
|
+
# Enable debug logging
|
|
11
|
+
VITE_DEBUG=true
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# EEN API Toolkit - Vue Jobs Example
|
|
2
|
+
|
|
3
|
+
A complete example showing how to use the Jobs, Files, and Exports APIs from the een-api-toolkit in a Vue 3 application.
|
|
4
|
+
|
|
5
|
+
## Storage Strategy: Memory
|
|
6
|
+
|
|
7
|
+
This example uses the `memory` storage strategy for maximum security. This means:
|
|
8
|
+
|
|
9
|
+
- **Tokens are never written to disk** - immune to localStorage/sessionStorage XSS attacks
|
|
10
|
+
- **Page refresh requires re-authentication** - tokens exist only in memory
|
|
11
|
+
- **Each tab is independent** - opening a new tab requires separate login
|
|
12
|
+
|
|
13
|
+
This is the recommended strategy for high-security deployments where protecting against XSS token theft is critical.
|
|
14
|
+
|
|
15
|
+
## Features Demonstrated
|
|
16
|
+
|
|
17
|
+
- OAuth authentication flow (login, callback, logout)
|
|
18
|
+
- Protected routes with navigation guards
|
|
19
|
+
- List jobs with state filtering (pending, started, success, failure)
|
|
20
|
+
- View job details with real-time progress polling
|
|
21
|
+
- Create video exports from cameras
|
|
22
|
+
- List and download files
|
|
23
|
+
- Error handling with Result pattern
|
|
24
|
+
- Reactive authentication state
|
|
25
|
+
|
|
26
|
+
## APIs Used
|
|
27
|
+
|
|
28
|
+
- `listJobs()` - List jobs with filtering and pagination
|
|
29
|
+
- `getJob()` - Get single job details
|
|
30
|
+
- `createExportJob()` - Create video export job
|
|
31
|
+
- `listFiles()` - List files with pagination
|
|
32
|
+
- `downloadFile()` - Download file content
|
|
33
|
+
- `getCameras()` - Get cameras for export selection
|
|
34
|
+
- `useAuthStore()` - Authentication state management
|
|
35
|
+
- `getAuthUrl()` - Generate OAuth login URL
|
|
36
|
+
- `handleAuthCallback()` - Process OAuth callback
|
|
37
|
+
- `initEenToolkit()` - Toolkit initialization
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
### Prerequisites
|
|
42
|
+
|
|
43
|
+
1. **Start the OAuth proxy** (required for authentication):
|
|
44
|
+
|
|
45
|
+
The OAuth proxy is a separate project that handles token management securely.
|
|
46
|
+
Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# In a separate terminal, from the een-oauth-proxy directory
|
|
50
|
+
npm install
|
|
51
|
+
npm run dev
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The proxy should be running at `http://localhost:8787`.
|
|
55
|
+
|
|
56
|
+
### Example Setup
|
|
57
|
+
|
|
58
|
+
All commands below should be run from this example directory (`examples/vue-jobs/`):
|
|
59
|
+
|
|
60
|
+
2. Copy the environment file:
|
|
61
|
+
```bash
|
|
62
|
+
# From examples/vue-jobs/
|
|
63
|
+
cp .env.example .env
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
3. Edit `.env` with your EEN credentials:
|
|
67
|
+
```env
|
|
68
|
+
VITE_EEN_CLIENT_ID=your-client-id
|
|
69
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
70
|
+
# DO NOT change the redirect URI - EEN IDP only permits this URL
|
|
71
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
4. Install dependencies and start:
|
|
75
|
+
```bash
|
|
76
|
+
# From examples/vue-jobs/
|
|
77
|
+
npm install
|
|
78
|
+
npm run dev
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
5. Open http://127.0.0.1:3333 in your browser.
|
|
82
|
+
|
|
83
|
+
**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.
|
|
84
|
+
|
|
85
|
+
**Note:** Development and testing was done on macOS. The `npm run stop` command uses `lsof`, which is not available on Windows. Windows users should manually stop any process on port 3333 or use `npx kill-port 3333` instead.
|
|
86
|
+
|
|
87
|
+
## Project Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
src/
|
|
91
|
+
├── main.ts # App entry, toolkit initialization
|
|
92
|
+
├── App.vue # Root component with navigation
|
|
93
|
+
├── router/
|
|
94
|
+
│ └── index.ts # Vue Router with auth guards
|
|
95
|
+
└── views/
|
|
96
|
+
├── Home.vue # Home page with user profile
|
|
97
|
+
├── Login.vue # OAuth login redirect
|
|
98
|
+
├── Callback.vue # OAuth callback handler
|
|
99
|
+
├── Jobs.vue # Job list with state filters
|
|
100
|
+
├── JobDetail.vue # Single job with progress polling
|
|
101
|
+
├── Files.vue # File browser with download
|
|
102
|
+
├── CreateExport.vue # Export creation form
|
|
103
|
+
└── Logout.vue # Logout handler
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Key Code Examples
|
|
107
|
+
|
|
108
|
+
### Initializing the Toolkit (main.ts)
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { initEenToolkit } from 'een-api-toolkit'
|
|
112
|
+
|
|
113
|
+
initEenToolkit({
|
|
114
|
+
proxyUrl: import.meta.env.VITE_PROXY_URL,
|
|
115
|
+
clientId: import.meta.env.VITE_EEN_CLIENT_ID,
|
|
116
|
+
storageStrategy: 'memory', // Maximum security - tokens lost on refresh
|
|
117
|
+
debug: true
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Listing Jobs with State Filtering (Jobs.vue)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { ref } from 'vue'
|
|
125
|
+
import { listJobs, type Job, type JobState, type ListJobsParams } from 'een-api-toolkit'
|
|
126
|
+
|
|
127
|
+
const jobs = ref<Job[]>([])
|
|
128
|
+
const selectedStates = ref<JobState[]>([])
|
|
129
|
+
|
|
130
|
+
async function fetchJobs(params: ListJobsParams) {
|
|
131
|
+
const mergedParams: ListJobsParams = { ...params }
|
|
132
|
+
if (selectedStates.value.length > 0) {
|
|
133
|
+
mergedParams.state__in = selectedStates.value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await listJobs(mergedParams)
|
|
137
|
+
if (result.error) {
|
|
138
|
+
// Handle error
|
|
139
|
+
} else {
|
|
140
|
+
jobs.value = result.data.results
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Polling Job Progress (JobDetail.vue)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
149
|
+
import { getJob, type Job } from 'een-api-toolkit'
|
|
150
|
+
|
|
151
|
+
const job = ref<Job | null>(null)
|
|
152
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
153
|
+
|
|
154
|
+
async function fetchJob(jobId: string) {
|
|
155
|
+
const result = await getJob(jobId)
|
|
156
|
+
if (result.error) {
|
|
157
|
+
// Handle error
|
|
158
|
+
} else {
|
|
159
|
+
job.value = result.data
|
|
160
|
+
|
|
161
|
+
// Auto-start polling if job is in progress
|
|
162
|
+
if (['pending', 'started'].includes(result.data.state)) {
|
|
163
|
+
startPolling()
|
|
164
|
+
} else {
|
|
165
|
+
stopPolling()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function startPolling() {
|
|
171
|
+
if (pollInterval) return
|
|
172
|
+
pollInterval = setInterval(() => fetchJob(jobId), 3000)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function stopPolling() {
|
|
176
|
+
if (pollInterval) {
|
|
177
|
+
clearInterval(pollInterval)
|
|
178
|
+
pollInterval = null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
onUnmounted(() => stopPolling())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Creating an Export (CreateExport.vue)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { createExportJob, formatTimestamp, type ExportType } from 'een-api-toolkit'
|
|
189
|
+
|
|
190
|
+
async function handleSubmit() {
|
|
191
|
+
const endTime = new Date()
|
|
192
|
+
const startTime = new Date(endTime.getTime() - duration.value * 60 * 1000)
|
|
193
|
+
|
|
194
|
+
const result = await createExportJob({
|
|
195
|
+
name: exportName.value || `Export - ${new Date().toLocaleString()}`,
|
|
196
|
+
type: exportType.value,
|
|
197
|
+
cameraId: selectedCamera.value,
|
|
198
|
+
startTimestamp: formatTimestamp(startTime.toISOString()),
|
|
199
|
+
endTimestamp: formatTimestamp(endTime.toISOString())
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (result.error) {
|
|
203
|
+
// Handle error
|
|
204
|
+
} else {
|
|
205
|
+
// Navigate to job detail page
|
|
206
|
+
router.push(`/jobs/${result.data.id}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Downloading Files (Files.vue)
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { downloadFile, type EenFile } from 'een-api-toolkit'
|
|
215
|
+
|
|
216
|
+
async function handleDownload(file: EenFile) {
|
|
217
|
+
const result = await downloadFile(file.id)
|
|
218
|
+
|
|
219
|
+
if (result.error) {
|
|
220
|
+
// Handle error
|
|
221
|
+
} else {
|
|
222
|
+
// Create download link
|
|
223
|
+
const url = URL.createObjectURL(result.data.blob)
|
|
224
|
+
const a = document.createElement('a')
|
|
225
|
+
a.href = url
|
|
226
|
+
a.download = result.data.filename || file.name
|
|
227
|
+
a.click()
|
|
228
|
+
URL.revokeObjectURL(url)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Auth Guard (router/index.ts)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
router.beforeEach((to, from, next) => {
|
|
237
|
+
const authStore = useAuthStore()
|
|
238
|
+
|
|
239
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
240
|
+
next('/login')
|
|
241
|
+
} else {
|
|
242
|
+
next()
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test.describe('Vue Jobs 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 Jobs/)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('header displays app name', async ({ page }) => {
|
|
13
|
+
await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN Jobs 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
|
+
// Jobs, Files, Create Export, and Logout should NOT be visible (requires auth)
|
|
24
|
+
await expect(page.locator('[data-testid="nav-jobs"]')).not.toBeVisible()
|
|
25
|
+
await expect(page.locator('[data-testid="nav-files"]')).not.toBeVisible()
|
|
26
|
+
await expect(page.locator('[data-testid="nav-create-export"]')).not.toBeVisible()
|
|
27
|
+
await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('home page shows not logged in message', async ({ page }) => {
|
|
31
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
32
|
+
await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
|
|
33
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('login page displays login button', async ({ page }) => {
|
|
37
|
+
await page.goto('/login')
|
|
38
|
+
|
|
39
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
40
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('protected route /jobs redirects to login', async ({ page }) => {
|
|
44
|
+
await page.goto('/jobs')
|
|
45
|
+
|
|
46
|
+
// Should be redirected to login page
|
|
47
|
+
await page.waitForURL('/login')
|
|
48
|
+
await expect(page).toHaveURL('/login')
|
|
49
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('protected route /files redirects to login', async ({ page }) => {
|
|
53
|
+
await page.goto('/files')
|
|
54
|
+
|
|
55
|
+
// Should be redirected to login page
|
|
56
|
+
await page.waitForURL('/login')
|
|
57
|
+
await expect(page).toHaveURL('/login')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('protected route /create-export redirects to login', async ({ page }) => {
|
|
61
|
+
await page.goto('/create-export')
|
|
62
|
+
|
|
63
|
+
// Should be redirected to login page
|
|
64
|
+
await page.waitForURL('/login')
|
|
65
|
+
await expect(page).toHaveURL('/login')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('navigation between pages works', async ({ page }) => {
|
|
69
|
+
// Click Login link
|
|
70
|
+
await page.click('[data-testid="nav-login"]')
|
|
71
|
+
await page.waitForURL('/login')
|
|
72
|
+
await expect(page).toHaveURL('/login')
|
|
73
|
+
|
|
74
|
+
// Click Home link
|
|
75
|
+
await page.click('[data-testid="nav-home"]')
|
|
76
|
+
await page.waitForURL('/')
|
|
77
|
+
await expect(page).toHaveURL('/')
|
|
78
|
+
})
|
|
79
|
+
})
|