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.
- package/CHANGELOG.md +257 -146
- package/README.md +108 -230
- package/docs/AI-CONTEXT.md +767 -0
- package/examples/vue-basic/.env.example +12 -0
- package/examples/vue-basic/README.md +146 -0
- package/examples/vue-basic/e2e/app.spec.ts +61 -0
- package/examples/vue-basic/e2e/auth.spec.ts +260 -0
- package/examples/vue-basic/index.html +13 -0
- package/examples/vue-basic/package-lock.json +1583 -0
- package/examples/vue-basic/package.json +28 -0
- package/examples/vue-basic/playwright.config.ts +47 -0
- package/examples/vue-basic/src/App.vue +108 -0
- package/examples/vue-basic/src/main.ts +23 -0
- package/examples/vue-basic/src/router/index.ts +61 -0
- package/examples/vue-basic/src/views/Callback.vue +76 -0
- package/examples/vue-basic/src/views/Home.vue +105 -0
- package/examples/vue-basic/src/views/Login.vue +33 -0
- package/examples/vue-basic/src/views/Logout.vue +59 -0
- package/examples/vue-basic/src/views/Users.vue +106 -0
- package/examples/vue-basic/src/vite-env.d.ts +12 -0
- package/examples/vue-basic/tsconfig.json +21 -0
- package/examples/vue-basic/tsconfig.node.json +10 -0
- package/examples/vue-basic/vite.config.ts +12 -0
- package/package.json +3 -1
|
@@ -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
|
+
}
|