Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f332404
feat: add settings page and validation for user menu settings
yaroslav8765 Sep 18, 2025
35f6271
feat: implement user menu settings pages in configuration and UI
yaroslav8765 Sep 18, 2025
0cb2ed2
feat: add icons support for options in SettingView
yaroslav8765 Sep 19, 2025
885e76c
feat: enhance settings routing
yaroslav8765 Sep 19, 2025
64a6f8f
docs: update docs for vertical tabs
yaroslav8765 Sep 19, 2025
a6c7d35
chore: update VerticalTabs component to use setActiveTab method and e…
yaroslav8765 Sep 19, 2025
99d0791
Revert "docs: update docs for vertical tabs"
yaroslav8765 Sep 19, 2025
b5e65ee
feat: add settings button in userMenu
yaroslav8765 Sep 19, 2025
fd4eddb
fix: improve ulr change handling for SettingsView
yaroslav8765 Sep 19, 2025
81a5ddd
chore: adjust layout and spacing in UserMenuSettingsButton component
yaroslav8765 Sep 19, 2025
a2826c4
chore: fix styles for dark theme for the UserMenuSettingsButton
yaroslav8765 Sep 19, 2025
568a56c
refactor: remove unused customLayout property and clean up SettingsVi…
yaroslav8765 Sep 19, 2025
52c229f
refactor: simplify user menu settings component and improve routing l…
yaroslav8765 Sep 22, 2025
d737a92
Merge branch 'next' of https://github.com/devforth/adminforth into Se…
yaroslav8765 Sep 22, 2025
0772b2b
fix: fix infinite redirect, when user logouts from setting page
yaroslav8765 Sep 22, 2025
97e2e81
refactor: update component type in configuration and streamline compo…
yaroslav8765 Sep 23, 2025
54d1582
fix: remove on-load blinking in SettingsView
yaroslav8765 Sep 24, 2025
598b840
Revert "refactor: update component type in configuration and streamli…
yaroslav8765 Sep 24, 2025
ea0633a
fix: enhance filter handling and visibility in filters store
NoOne7135 Sep 24, 2025
1cc5bad
fix: update setListFilter to modify existing filters instead of throw…
NoOne7135 Sep 24, 2025
c58fd90
fix: remove outdated comment regarding filter visibility criteria
NoOne7135 Sep 24, 2025
ca4597a
docs: update docs for the Two-Factor Authentication plugin
yaroslav8765 Sep 24, 2025
3f63fa2
docs: update passkeys description in TFA docs
yaroslav8765 Sep 25, 2025
c567845
Merge branch 'next' into feature/filters-visability-handling
NoOne7135 Sep 25, 2025
5a3386e
fix: update filter handling to use visibleFiltersCount for button state
NoOne7135 Sep 25, 2025
df72066
Merge branch 'feature/filters-visability-handling' of https://github.…
NoOne7135 Sep 25, 2025
bed9d37
fix: remove outdated comment regarding filter visibility criteria
NoOne7135 Sep 25, 2025
eb5493a
Merge pull request #359 from devforth/feature/filters-visability-hand…
NoOne7135 Sep 25, 2025
62f55c1
docs: update passkeys description in TFA docs
yaroslav8765 Sep 25, 2025
74d78bc
Merge pull request #367 from devforth/PasskeysDocs
ivictbor Sep 25, 2025
78c4ede
froze infiscal CLI
ivictbor Sep 25, 2025
85693dc
Merge branch 'next' of github.com:devforth/adminforth into next
ivictbor Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,166 @@ plugins: [
}),
],
...
```
```

## Passkeys setup

If you want to use both passkeys and TOTP simultaneously, you can set them up as follows:

First, you need to create a passkeys table in your schema.prisma file:

```ts title='./schema.prisma'
//diff-add
model passkeys {
//diff-add
credential_id String @id
//diff-add
user_id String
//diff-add
meta String
//diff-add
@@index([user_id])
//diff-add
}
```

And make migration:

```bash
npm run makemigration -- --name add-passkeys ; npm run migrate:local
```


Next, you need to create a new resource for passkeys:

```ts title='./resources/passkeys.ts'
import { AdminForthDataTypes, AdminForthResourceInput } from "../../adminforth";

export default {
dataSource: 'maindb',
table: 'passkeys',
resourceId: 'passkeys',
label: 'Passkeys',
columns: [
{
name: 'credential_id',
label: 'Credential ID',
primaryKey: true,
},
{
name: 'user_id',
label: 'User ID',
},
{
name: "meta",
type: AdminForthDataTypes.JSON,
label: "Meta",
}
],
plugins: [],
options: {},
} as AdminForthResourceInput;
```

Add the new resource to index.ts:

```ts title='./index.ts'
...
//diff-add
import passkeysResource from './resources/passkeys.js';
...

resources: [
...
//diff-add
passkeysResource,
...
],
```

Now, update the settings of the Two-Factor Authentication plugin:

```ts tittle='./resources/adminuser.ts'
plugins: [
new TwoFactorsAuthPlugin ({
twoFaSecretFieldName: 'secret2fa',
timeStepWindow: 1
//diff-add
passkeys: {
//diff-add
credentialResourceID: "passkeys",
//diff-add
credentialIdFieldName: "credential_id",
//diff-add
credentialMetaFieldName: "meta",
//diff-add
credentialUserIdFieldName: "user_id",
//diff-add
settings: {
// diff-add
// relying party config
//diff-add
rp: {
//diff-add
name: "New Reality",
// diff-add
// id should be a app domain name without port
// diff-add
// e.g. if you run locally in https://localhost:3500 -> then write "localhost"
// diff-add
// if you run at https://myadmin.myproduct.com -> write "myadmin.myproduct.com"
//diff-add
id: "localhost",
//diff-add
},
//diff-add
user: {
//diff-add
nameField: "email",
//diff-add
displayNameField: "email",
//diff-add
},
//diff-add
authenticatorSelection: {
// diff-add
// Can be "platform" or "cross-platform"
//diff-add
authenticatorAttachment: "platform",
//diff-add
requireResidentKey: true,
//diff-add
userVerification: "required",
//diff-add
},
//diff-add
},
//diff-add
}
}),
],
```
> ☝️ most likely you should set `passkeys.settings.rp.id` it from your process.env depending on your env

The setup is complete. To create a passkey:

> 1) Go to the user menu
> 2) Click settings
> 3) Select "passkeys"

![alt text](Passkeys1.png)

> 4) Add passkey

![alt text](Passkeys2.png)


After adding passkey you can use passkey, instead of TOTP:

![alt text](Passkeys3.png)

> 💡 **Note**: Adding a passkey does not remove the option to use TOTP. If you lose access to your passkey, you can log in using TOTP and reset your passkey.




Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion adminforth/modules/codeInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,20 @@ class CodeInjector implements ICodeInjector {
}
},`})
}}
const registerSettingPages = ( settingPage ) => {
if (!settingPage) {
return;
}
for (const page of settingPage) {
if (page.icon) {
icons.push(page.icon);
}
}
}

registerCustomPages(this.adminforth.config);
collectAssetsFromMenu(this.adminforth.config.menu);

registerSettingPages(this.adminforth.config.auth.userMenuSettingsPages);
const spaDir = this.getSpaDir();

if (process.env.HEAVY_DEBUG) {
Expand Down Expand Up @@ -477,6 +487,12 @@ class CodeInjector implements ICodeInjector {
});
}

if (this.adminforth.config.auth.userMenuSettingsPages) {
for (const settingPage of this.adminforth.config.auth.userMenuSettingsPages) {
checkInjections([{ file: settingPage.component }]);
}
}


customResourceComponents.forEach((filePath) => {
const componentName = getComponentNameFromPath(filePath);
Expand Down
5 changes: 5 additions & 0 deletions adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,11 @@ export default class ConfigValidator implements IConfigValidator {
const similar = suggestIfTypo(newConfig.resources.map((res) => res.resourceId ), newConfig.auth.usersResourceId);
throw new Error(`Resource with id "${newConfig.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
}
if (newConfig.auth.userMenuSettingsPages) {
for (const page of newConfig.auth.userMenuSettingsPages) {
this.validateComponent({file: page.component}, errors);
}
}

// normalize beforeLoginConfirmation hooks
const blc = this.inputConfig.auth.beforeLoginConfirmation;
Expand Down
1 change: 1 addition & 0 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
announcementBadge,
globalInjections: this.adminforth.config.customization.globalInjections,
userFullnameField: this.adminforth.config.auth.userFullNameField,
settingPages: this.adminforth.config.auth.userMenuSettingsPages,
}

// translate menu labels
Expand Down
5 changes: 4 additions & 1 deletion adminforth/spa/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
:adminUser="coreStore.adminUser"
/>
</li>
<li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
<UserMenuSettingsButton />
</li>
<li>
<button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive w-full" role="menuitem">{{ $t('Sign out') }}</button>
</li>
Expand Down Expand Up @@ -268,7 +271,7 @@ import type { AdminForthConfigMenuItem, AnnouncementBadgeResponse } from './type
import { Tooltip } from '@/afcl';
import { initFrontedAPI } from '@/adminforth';
import adminforth from '@/adminforth';

import UserMenuSettingsButton from './components/UserMenuSettingsButton.vue';

const coreStore = useCoreStore();
const toastStore = useToastStore();
Expand Down
24 changes: 14 additions & 10 deletions adminforth/spa/src/adminforth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,27 @@ class FrontendAPI implements FrontendAPIInterface {
listFilterValidation(filter: FilterParams): boolean {
if(router.currentRoute.value.meta.type !== 'list'){
throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
} else {
console.log(this.coreStore.resourceColumnsWithFilters,'core store')
const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumnCommon) => col.name === filter.field)
if(!filterField){
throw new Error(`Field ${filter.field} is not available for filtering`)
}

}
return true
}

setListFilter(filter: FilterParams): void {
if(this.listFilterValidation(filter)){
if(this.filtersStore.filters.some((f: any) => {return f.field === filter.field && f.operator === filter.operator})){
throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
const existingFilterIndex = this.filtersStore.filters.findIndex((f: any) => {
return f.field === filter.field && f.operator === filter.operator
});

if(existingFilterIndex !== -1){
// Update existing filter instead of throwing error
const filters = [...this.filtersStore.filters];
if (filter.value === undefined) {
filters.splice(existingFilterIndex, 1);
} else {
filters[existingFilterIndex] = filter;
}
this.filtersStore.setFilters(filters);
} else {
this.filtersStore.setFilter(filter)
this.filtersStore.setFilter(filter);
}
}
}
Expand Down
17 changes: 13 additions & 4 deletions adminforth/spa/src/afcl/VerticalTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<li v-for="tab in tabs" :key="`${tab}-tab-controll`">
<a
href="#"
@click="activeTab = tab"
@click="setActiveTab(tab)"
class="inline-flex items-center px-4 py-3 rounded-lg w-full"
:class="tab === activeTab ? 'text-lightVerticalTabsTextActive bg-lightVerticalTabsBackgroundActive active dark:bg-darkVerticalTabsBackgroundActive dark:text-darkVerticalTabsTextActive' : 'text-lightVerticalTabsText dark:text-darkVerticalTabsText hover:text-lightVerticalTabsTextHover bg-lightVerticalTabsBackground hover:bg-lightVerticalTabsBackgroundHover dark:bg-darkVerticalTabsBackground dark:hover:bg-darkVerticalTabsBackgroundHover dark:hover:darkVerticalTabsTextHover'"
aria-current="page"
Expand All @@ -20,7 +20,7 @@
</template>

<script setup lang="ts">
import { onMounted, useSlots, ref, type Ref } from 'vue';
import { onMounted, useSlots, ref, type Ref } from 'vue';
const tabs : Ref<string[]> = ref([]);
const activeTab = ref('');
const props = defineProps({
Expand All @@ -31,6 +31,11 @@
const emites = defineEmits([
'update:activeTab',
]);

defineExpose({
setActiveTab
});

onMounted(() => {
const slots = useSlots();
tabs.value = Object.keys(slots).reduce((tbs: string[], tb: string) => {
Expand All @@ -44,6 +49,10 @@
}
});



function setActiveTab(tab: string) {
if (tabs.value.includes(tab)) {
activeTab.value = tab;
emites('update:activeTab', tab);
}
}
</script>
2 changes: 1 addition & 1 deletion adminforth/spa/src/components/Filters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@

<div class="flex justify-end gap-2">
<button
:disabled="!filtersStore.filters.length"
:disabled="!filtersStore.visibleFiltersCount"
type="button"
class="flex items-center py-1 px-3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
@click="clear">{{ $t('Clear all') }}</button>
Expand Down
Loading