Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -484,5 +484,6 @@
"Matches media objects referenced by a specific object type": "Matches media objects referenced by a specific object type",
"Matching the <GOQL expression>": "Matching the <GOQL expression>",
"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."
}
6 changes: 6 additions & 0 deletions src/components/GrampsjsOidcAccounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
39 changes: 36 additions & 3 deletions src/views/GrampsjsViewSettingsUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -78,6 +79,8 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {
_translations: {type: Array},
_langLoading: {type: Boolean},
_tokenCopied: {type: Boolean},
_localAuthDisabled: {type: Boolean},
_hasLinkedAccount: {type: Boolean},
}
}

Expand All @@ -87,6 +90,8 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {
this._translations = []
this._langLoading = false
this._tokenCopied = false
this._localAuthDisabled = false
this._hasLinkedAccount = false
}

renderContent() {
Expand Down Expand Up @@ -125,9 +130,12 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {

<h3>${this._('Change E-mail')}</h3>
${this.renderChangeEmail()}

<h3>${this._('Change password')}</h3>
${this.renderChangePw()}
${this._localAuthDisabled
? ''
: html`
<h3>${this._('Change password')}</h3>
${this.renderChangePw()}
`}
${supportsPersistentAccessTokens(this.appState?.dbInfo)
? html`
<h3>${this._('Access tokens')}</h3>
Expand All @@ -137,6 +145,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {
`
: ''}
<grampsjs-oidc-accounts
@oidc-accounts:changed="${this._handleOidcAccountsChanged}"
.appState="${this.appState}"
username="${this._userInfo?.name || ''}"
></grampsjs-oidc-accounts>
Expand Down Expand Up @@ -205,6 +214,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {
if (this.active) {
this._fetchOwnUserDetails()
}
this._fetchAuthConfig()
}

update(changed) {
Expand Down Expand Up @@ -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()}"
></md-filled-text-field>
<md-outlined-button @click="${this._changeFullName}">
${this._('Save')}
Expand All @@ -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()}"
></md-filled-text-field>
</p>
<p>
Expand Down Expand Up @@ -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(
Expand Down
66 changes: 65 additions & 1 deletion test/unit/settingsUser.test.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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()
Expand Down
Loading