diff --git a/lang/en.json b/lang/en.json index 2009c867..e99bf4ad 100644 --- a/lang/en.json +++ b/lang/en.json @@ -484,5 +484,6 @@ "Matches media objects referenced by a specific object type": "Matches media objects referenced by a specific object type", "Matching the ": "Matching the ", "Matches objects for which the given Gramps Object Query Language (GOQL) expression is true": "Matches objects for which the given Gramps Object Query Language (GOQL) expression is true", - "Expression:": "Expression:" + "Expression:": "Expression:", + "Your full name and e-mail address may be updated whenever you sign in with a linked account.": "Your full name and e-mail address may be updated whenever you sign in with a linked account." } diff --git a/src/components/GrampsjsOidcAccounts.js b/src/components/GrampsjsOidcAccounts.js index 9dc581c1..373d8a01 100644 --- a/src/components/GrampsjsOidcAccounts.js +++ b/src/components/GrampsjsOidcAccounts.js @@ -113,6 +113,12 @@ export class GrampsjsOidcAccounts extends GrampsjsAppStateMixin(LitElement) { } } + updated(changed) { + if (changed.has('_accounts')) { + fireEvent(this, 'oidc-accounts:changed', {count: this._accounts.length}) + } + } + // Reloads the accounts, e.g. when the containing view is shown again. refresh() { if (this._status !== 'hidden') { diff --git a/src/views/GrampsjsViewSettingsUser.js b/src/views/GrampsjsViewSettingsUser.js index 959b62c3..3105026d 100644 --- a/src/views/GrampsjsViewSettingsUser.js +++ b/src/views/GrampsjsViewSettingsUser.js @@ -22,6 +22,7 @@ import {GrampsjsView} from './GrampsjsView.js' import {mdiCheck, mdiContentCopy} from '@mdi/js' import { __APIHOST__, + apiGetOIDCConfig, TREE_CONFIG_PRIMARY_COLOR, TREE_CONFIG_SECONDARY_COLOR, } from '../api.js' @@ -78,6 +79,8 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { _translations: {type: Array}, _langLoading: {type: Boolean}, _tokenCopied: {type: Boolean}, + _localAuthDisabled: {type: Boolean}, + _hasLinkedAccount: {type: Boolean}, } } @@ -87,6 +90,8 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { this._translations = [] this._langLoading = false this._tokenCopied = false + this._localAuthDisabled = false + this._hasLinkedAccount = false } renderContent() { @@ -125,9 +130,12 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {

${this._('Change E-mail')}

${this.renderChangeEmail()} - -

${this._('Change password')}

- ${this.renderChangePw()} + ${this._localAuthDisabled + ? '' + : html` +

${this._('Change password')}

+ ${this.renderChangePw()} + `} ${supportsPersistentAccessTokens(this.appState?.dbInfo) ? html`

${this._('Access tokens')}

@@ -137,6 +145,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { ` : ''} @@ -205,6 +214,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { if (this.active) { this._fetchOwnUserDetails() } + this._fetchAuthConfig() } update(changed) { @@ -366,6 +376,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { id="change-full-name" label="${this._('Full Name')}" value="${this._userInfo?.full_name || ''}" + supporting-text="${this._idpHint()}" > ${this._('Save')} @@ -381,6 +392,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { id="change-email" label="${this._('New E-mail')}" value="${this._userInfo?.email ? this._userInfo.email : ''}" + supporting-text="${this._idpHint()}" >

@@ -594,6 +606,27 @@ export class GrampsjsViewSettingsUser extends GrampsjsView { this._userInfo = data.data } } + + // the identity provider may overwrite full name and e-mail at each sign-in + _idpHint() { + return this._hasLinkedAccount + ? this._( + 'Your full name and e-mail address may be updated whenever you sign in with a linked account.' + ) + : '' + } + + _handleOidcAccountsChanged(e) { + this._hasLinkedAccount = e.detail.count > 0 + } + + // users without local auth have no password to change + async _fetchAuthConfig() { + const config = await apiGetOIDCConfig() + this._localAuthDisabled = Boolean( + config?.enabled && config.disable_local_auth + ) + } } window.customElements.define( diff --git a/test/unit/settingsUser.test.js b/test/unit/settingsUser.test.js index 3a4cbd9f..4b4c97cb 100644 --- a/test/unit/settingsUser.test.js +++ b/test/unit/settingsUser.test.js @@ -1,4 +1,6 @@ -import {describe, it, expect, vi} from 'vitest' +import {describe, it, expect, vi, afterEach} from 'vitest' +import * as api from '../../src/api.js' +import {GrampsjsOidcAccounts} from '../../src/components/GrampsjsOidcAccounts.js' import {GrampsjsViewSettingsUser} from '../../src/views/GrampsjsViewSettingsUser.js' import {SYMBOL_SET_DEFAULT, SYMBOL_SET_TEXT} from '../../src/symbols.js' @@ -129,6 +131,68 @@ describe('user profile settings', () => { }) }) +describe('password form visibility', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + const localAuthDisabledFor = async config => { + vi.spyOn(api, 'apiGetOIDCConfig').mockResolvedValue(config) + const element = new GrampsjsViewSettingsUser() + await element._fetchAuthConfig() + return element._localAuthDisabled + } + + it('hides the form when OIDC disables local auth', async () => { + expect( + await localAuthDisabledFor({enabled: true, disable_local_auth: true}) + ).toBe(true) + }) + + it('shows the form when local auth is enabled', async () => { + expect( + await localAuthDisabledFor({enabled: true, disable_local_auth: false}) + ).toBe(false) + }) + + it('shows the form when OIDC is off', async () => { + expect( + await localAuthDisabledFor({enabled: false, disable_local_auth: true}) + ).toBe(false) + }) + + it('shows the form when the config request fails', async () => { + expect(await localAuthDisabledFor({error: 'Not Found'})).toBe(false) + }) +}) + +describe('identity provider hint', () => { + it('reports the number of linked accounts', () => { + const accounts = new GrampsjsOidcAccounts() + vi.spyOn(accounts, 'dispatchEvent') + accounts._accounts = [{id: 'a'}, {id: 'b'}] + + accounts.updated(new Map([['_accounts', []]])) + + expect(accounts.dispatchEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'oidc-accounts:changed', + detail: {count: 2}, + }) + ) + }) + + it('shows the hint while an account is linked', () => { + const element = new GrampsjsViewSettingsUser() + + element._handleOidcAccountsChanged({detail: {count: 1}}) + expect(element._idpHint()).not.toBe('') + + element._handleOidcAccountsChanged({detail: {count: 0}}) + expect(element._idpHint()).toBe('') + }) +}) + describe('genealogical symbols setting', () => { it('saves the selected symbol set', () => { const element = new GrampsjsViewSettingsUser()