aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-06-25 00:00:31 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-07-09 17:13:30 +0200
commit691f570237e26398aa22f40c0efca23141d5583e (patch)
tree4409270ac8ee482ad03f745f77003c726ffbf09f /apps/settings/src
parent3a97dbf248b3e581b5782a638743958eb6f2a640 (diff)
downloadnextcloud-server-691f570237e26398aa22f40c0efca23141d5583e.tar.gz
nextcloud-server-691f570237e26398aa22f40c0efca23141d5583e.zip
chore: Enable ESLint for apps and fix all errors
Nevertheless this causes a huge amount of new warnings. Previously the shell script for directories to lint was wrong it was generating all app names to lint, but was missing the `apps/` prefix. Causing only `core` to be linted. Co-authored-by: Grigorii K. Shartsev <me@shgk.me> Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/settings/src')
-rw-r--r--apps/settings/src/components/AdminSettingsSharingForm.vue20
-rw-r--r--apps/settings/src/components/BasicSettings/BackgroundJob.vue10
-rw-r--r--apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue46
-rw-r--r--apps/settings/src/components/Encryption.vue18
-rw-r--r--apps/settings/src/components/PersonalInfo/BirthdaySection.vue16
-rw-r--r--apps/settings/src/components/Users/UserRow.vue2
-rw-r--r--apps/settings/src/components/WebAuthn/AddDevice.vue2
-rw-r--r--apps/settings/src/components/WebAuthn/Section.vue3
-rw-r--r--apps/settings/src/main-declarative-settings-forms.ts41
-rw-r--r--apps/settings/src/service/PersonalInfo/EmailService.js5
-rw-r--r--apps/settings/src/service/PersonalInfo/PersonalInfoService.js5
-rw-r--r--apps/settings/src/store/apps.js2
-rw-r--r--apps/settings/src/store/authtoken.ts2
-rw-r--r--apps/settings/src/store/users.js1
-rw-r--r--apps/settings/src/utils/userUtils.ts4
15 files changed, 94 insertions, 83 deletions
diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue
index 38484c00d23..dda65191708 100644
--- a/apps/settings/src/components/AdminSettingsSharingForm.vue
+++ b/apps/settings/src/components/AdminSettingsSharingForm.vue
@@ -62,18 +62,24 @@
<label>{{ t('settings', 'Limit sharing based on groups') }}</label>
<div class="sharing__sub-section">
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="no"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="no"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Allow sharing for everyone (default)') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="yes"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="yes"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Exclude some groups from sharing') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="allow"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="allow"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Limit sharing to some groups') }}
</NcCheckboxRadioSwitch>
<div v-show="settings.excludeGroups !== 'no'" class="sharing__labeled-entry sharing__input">
@@ -305,7 +311,7 @@ export default defineComponent({
onUpdateExcludeGroups: debounce(function(value: string) {
window.OCP.AppConfig.setValue('core', 'excludeGroups', value)
this.settings.excludeGroups = value
- }, 500) as (v?: string) => void
+ }, 500) as (v?: string) => void,
},
})
</script>
diff --git a/apps/settings/src/components/BasicSettings/BackgroundJob.vue b/apps/settings/src/components/BasicSettings/BackgroundJob.vue
index e699323be80..835f65a2477 100644
--- a/apps/settings/src/components/BasicSettings/BackgroundJob.vue
+++ b/apps/settings/src/components/BasicSettings/BackgroundJob.vue
@@ -63,13 +63,15 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
+import { generateOcsUrl } from '@nextcloud/router'
+import { confirmPassword } from '@nextcloud/password-confirmation'
+import axios from '@nextcloud/axios'
+import moment from '@nextcloud/moment'
+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import moment from '@nextcloud/moment'
-import axios from '@nextcloud/axios'
-import { generateOcsUrl } from '@nextcloud/router'
-import { confirmPassword } from '@nextcloud/password-confirmation'
+
import '@nextcloud/password-confirmation/dist/style.css'
const lastCron = loadState('settings', 'lastCron')
diff --git a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
index 25bbfa48bf0..0a5e2415668 100644
--- a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
+++ b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
@@ -3,14 +3,13 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <NcSettingsSection
- class="declarative-settings-section"
+ <NcSettingsSection class="declarative-settings-section"
:name="t(formApp, form.title)"
:description="t(formApp, form.description)"
:doc-url="form.doc_url || ''">
<div v-for="formField in formFields"
- :key="formField.id"
- class="declarative-form-field"
+ :key="formField.id"
+ class="declarative-form-field"
:aria-label="t('settings', '{app}\'s declarative setting field: {name}', { app: formApp, name: t(formApp, formField.title) })"
:class="{
'declarative-form-field-text': isTextFormField(formField),
@@ -20,16 +19,14 @@
'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox',
'declarative-form-field-radio': formField.type === 'radio'
}">
-
<template v-if="isTextFormField(formField)">
<div class="input-wrapper">
- <NcInputField
- :type="formField.type"
+ <NcInputField :type="formField.type"
:label="t(formApp, formField.title)"
:value.sync="formFieldsData[formField.id].value"
:placeholder="t(formApp, formField.placeholder)"
@update:value="onChangeDebounced(formField)"
- @submit="updateDeclarativeSettingsValue(formField)"/>
+ @submit="updateDeclarativeSettingsValue(formField)" />
</div>
<span class="hint">{{ t(formApp, formField.description) }}</span>
</template>
@@ -37,13 +34,12 @@
<template v-if="formField.type === 'select'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
<div class="input-wrapper">
- <NcSelect
- :id="formField.id + '_field'"
+ <NcSelect :id="formField.id + '_field'"
:options="formField.options"
:placeholder="t(formApp, formField.placeholder)"
:label-outside="true"
:value="formFieldsData[formField.id].value"
- @input="(value) => updateFormFieldDataValue(value, formField, true)"/>
+ @input="(value) => updateFormFieldDataValue(value, formField, true)" />
</div>
<span class="hint">{{ t(formApp, formField.description) }}</span>
</template>
@@ -51,8 +47,7 @@
<template v-if="formField.type === 'multi-select'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
<div class="input-wrapper">
- <NcSelect
- :id="formField.id + '_field'"
+ <NcSelect :id="formField.id + '_field'"
:options="formField.options"
:placeholder="t(formApp, formField.placeholder)"
:multiple="true"
@@ -62,21 +57,20 @@
formFieldsData[formField.id].value = value
updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
}
- "/>
+ " />
</div>
<span class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'checkbox'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- :id="formField.id + '_field'"
+ <NcCheckboxRadioSwitch :id="formField.id + '_field'"
:checked="Boolean(formFieldsData[formField.id].value)"
@update:checked="(value) => {
formField.value = value
updateFormFieldDataValue(+value, formField, true)
}
- ">
+ ">
{{ t(formApp, formField.label) }}
</NcCheckboxRadioSwitch>
<span class="hint">{{ t(formApp, formField.description) }}</span>
@@ -84,8 +78,7 @@
<template v-if="formField.type === 'multi-checkbox'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- v-for="option in formField.options"
+ <NcCheckboxRadioSwitch v-for="option in formField.options"
:id="formField.id + '_field_' + option.value"
:key="option.value"
:checked="formFieldsData[formField.id].value[option.value]"
@@ -94,7 +87,7 @@
// Update without re-generating initial formFieldsData.value object as the link to components are lost
updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
}
- ">
+ ">
{{ t(formApp, option.name) }}
</NcCheckboxRadioSwitch>
<span class="hint">{{ t(formApp, formField.description) }}</span>
@@ -102,8 +95,7 @@
<template v-if="formField.type === 'radio'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- v-for="option in formField.options"
+ <NcCheckboxRadioSwitch v-for="option in formField.options"
:key="option.value"
:value="option.value"
type="radio"
@@ -146,9 +138,6 @@ export default {
formFieldsData: {},
}
},
- beforeMount() {
- this.initFormFieldsData()
- },
computed: {
formApp() {
return this.form.app || ''
@@ -157,6 +146,9 @@ export default {
return this.form.fields || []
},
},
+ beforeMount() {
+ this.initFormFieldsData()
+ },
methods: {
initFormFieldsData() {
this.form.fields.forEach((formField) => {
@@ -175,7 +167,7 @@ export default {
this.$set(formField, 'value', JSON.parse(formField.value))
// Merge possible new options
formField.options.forEach(option => {
- if (!formField.value.hasOwnProperty(option.value)) {
+ if (!Object.prototype.hasOwnProperty.call(formField.value, option.value)) {
this.$set(formField.value, option.value, false)
}
})
@@ -216,7 +208,7 @@ export default {
formId: this.form.id.replace(this.formApp + '_', ''), // Remove app prefix to send clean form id
fieldId: formField.id,
value: value === null ? this.formFieldsData[formField.id].value : value,
- });
+ })
} catch (err) {
console.debug(err)
showError(t('settings', 'Failed to save setting'))
diff --git a/apps/settings/src/components/Encryption.vue b/apps/settings/src/components/Encryption.vue
index ba9fa186f9f..6574f4e728d 100644
--- a/apps/settings/src/components/Encryption.vue
+++ b/apps/settings/src/components/Encryption.vue
@@ -59,22 +59,18 @@
</template>
<script>
+import { showError } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { generateOcsUrl } from '@nextcloud/router'
+import { confirmPassword } from '@nextcloud/password-confirmation'
import axios from '@nextcloud/axios'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import { loadState } from '@nextcloud/initial-state'
-import { getLoggerBuilder } from '@nextcloud/logger'
-import { generateOcsUrl } from '@nextcloud/router'
-import { confirmPassword } from '@nextcloud/password-confirmation'
-import '@nextcloud/password-confirmation/dist/style.css'
-import { showError } from '@nextcloud/dialogs'
+import logger from '../logger'
-const logger = getLoggerBuilder()
- .setApp('settings')
- .detectUser()
- .build()
+import '@nextcloud/password-confirmation/dist/style.css'
export default {
name: 'Encryption',
@@ -122,7 +118,7 @@ export default {
try {
const { data } = await axios.post(url, {
- value: value,
+ value,
})
this.handleResponse({
status: data.ocs?.meta?.status,
diff --git a/apps/settings/src/components/PersonalInfo/BirthdaySection.vue b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue
index 7580e958023..86917257600 100644
--- a/apps/settings/src/components/PersonalInfo/BirthdaySection.vue
+++ b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue
@@ -23,15 +23,15 @@
</template>
<script>
-import HeaderBar from './shared/HeaderBar.vue'
-import AccountPropertySection from './shared/AccountPropertySection.vue'
+import { loadState } from '@nextcloud/initial-state'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
-import { NcDateTimePickerNative } from '@nextcloud/vue'
-import debounce from 'debounce'
import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService'
import { handleError } from '../../utils/handlers'
-import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
-import { loadState } from '@nextcloud/initial-state'
+
+import debounce from 'debounce'
+
+import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
+import HeaderBar from './shared/HeaderBar.vue'
const { birthdate } = loadState('settings', 'personalInfoParameters', {})
@@ -39,8 +39,6 @@ export default {
name: 'BirthdaySection',
components: {
- AlertCircle,
- AccountPropertySection,
NcDateTimePickerNative,
HeaderBar,
},
@@ -74,7 +72,7 @@ export default {
const month = (value.getMonth() + 1).toString().padStart(2, '0')
const year = value.getFullYear()
this.birthdate.value = `${year}-${month}-${day}`
- }
+ },
},
},
diff --git a/apps/settings/src/components/Users/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue
index 4a4a221e199..8d6cfdc5f35 100644
--- a/apps/settings/src/components/Users/UserRow.vue
+++ b/apps/settings/src/components/Users/UserRow.vue
@@ -288,7 +288,7 @@ import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import UserRowActions from './UserRowActions.vue'
import UserRowMixin from '../../mixins/UserRowMixin.js'
-import { isObfuscated, unlimitedQuota } from '../../utils/userUtils.ts';
+import { isObfuscated, unlimitedQuota } from '../../utils/userUtils.ts'
export default {
name: 'UserRow',
diff --git a/apps/settings/src/components/WebAuthn/AddDevice.vue b/apps/settings/src/components/WebAuthn/AddDevice.vue
index 818341ba5fc..8e686af9b01 100644
--- a/apps/settings/src/components/WebAuthn/AddDevice.vue
+++ b/apps/settings/src/components/WebAuthn/AddDevice.vue
@@ -59,6 +59,8 @@ import {
finishRegistration,
} from '../../service/WebAuthnRegistrationSerice.ts'
+import '@nextcloud/password-confirmation/dist/style.css'
+
const logAndPass = (text) => (data) => {
logger.debug(text)
return data
diff --git a/apps/settings/src/components/WebAuthn/Section.vue b/apps/settings/src/components/WebAuthn/Section.vue
index bbeb880b8b9..71ec616534c 100644
--- a/apps/settings/src/components/WebAuthn/Section.vue
+++ b/apps/settings/src/components/WebAuthn/Section.vue
@@ -38,7 +38,6 @@
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
import { confirmPassword } from '@nextcloud/password-confirmation'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import '@nextcloud/password-confirmation/dist/style.css'
import sortBy from 'lodash/fp/sortBy.js'
import AddDevice from './AddDevice.vue'
@@ -46,6 +45,8 @@ import Device from './Device.vue'
import logger from '../../logger.ts'
import { removeRegistration } from '../../service/WebAuthnRegistrationSerice.js'
+import '@nextcloud/password-confirmation/dist/style.css'
+
const sortByName = sortBy('name')
export default {
diff --git a/apps/settings/src/main-declarative-settings-forms.ts b/apps/settings/src/main-declarative-settings-forms.ts
index 4644f3e7d87..7cd4cb68345 100644
--- a/apps/settings/src/main-declarative-settings-forms.ts
+++ b/apps/settings/src/main-declarative-settings-forms.ts
@@ -2,10 +2,12 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import Vue from 'vue';
-import { loadState } from '@nextcloud/initial-state';
-import { translate as t, translatePlural as n } from '@nextcloud/l10n';
-import DeclarativeSection from './components/DeclarativeSettings/DeclarativeSection.vue';
+import type { ComponentInstance } from 'vue'
+
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import Vue from 'vue'
+import DeclarativeSection from './components/DeclarativeSettings/DeclarativeSection.vue'
interface DeclarativeFormField {
id: string,
@@ -14,9 +16,9 @@ interface DeclarativeFormField {
type: string,
placeholder: string,
label: string,
- options: Array<any>|null,
- value: any,
- default: any,
+ options: Array<unknown>|null,
+ value: unknown,
+ default: unknown,
}
interface DeclarativeForm {
@@ -32,23 +34,28 @@ interface DeclarativeForm {
fields: Array<DeclarativeFormField>,
}
-const forms = loadState('settings', 'declarative-settings-forms', []) as Array<DeclarativeForm>;
-console.debug('Loaded declarative forms:', forms);
+const forms = loadState('settings', 'declarative-settings-forms', []) as Array<DeclarativeForm>
+console.debug('Loaded declarative forms:', forms)
-function renderDeclarativeSettingsSections(forms: Array<DeclarativeForm>): void {
+/**
+ *
+ * @param forms
+ */
+function renderDeclarativeSettingsSections(forms: Array<DeclarativeForm>): ComponentInstance[] {
Vue.mixin({ methods: { t, n } })
- const DeclarativeSettingsSection = Vue.extend(<any>DeclarativeSection);
- for (const form of forms) {
+ const DeclarativeSettingsSection = Vue.extend(DeclarativeSection as never)
+
+ return forms.map((form) => {
const el = `#${form.app}_${form.id}`
- new DeclarativeSettingsSection({
- el: el,
+ return new DeclarativeSettingsSection({
+ el,
propsData: {
form,
},
})
- }
+ })
}
document.addEventListener('DOMContentLoaded', () => {
- renderDeclarativeSettingsSections(forms);
-});
+ renderDeclarativeSettingsSections(forms)
+})
diff --git a/apps/settings/src/service/PersonalInfo/EmailService.js b/apps/settings/src/service/PersonalInfo/EmailService.js
index 5439e7cc1b1..52e5106328d 100644
--- a/apps/settings/src/service/PersonalInfo/EmailService.js
+++ b/apps/settings/src/service/PersonalInfo/EmailService.js
@@ -3,14 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import axios from '@nextcloud/axios'
import { getCurrentUser } from '@nextcloud/auth'
import { generateOcsUrl } from '@nextcloud/router'
import { confirmPassword } from '@nextcloud/password-confirmation'
-import '@nextcloud/password-confirmation/dist/style.css'
+import axios from '@nextcloud/axios'
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
+import '@nextcloud/password-confirmation/dist/style.css'
+
/**
* Save the primary email of the user
*
diff --git a/apps/settings/src/service/PersonalInfo/PersonalInfoService.js b/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
index 6f0e02bf1a5..678fab628d3 100644
--- a/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
+++ b/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
@@ -3,14 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import axios from '@nextcloud/axios'
import { getCurrentUser } from '@nextcloud/auth'
import { generateOcsUrl } from '@nextcloud/router'
import { confirmPassword } from '@nextcloud/password-confirmation'
-import '@nextcloud/password-confirmation/dist/style.css'
+import axios from '@nextcloud/axios'
import { SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
+import '@nextcloud/password-confirmation/dist/style.css'
+
/**
* Save the primary account property value for the user
*
diff --git a/apps/settings/src/store/apps.js b/apps/settings/src/store/apps.js
index 84310ef8e13..ed5a7245371 100644
--- a/apps/settings/src/store/apps.js
+++ b/apps/settings/src/store/apps.js
@@ -184,7 +184,7 @@ const actions = {
showInfo(
t(
'settings',
- 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
+ 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
),
{
onClick: () => window.location.reload(),
diff --git a/apps/settings/src/store/authtoken.ts b/apps/settings/src/store/authtoken.ts
index efd3b49e32c..daf5583ab8c 100644
--- a/apps/settings/src/store/authtoken.ts
+++ b/apps/settings/src/store/authtoken.ts
@@ -12,6 +12,8 @@ import { defineStore } from 'pinia'
import axios from '@nextcloud/axios'
import logger from '../logger'
+import '@nextcloud/password-confirmation/dist/style.css'
+
const BASE_URL = generateUrl('/settings/personal/authtokens')
const confirm = () => {
diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js
index bd53aca6704..03a10b04b5d 100644
--- a/apps/settings/src/store/users.js
+++ b/apps/settings/src/store/users.js
@@ -390,6 +390,7 @@ const actions = {
* @param {object} options destructuring object
* @param {number} options.offset List offset to request
* @param {number} options.limit List number to return from offset
+ * @param options.search
* @return {Promise<number>}
*/
async getDisabledUsers(context, { offset, limit, search }) {
diff --git a/apps/settings/src/utils/userUtils.ts b/apps/settings/src/utils/userUtils.ts
index 0d62138d7fe..9ac21fd4c0e 100644
--- a/apps/settings/src/utils/userUtils.ts
+++ b/apps/settings/src/utils/userUtils.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { translate as t } from '@nextcloud/l10n'
+
export const unlimitedQuota = {
id: 'none',
label: t('settings', 'Unlimited'),
@@ -19,7 +21,7 @@ export const defaultQuota = {
* @param user
* @param user.id
*/
-export const isObfuscated = (user: { id: string, [key: string]: any }) => {
+export const isObfuscated = (user: { id: string, [key: string]: unknown }) => {
const keys = Object.keys(user)
return keys.length === 1 && keys.at(0) === 'id'
}