adminforth 2.27.0-next.71 → 2.27.0-next.73
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/dist/spa/src/App.vue +55 -55
- package/dist/spa/src/utils/utils.ts +102 -14
- package/package.json +1 -1
package/dist/spa/src/App.vue
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</svg>
|
|
16
16
|
</button>
|
|
17
17
|
</div>
|
|
18
|
-
<div class="flex items-center">
|
|
18
|
+
<div class="flex items-center gap-4">
|
|
19
19
|
<component
|
|
20
20
|
v-if="coreStore?.adminUser"
|
|
21
21
|
v-for="c in coreStore?.config?.globalInjections?.header || []"
|
|
@@ -24,61 +24,61 @@
|
|
|
24
24
|
:adminUser="coreStore.adminUser"
|
|
25
25
|
/>
|
|
26
26
|
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</
|
|
55
|
-
</
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</
|
|
65
|
-
|
|
66
|
-
<ul class="py-1" role="none">
|
|
67
|
-
<li v-for="c in userMenuComponents" class="bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover" >
|
|
68
|
-
<component
|
|
69
|
-
:is="getCustomComponent(c)"
|
|
70
|
-
:meta="c.meta"
|
|
71
|
-
:adminUser="coreStore.adminUser"
|
|
72
|
-
/>
|
|
73
|
-
</li>
|
|
74
|
-
<li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
|
|
75
|
-
<UserMenuSettingsButton />
|
|
76
|
-
</li>
|
|
77
|
-
<li>
|
|
78
|
-
<button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover w-full" role="menuitem">{{ $t('Sign out') }}</button>
|
|
79
|
-
</li>
|
|
80
|
-
</ul>
|
|
27
|
+
<Tooltip v-if="coreStore.isInternetError">
|
|
28
|
+
<IconWifiOff class="blinking-icon w-8 h-8 text-red-500 hover:scale-110 transition-transform duration-200" />
|
|
29
|
+
<template #tooltip>
|
|
30
|
+
{{$t('Internet connection lost')}}
|
|
31
|
+
</template>
|
|
32
|
+
</Tooltip>
|
|
33
|
+
|
|
34
|
+
<span
|
|
35
|
+
v-if="!coreStore.config?.singleTheme"
|
|
36
|
+
class="flex items-center gap-1 block py-2 text-sm text-black dark:text-darkSidebarTextHover dark:hover:text-darkSidebarTextActive" role="menuitem">
|
|
37
|
+
<IconMoonSolid class="w-6 h-6 text-blue-300 hover:scale-110 cursor-pointer transition-transform duration-200" @click="toggleTheme" v-if="coreStore.theme !== 'dark'" />
|
|
38
|
+
<IconSunSolid class="w-6 h-6 text-yellow-300 hover:scale-110 cursor-pointer transition-transform duration-200" @click="toggleTheme" v-else />
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
<button
|
|
43
|
+
ref="dropdownUserButton"
|
|
44
|
+
type="button" class=" hover:scale-110 transition-transform duration-200 flex text-sm bg- rounded-full focus:ring-4 focus:ring-lightSidebarDevider dark:focus:ring-darkSidebarDevider dark:bg-" aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
|
45
|
+
<span class="sr-only">{{ $t('Open user menu') }}</span>
|
|
46
|
+
<img
|
|
47
|
+
v-if="coreStore.userAvatarUrl"
|
|
48
|
+
class="w-8 h-8 rounded-full object-cover"
|
|
49
|
+
:src="coreStore.userAvatarUrl"
|
|
50
|
+
alt="user photo"
|
|
51
|
+
/>
|
|
52
|
+
<svg v-else class="w-8 h-8 text-lightNavbarIcons dark:text-darkNavbarIcons" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
|
|
54
|
+
</svg>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="z-50 hidden my-4 text-base list-none bg-lightUserMenuBackground divide-y divide-lightUserMenuBorder text-lightUserMenuText rounded shadow dark:shadow-black dark:bg-darkUserMenuBackground dark:divide-darkUserMenuBorder text-darkUserMenuText dark:shadow-black" id="dropdown-user">
|
|
58
|
+
<div class="px-4 py-3" role="none">
|
|
59
|
+
<p class="text-sm text-gray-900 dark:text-darkNavbarText" role="none" v-if="coreStore.userFullname">
|
|
60
|
+
{{ coreStore.userFullname }}
|
|
61
|
+
</p>
|
|
62
|
+
<p class="text-sm font-medium text-gray-900 truncate dark:text-darkSidebarText" role="none">
|
|
63
|
+
{{ coreStore.username }}
|
|
64
|
+
</p>
|
|
81
65
|
</div>
|
|
66
|
+
|
|
67
|
+
<ul class="py-1" role="none">
|
|
68
|
+
<li v-for="c in userMenuComponents" class="bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover" >
|
|
69
|
+
<component
|
|
70
|
+
:is="getCustomComponent(c)"
|
|
71
|
+
:meta="c.meta"
|
|
72
|
+
:adminUser="coreStore.adminUser"
|
|
73
|
+
/>
|
|
74
|
+
</li>
|
|
75
|
+
<li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
|
|
76
|
+
<UserMenuSettingsButton />
|
|
77
|
+
</li>
|
|
78
|
+
<li>
|
|
79
|
+
<button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover w-full" role="menuitem">{{ $t('Sign out') }}</button>
|
|
80
|
+
</li>
|
|
81
|
+
</ul>
|
|
82
82
|
</div>
|
|
83
83
|
</div>
|
|
84
84
|
</div>
|
|
@@ -12,12 +12,113 @@ import type { AdminForthActionFront, AdminForthResourceColumnInputCommon, AdminF
|
|
|
12
12
|
import { i18nInstance } from '../i18n'
|
|
13
13
|
import { useI18n } from 'vue-i18n';
|
|
14
14
|
import { onBeforeRouteLeave } from 'vue-router';
|
|
15
|
+
import { reconnect } from '@/websocket';
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
const LS_LANG_KEY = `afLanguage`;
|
|
19
20
|
const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
|
|
20
21
|
const ITEMS_PER_PAGE_LIMIT = 100;
|
|
22
|
+
const AUTOLOGIN_QUERY_PARAM = 'autologin';
|
|
23
|
+
|
|
24
|
+
export function getAutologinCredentials(autologin: unknown): { username: string, password: string } | null {
|
|
25
|
+
if (typeof autologin !== 'string') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const separatorIndex = autologin.indexOf(':');
|
|
30
|
+
if (separatorIndex === -1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
username: autologin.slice(0, separatorIndex),
|
|
36
|
+
password: autologin.slice(separatorIndex + 1),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildLoginRedirectQuery() {
|
|
41
|
+
const { path, query } = router.currentRoute.value;
|
|
42
|
+
const nextQuery = new URLSearchParams();
|
|
43
|
+
|
|
44
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
45
|
+
if (key === AUTOLOGIN_QUERY_PARAM || rawValue == null) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(rawValue)) {
|
|
50
|
+
rawValue.forEach((value) => {
|
|
51
|
+
if (value != null) {
|
|
52
|
+
nextQuery.append(key, value);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
nextQuery.set(key, rawValue);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const next = nextQuery.size > 0 ? `${path}?${nextQuery.toString()}` : path;
|
|
62
|
+
const autologin = typeof query[AUTOLOGIN_QUERY_PARAM] === 'string'
|
|
63
|
+
? query[AUTOLOGIN_QUERY_PARAM]
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
next,
|
|
68
|
+
autologin,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function tryAutologin(autologin: string): Promise<boolean> {
|
|
73
|
+
const credentials = getAutologinCredentials(autologin);
|
|
74
|
+
if (!credentials) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const response = await fetch(`${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/login`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'accept-language': localStorage.getItem(LS_LANG_KEY) || 'en',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
username: credentials.username,
|
|
86
|
+
password: credentials.password,
|
|
87
|
+
rememberMe: false,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const loginResponse = await response.json();
|
|
92
|
+
if (loginResponse?.error) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const userStore = useUserStore();
|
|
97
|
+
const coreStore = useCoreStore();
|
|
98
|
+
userStore.authorize();
|
|
99
|
+
reconnect();
|
|
100
|
+
await coreStore.fetchMenuAndResource();
|
|
101
|
+
return !!coreStore.adminUser;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function redirectToLogin() {
|
|
105
|
+
const currentPath = router.currentRoute.value.path;
|
|
106
|
+
const homeRoute = router.getRoutes().find(route => route.name === 'home');
|
|
107
|
+
const homePagePath = (homeRoute?.redirect as string) || '/';
|
|
108
|
+
const { next, autologin } = buildLoginRedirectQuery();
|
|
109
|
+
|
|
110
|
+
if (autologin && await tryAutologin(autologin)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const query: Record<string, string> = {};
|
|
115
|
+
|
|
116
|
+
if (currentPath !== '/login' && currentPath !== homePagePath) {
|
|
117
|
+
query.next = next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await router.push({ name: 'login', query });
|
|
121
|
+
}
|
|
21
122
|
|
|
22
123
|
export async function callApi({path, method, body, headers, silentError = false, abortSignal}: {
|
|
23
124
|
path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
@@ -43,20 +144,7 @@ export async function callApi({path, method, body, headers, silentError = false,
|
|
|
43
144
|
if (r.status == 401 ) {
|
|
44
145
|
useUserStore().unauthorize();
|
|
45
146
|
useCoreStore().resetAdminUser();
|
|
46
|
-
|
|
47
|
-
const homeRoute = router.getRoutes().find(route => route.name === 'home');
|
|
48
|
-
const homePagePath = (homeRoute?.redirect as string) || '/';
|
|
49
|
-
let next = '';
|
|
50
|
-
if (currentPath !== '/login' && currentPath !== homePagePath) {
|
|
51
|
-
if (Object.keys(router.currentRoute.value.query).length > 0) {
|
|
52
|
-
next = currentPath + '?' + Object.entries(router.currentRoute.value.query).map(([key, value]) => `${key}=${value}`).join('&');
|
|
53
|
-
} else {
|
|
54
|
-
next = currentPath;
|
|
55
|
-
}
|
|
56
|
-
await router.push({ name: 'login', query: { next: next } });
|
|
57
|
-
} else {
|
|
58
|
-
await router.push({ name: 'login' });
|
|
59
|
-
}
|
|
147
|
+
await redirectToLogin();
|
|
60
148
|
return null;
|
|
61
149
|
}
|
|
62
150
|
return await r.json();
|