Skip to content

Commit d334d16

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 3755d7c + 5a3789a commit d334d16

File tree

10 files changed

+113
-69
lines changed

10 files changed

+113
-69
lines changed

adminforth/documentation/docs/tutorial/07-Plugins/02-TwoFactorsAuth.md

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ Add the new resource to index.ts:
276276

277277
resources: [
278278
...
279-
//diff-add
280-
passkeysResource,
279+
//diff-add
280+
passkeysResource,
281281
...
282282
],
283283
```
@@ -291,9 +291,6 @@ Now, update the settings of the Two-Factor Authentication plugin:
291291
timeStepWindow: 1
292292
//diff-add
293293
passkeys: {
294-
//diff-add
295-
challengesKeyValueAdapter: new RAMKeyValueAdapter(), // you can use any key-value adapter
296-
297294
//diff-add
298295
credentialResourceID: "passkeys",
299296
//diff-add
@@ -305,20 +302,19 @@ Now, update the settings of the Two-Factor Authentication plugin:
305302
//diff-add
306303
settings: {
307304
// diff-add
305+
expectedOrigin: "http://localhost:3000", // important, set it to your backoffice origin (starts from scheme, no slash at the end)
306+
//diff-add
308307
// relying party config
309308
//diff-add
310-
rp: {
309+
rp: {
310+
//diff-add
311+
name: "New Reality",
312+
313+
//diff-add
314+
// optionaly you can set expected id explicitly if you need to:
315+
//diff-add
316+
// id: "localhost",
311317
//diff-add
312-
name: "New Reality",
313-
// diff-add
314-
// id should be a app domain name without port
315-
// diff-add
316-
// e.g. if you run locally in https://localhost:3500 -> then write "localhost"
317-
// diff-add
318-
// if you run at https://myadmin.myproduct.com -> write "myadmin.myproduct.com"
319-
//diff-add
320-
id: "localhost",
321-
//diff-add
322318
},
323319
//diff-add
324320
user: {
@@ -331,8 +327,16 @@ Now, update the settings of the Two-Factor Authentication plugin:
331327
//diff-add
332328
authenticatorSelection: {
333329
// diff-add
334-
// Can be "platform" or "cross-platform"
335-
//diff-add
330+
// impacts a way how passkey will be created
331+
// diff-add
332+
// - platform - using browser internal authenticator (e.g. Google Chrome passkey / Google Password Manager )
333+
// diff-add
334+
// - cross-platform - using external authenticator (e.g. Yubikey, Google Titan etc)
335+
// diff-add
336+
// - both - plging will show both options to the user
337+
// diff-add
338+
// Can be "platform", "cross-platform" or "both"
339+
// diff-add
336340
authenticatorAttachment: "platform",
337341
//diff-add
338342
requireResidentKey: true,
@@ -347,7 +351,14 @@ Now, update the settings of the Two-Factor Authentication plugin:
347351
}),
348352
],
349353
```
350-
> ☝️ most likely you should set `passkeys.settings.rp.id` it from your process.env depending on your env
354+
355+
> ☝️ most likely you should set `passkeys.settings.expectedOrigin` from your process.env depending on your env (e.g. http://localhost:3500 for local dev, https://admin.yourproduct.com for production etc)
356+
357+
358+
> 💡**Note** By default `passkeys.settings.rp.id` is generated from the expectedOrigin so you don't need to set it
359+
> unless you know what you are doing. Manual setting might be needed for sub-domains isolation.
360+
> By default, if you set expected origin to https://localhost:3500 it will use "localhost" as rpid
361+
> If you set origin to https://myadmin.myproduct.com -> it will use "myadmin.myproduct.com" as rpid
351362
352363
The setup is complete. To create a passkey:
353364

adminforth/modules/codeInjector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import os from 'os';
66
import path from 'path';
77
import { promisify } from 'util';
88
import AdminForth, { AdminForthConfigMenuItem } from '../index.js';
9-
import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge, md5hash } from './utils.js';
9+
import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge, md5hash, slugifyString } from './utils.js';
1010
import { ICodeInjector } from '../types/Back.js';
1111
import { StylesGenerator } from './styleGenerator.js';
1212

adminforth/modules/configValidator.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717

1818
import fs from 'fs';
1919
import path from 'path';
20-
import { guessLabelFromName, md5hash, suggestIfTypo } from './utils.js';
20+
import { guessLabelFromName, md5hash, suggestIfTypo, slugifyString } from './utils.js';
2121
import {
2222
AdminForthSortDirections,
2323
type AdminForthComponentDeclarationFull,
@@ -30,7 +30,6 @@ import AdminForth from "adminforth";
3030
import { AdminForthConfigMenuItem } from "adminforth";
3131

3232

33-
3433
export default class ConfigValidator implements IConfigValidator {
3534

3635
customComponentsDir: string | undefined;
@@ -160,18 +159,31 @@ export default class ConfigValidator implements IConfigValidator {
160159
if (!customization.customPages) {
161160
customization.customPages = [];
162161
}
162+
const normalizeComponent = (comp: any) => {
163+
if (typeof comp === 'string') {
164+
return { file: comp, meta: {} };
165+
}
166+
const meta = comp.meta || {};
167+
if (meta.sidebarAndHeader === undefined) {
168+
meta.sidebarAndHeader = meta.customLayout === true ? 'none' : 'default';
169+
}
170+
delete meta.customLayout;
171+
return { ...comp, meta };
172+
};
173+
163174
customization.customPages.forEach((page, i) => {
164-
this.validateComponent(page.component, errors);
175+
const normalizedComponent = normalizeComponent(page.component);
176+
this.validateComponent(normalizedComponent, errors);
177+
customization.customPages[i].component = normalizedComponent;
165178
});
166179

167180
if (!customization.brandName) { //} === undefined) {
168181
customization.brandName = 'AdminForth';
169182
}
170183

171184
// slug should have only lowercase letters, dashes and numbers
172-
customization.brandNameSlug = customization.brandName.toLowerCase().replace(/[^a-z0-9-]/g, '');
185+
customization.brandNameSlug = slugifyString(customization.brandName);
173186

174-
175187
if (customization.brandLogo) {
176188
errors.push(...this.checkCustomFileExists(customization.brandLogo));
177189
}
@@ -1005,6 +1017,7 @@ export default class ConfigValidator implements IConfigValidator {
10051017
if (newConfig.auth.userMenuSettingsPages) {
10061018
for (const page of newConfig.auth.userMenuSettingsPages) {
10071019
this.validateComponent({file: page.component}, errors);
1020+
page.slug = page.slug ?? slugifyString(page.pageLabel);
10081021
}
10091022
}
10101023

adminforth/modules/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,4 +509,12 @@ export function isProbablyUUIDColumn(column: { name: string; type?: string; samp
509509
}
510510

511511
return false;
512+
}
513+
514+
export function slugifyString(str: string): string {
515+
return str
516+
.toString()
517+
.toLowerCase()
518+
.replace(/\s+/g, '-')
519+
.replace(/[^a-z0-9-_]/g, '-');
512520
}

adminforth/spa/src/App.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,16 @@
7575
<Sidebar
7676
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout"
7777
:sideBarOpen="sideBarOpen"
78+
:forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
7879
@hideSidebar="hideSidebar"
7980
@loadMenu="loadMenu"
8081
@sidebarStateChange="handleSidebarStateChange"
8182
/>
8283

8384
<div class="transition-all duration-300 ease-in-out max-w-[100vw]"
8485
:class="{
85-
'sm:ml-18': isSidebarIconOnly,
86-
'sm:ml-64': !isSidebarIconOnly,
86+
'sm:ml-20': isSidebarIconOnly,
87+
'sm:ml-[264px]': !isSidebarIconOnly,
8788
'sm:max-w-[calc(100%-4.5rem)]': isSidebarIconOnly,
8889
'sm:max-w-[calc(100%-16rem)]': !isSidebarIconOnly
8990
}"
@@ -229,16 +230,19 @@ async function initRouter() {
229230
230231
async function loadMenu() {
231232
await initRouter();
232-
if (!route.meta.customLayout) {
233+
if (route.meta.sidebarAndHeader !== 'none') {
233234
// for custom layouts we don't need to fetch menu
234235
await coreStore.fetchMenuAndResource();
235236
}
236237
loginRedirectCheckIsReady.value = true;
237238
}
238239
239240
function handleCustomLayout() {
240-
if (route.meta?.customLayout) {
241+
if (route.meta?.sidebarAndHeader === 'none') {
241242
defaultLayout.value = false;
243+
} else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
244+
defaultLayout.value = true;
245+
isSidebarIconOnly.value = true;
242246
} else {
243247
defaultLayout.value = true;
244248
}

adminforth/spa/src/components/Sidebar.vue

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
:adminUser="coreStore.adminUser"
3232
/>
3333
</div>
34-
<div class="absolute top-1.5 -right-4 z-10 hidden sm:block" v-if="iconOnlySidebarEnabled && (!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))">
34+
<div class="absolute top-1.5 -right-4 z-10 hidden sm:block" v-if="!forceIconOnly && iconOnlySidebarEnabled && (!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))">
3535
<button class="text-sm text-lightSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" @click="toggleSidebar">
3636
<IconCloseSidebarSolid v-if="!isSidebarIconOnly" class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
3737
<IconOpenSidebarSolid v-else class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
@@ -286,6 +286,7 @@ import adminforth from '@/adminforth';
286286
287287
interface Props {
288288
sideBarOpen: boolean;
289+
forceIconOnly?: boolean;
289290
}
290291
291292
const props = defineProps<Props>();
@@ -304,15 +305,21 @@ const sidebarAside = ref(null);
304305
305306
const smQuery = window.matchMedia('(min-width: 640px)');
306307
const isMobile = ref(!smQuery.matches);
307-
const iconOnlySidebarEnabled = computed(() => coreStore.config?.iconOnlySidebar?.enabled !== false);
308-
const isSidebarIconOnly = ref(!isMobile.value && localStorage.getItem('afIconOnlySidebar') === 'true');
308+
const iconOnlySidebarEnabled = computed(() => props.forceIconOnly === true || coreStore.config?.iconOnlySidebar?.enabled !== false);
309+
const isSidebarIconOnly = ref(false);
309310
310311
function handleBreakpointChange(e: MediaQueryListEvent) {
311312
isMobile.value = !e.matches;
312313
if (isMobile.value) {
313314
isSidebarIconOnly.value = false;
314315
} else {
315-
isSidebarIconOnly.value = iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true';
316+
if (props.forceIconOnly === true) {
317+
isSidebarIconOnly.value = true;
318+
} else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
319+
isSidebarIconOnly.value = true;
320+
} else {
321+
isSidebarIconOnly.value = false;
322+
}
316323
}
317324
}
318325
@@ -323,6 +330,9 @@ const isSidebarHovering = ref(false);
323330
const isTogglingSidebar = ref(false);
324331
325332
function toggleSidebar() {
333+
if (props.forceIconOnly) {
334+
return;
335+
}
326336
if (!iconOnlySidebarEnabled.value) {
327337
return;
328338
}
@@ -358,7 +368,7 @@ watch(()=>coreStore.menu, () => {
358368
359369
360370
watch(isSidebarIconOnly, (isIconOnly) => {
361-
if (!isMobile.value && iconOnlySidebarEnabled.value) {
371+
if (!isMobile.value && iconOnlySidebarEnabled.value && !props.forceIconOnly) {
362372
localStorage.setItem('afIconOnlySidebar', isIconOnly.toString());
363373
}
364374
emit('sidebarStateChange', { isSidebarIconOnly: isIconOnly, isSidebarHovering: isSidebarHovering.value });
@@ -416,4 +426,18 @@ onMounted(() => {
416426
onUnmounted(() => {
417427
smQuery.removeEventListener('change', handleBreakpointChange);
418428
})
429+
430+
watch(() => props.forceIconOnly, (force) => {
431+
if (isMobile.value) {
432+
isSidebarIconOnly.value = false;
433+
return;
434+
}
435+
if (props.forceIconOnly === true) {
436+
isSidebarIconOnly.value = true;
437+
} else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
438+
isSidebarIconOnly.value = true;
439+
} else {
440+
isSidebarIconOnly.value = false;
441+
}
442+
}, { immediate: true })
419443
</script>

adminforth/spa/src/components/UserMenuSettingsButton.vue

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
</template>
3737

3838
<script setup lang="ts">
39-
import 'flag-icon-css/css/flag-icons.min.css';
4039
import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
4140
import { computed, ref, onMounted, watch } from 'vue';
4241
import { useCoreStore } from '@/stores/core';
@@ -59,18 +58,10 @@ const options = computed(() => {
5958
});
6059
});
6160
62-
function slugifyString(str: string): string {
63-
return str
64-
.toString()
65-
.toLowerCase()
66-
.replace(/\s+/g, '-')
67-
.replace(/[^a-z0-9-_]/g, '-');
68-
}
69-
7061
function getRoute(option: { slug?: string | null, pageLabel: string }) {
7162
return {
7263
name: 'settings',
73-
params: { page: option.slug ?? slugifyString(option.pageLabel) }
64+
params: { page: option.slug }
7465
}
7566
}
7667

adminforth/spa/src/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const router = createRouter({
6868
component: () => import('@/views/SettingsView.vue'),
6969
meta: {
7070
title: 'Settings',
71+
sidebarAndHeader: 'preferIconOnly',
7172
},
7273
},
7374
/* IMPORTANT:ADMINFORTH ROUTES */

adminforth/spa/src/views/SettingsView.vue

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
<p>No setting pages configured or still loading...</p>
55
</div>
66
<VerticalTabs v-else ref="VerticalTabsRef" v-model:active-tab="activeTab" @update:active-tab="setURL({slug: $event, pageLabel: ''})">
7-
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`tab:${settingPageSlotName(c,i)}`" v-slot:['tab:'+settingPageSlotName(c,i)]>
7+
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`tab:${settingPageSlotName(c,i)}`" v-slot:['tab:'+c.slug]>
88
<div class="flex items-center justify-center whitespace-nowrap w-full px-4 gap-2" @click="setURL(c)">
99
<component v-if="c.icon" :is="getIcon(c.icon)" class="w-5 h-5 group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
1010
{{ c.pageLabel }}
1111
</div>
1212
</template>
1313

14-
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`${settingPageSlotName(c,i)}-content`" v-slot:[settingPageSlotName(c,i)]>
14+
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`${settingPageSlotName(c,i)}-content`" v-slot:[c.slug]>
1515
<component
1616
:is="getCustomComponent({file: c.component || ''})"
1717
:resource="coreStore.resource"
@@ -52,15 +52,6 @@ async function initRouter() {
5252
routerIsReady.value = true;
5353
}
5454
55-
function settingPageSlotName(c: { slug?: string; pageLabel?: string }, idx: number) {
56-
const base = (c.slug && c.slug.trim()) || (c.pageLabel && c.pageLabel.trim()) || `tab-${idx}`;
57-
return base
58-
.toString()
59-
.toLowerCase()
60-
.replace(/\s+/g, '-')
61-
.replace(/[^a-z0-9-_]/g, '-') || `tab-${idx}`;
62-
}
63-
6455
watch(dropdownUserButton, (el) => {
6556
if (el) {
6657
const dd = new Dropdown(
@@ -103,19 +94,10 @@ function setURL(item: {
10394
pageLabel: string;
10495
slug?: string | undefined;
10596
}) {
106-
const slug = item?.slug;
107-
if (slug) {
108-
router.replace({
109-
name: 'settings',
110-
params: { page: slug }
111-
});
112-
} else {
113-
const slugified = slugifyString(item.pageLabel);
114-
router.replace({
115-
name: 'settings',
116-
params: { page: slugified }
117-
});
118-
}
97+
router.replace({
98+
name: 'settings',
99+
params: { page: item?.slug }
100+
});
119101
}
120102
121103
function handleURLChange(val: string | null) {

0 commit comments

Comments
 (0)