Signed-off-by: skjnldsv <skjnldsv@protonmail.com>pull/52537/head
parent
506afad862
commit
bf3ce79abd
@ -1,57 +0,0 @@ |
||||
/** |
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
import { defineAsyncComponent } from 'vue' |
||||
import { getBuilder } from '@nextcloud/browser-storage' |
||||
import { getGuestNickname, setGuestNickname } from '@nextcloud/auth' |
||||
import { getUploader } from '@nextcloud/upload' |
||||
import { spawnDialog } from '@nextcloud/dialogs' |
||||
|
||||
import logger from './services/logger' |
||||
|
||||
const storage = getBuilder('files_sharing').build() |
||||
|
||||
/** |
||||
* Setup file-request nickname header for the uploader |
||||
* @param nickname The nickname |
||||
*/ |
||||
function registerFileRequestHeader(nickname: string) { |
||||
const uploader = getUploader() |
||||
uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname)) |
||||
logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders }) |
||||
} |
||||
|
||||
/** |
||||
* Callback when a nickname was chosen |
||||
* @param nickname The chosen nickname |
||||
*/ |
||||
function onSetNickname(nickname: string): void { |
||||
// Set the nickname
|
||||
setGuestNickname(nickname) |
||||
// Set the dialog as shown
|
||||
storage.setItem('public-auth-prompt-shown', 'true') |
||||
// Register header for uploader
|
||||
registerFileRequestHeader(nickname) |
||||
} |
||||
|
||||
window.addEventListener('DOMContentLoaded', () => { |
||||
const nickname = getGuestNickname() ?? '' |
||||
const dialogShown = storage.getItem('public-auth-prompt-shown') !== null |
||||
|
||||
// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
|
||||
// We still show the prompt if the user has a nickname to double check
|
||||
if (!nickname || !dialogShown) { |
||||
spawnDialog( |
||||
defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')), |
||||
{ |
||||
nickname, |
||||
}, |
||||
onSetNickname as (...rest: unknown[]) => void, |
||||
) |
||||
} else { |
||||
logger.debug('Public auth prompt already shown.', { nickname }) |
||||
registerFileRequestHeader(nickname) |
||||
} |
||||
}) |
||||
@ -0,0 +1,86 @@ |
||||
/** |
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
import { getBuilder } from '@nextcloud/browser-storage' |
||||
import { getGuestNickname, type NextcloudUser } from '@nextcloud/auth' |
||||
import { getUploader } from '@nextcloud/upload' |
||||
import { loadState } from '@nextcloud/initial-state' |
||||
import { showGuestUserPrompt } from '@nextcloud/dialogs' |
||||
import { t } from '@nextcloud/l10n' |
||||
|
||||
import logger from './services/logger' |
||||
import { subscribe } from '@nextcloud/event-bus' |
||||
|
||||
const storage = getBuilder('files_sharing').build() |
||||
|
||||
// Setup file-request nickname header for the uploader
|
||||
const registerFileRequestHeader = (nickname: string) => { |
||||
const uploader = getUploader() |
||||
uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname)) |
||||
logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders }) |
||||
} |
||||
|
||||
// Callback when a nickname was chosen
|
||||
const onUserInfoChanged = (guest: NextcloudUser) => { |
||||
logger.debug('User info changed', { guest }) |
||||
registerFileRequestHeader(guest.displayName ?? '') |
||||
} |
||||
|
||||
// Monitor nickname changes
|
||||
subscribe('user:info:changed', onUserInfoChanged) |
||||
|
||||
window.addEventListener('DOMContentLoaded', () => { |
||||
const nickname = getGuestNickname() ?? '' |
||||
const dialogShown = storage.getItem('public-auth-prompt-shown') !== null |
||||
|
||||
// Check if a nickname is mandatory
|
||||
const isFileRequest = loadState('files_sharing', 'isFileRequest', false) |
||||
|
||||
const owner = loadState('files_sharing', 'owner', '') |
||||
const ownerDisplayName = loadState('files_sharing', 'ownerDisplayName', '') |
||||
const label = loadState('files_sharing', 'label', '') |
||||
const filename = loadState('files_sharing', 'filename', '') |
||||
|
||||
// If the owner provided a custom label, use it instead of the filename
|
||||
const folder = label || filename |
||||
|
||||
const options = { |
||||
nickname, |
||||
notice: t('files_sharing', 'To upload files to {folder}, you need to provide your name first.', { folder }), |
||||
subtitle: undefined as string | undefined, |
||||
title: t('files_sharing', 'Upload files to {folder}', { folder }), |
||||
} |
||||
|
||||
// If the guest already has a nickname, we just make them double check
|
||||
if (nickname) { |
||||
options.notice = t('files_sharing', 'Please confirm your name to upload files to {folder}', { folder }) |
||||
} |
||||
|
||||
// If the account owner set their name as public,
|
||||
// we show it in the subtitle
|
||||
if (owner) { |
||||
options.subtitle = t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) |
||||
} |
||||
|
||||
// If this is a file request, then we need a nickname
|
||||
if (isFileRequest) { |
||||
// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
|
||||
// We still show the prompt if the user has a nickname to double check
|
||||
if (!nickname || !dialogShown) { |
||||
logger.debug('Showing public auth prompt.', { nickname }) |
||||
showGuestUserPrompt(options) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if (!dialogShown && !nickname) { |
||||
logger.debug('Public auth prompt not shown yet but nickname is not mandatory.', { nickname }) |
||||
return |
||||
} |
||||
|
||||
// Else, we just register the nickname header if any.
|
||||
logger.debug('Public auth prompt already shown.', { nickname }) |
||||
registerFileRequestHeader(nickname) |
||||
}) |
||||
@ -1,138 +0,0 @@ |
||||
<!-- |
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
- SPDX-License-Identifier: AGPL-3.0-or-later |
||||
--> |
||||
|
||||
<template> |
||||
<NcDialog :buttons="dialogButtons" |
||||
class="public-auth-prompt" |
||||
data-cy-public-auth-prompt-dialog |
||||
is-form |
||||
:can-close="false" |
||||
:name="dialogName" |
||||
@submit="$emit('close', name)"> |
||||
<p v-if="owner" class="public-auth-prompt__subtitle"> |
||||
{{ t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) }} |
||||
</p> |
||||
|
||||
<!-- Header --> |
||||
<NcNoteCard class="public-auth-prompt__header" |
||||
:text="t('files_sharing', 'To upload files, you need to provide your name first.')" |
||||
type="info" /> |
||||
|
||||
<!-- Form --> |
||||
<NcTextField ref="input" |
||||
class="public-auth-prompt__input" |
||||
data-cy-public-auth-prompt-dialog-name |
||||
:label="t('files_sharing', 'Name')" |
||||
:placeholder="t('files_sharing', 'Enter your name')" |
||||
minlength="2" |
||||
name="name" |
||||
required |
||||
:value.sync="name" /> |
||||
</NcDialog> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue' |
||||
import { loadState } from '@nextcloud/initial-state' |
||||
import { t } from '@nextcloud/l10n' |
||||
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog' |
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' |
||||
import NcTextField from '@nextcloud/vue/components/NcTextField' |
||||
|
||||
import { getGuestNameValidity } from '../services/GuestNameValidity' |
||||
|
||||
export default defineComponent({ |
||||
name: 'PublicAuthPrompt', |
||||
|
||||
components: { |
||||
NcDialog, |
||||
NcNoteCard, |
||||
NcTextField, |
||||
}, |
||||
|
||||
props: { |
||||
/** |
||||
* Preselected nickname |
||||
* @default '' No name preselected by default |
||||
*/ |
||||
nickname: { |
||||
type: String, |
||||
default: '', |
||||
}, |
||||
}, |
||||
|
||||
setup() { |
||||
return { |
||||
t, |
||||
|
||||
owner: loadState('files_sharing', 'owner', ''), |
||||
ownerDisplayName: loadState('files_sharing', 'ownerDisplayName', ''), |
||||
label: loadState('files_sharing', 'label', ''), |
||||
note: loadState('files_sharing', 'note', ''), |
||||
filename: loadState('files_sharing', 'filename', ''), |
||||
} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
name: '', |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
dialogName() { |
||||
return this.t('files_sharing', 'Upload files to {folder}', { folder: this.label || this.filename }) |
||||
}, |
||||
dialogButtons() { |
||||
return [{ |
||||
label: t('files_sharing', 'Submit name'), |
||||
type: 'primary', |
||||
nativeType: 'submit', |
||||
}] |
||||
}, |
||||
}, |
||||
|
||||
watch: { |
||||
/** Reset name to pre-selected nickname (e.g. Talk / Collabora ) */ |
||||
nickname: { |
||||
handler() { |
||||
this.name = this.nickname |
||||
}, |
||||
immediate: true, |
||||
}, |
||||
|
||||
name() { |
||||
// Check validity of the new name |
||||
const newName = this.name.trim?.() || '' |
||||
const input = (this.$refs.input as Vue|undefined)?.$el.querySelector('input') |
||||
if (!input) { |
||||
return |
||||
} |
||||
|
||||
const validity = getGuestNameValidity(newName) |
||||
input.setCustomValidity(validity) |
||||
input.reportValidity() |
||||
}, |
||||
}, |
||||
}) |
||||
</script> |
||||
<style scoped lang="scss"> |
||||
.public-auth-prompt { |
||||
&__subtitle { |
||||
// Smaller than dialog title |
||||
font-size: 1.25em; |
||||
margin-block: 0 calc(3 * var(--default-grid-baseline)); |
||||
} |
||||
|
||||
&__header { |
||||
margin-block: 0 calc(3 * var(--default-grid-baseline)); |
||||
} |
||||
|
||||
&__input { |
||||
margin-block: calc(4 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline)); |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,15 @@ |
||||
/** |
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
|
||||
import { getCSPNonce } from '@nextcloud/auth' |
||||
import Vue from 'vue' |
||||
|
||||
import PublicPageUserMenu from './views/PublicPageUserMenu.vue' |
||||
|
||||
__webpack_nonce__ = getCSPNonce() |
||||
|
||||
const View = Vue.extend(PublicPageUserMenu) |
||||
const instance = new View() |
||||
instance.$mount('#public-page-user-menu') |
||||
@ -0,0 +1,135 @@ |
||||
<!-- |
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
- SPDX-License-Identifier: AGPL-3.0-or-later |
||||
--> |
||||
<template> |
||||
<NcHeaderMenu id="public-page-user-menu" |
||||
class="public-page-user-menu" |
||||
is-nav |
||||
:aria-label="t('core', 'User menu')" |
||||
:description="avatarDescription"> |
||||
<template #trigger> |
||||
<NcAvatar class="public-page-user-menu__avatar" |
||||
disable-menu |
||||
disable-tooltip |
||||
is-guest |
||||
:user="displayName || '?'" /> |
||||
</template> |
||||
<ul class="public-page-user-menu__list"> |
||||
<!-- Privacy notice --> |
||||
<NcNoteCard class="public-page-user-menu__list-note" |
||||
:text="privacyNotice" |
||||
type="info" /> |
||||
|
||||
<!-- Nickname dialog --> |
||||
<AccountMenuEntry id="set-nickname" |
||||
:name="!displayName ? t('core', 'Set public name') : t('core', 'Change public name')" |
||||
href="#" |
||||
@click.prevent.stop="setNickname"> |
||||
<template #icon> |
||||
<IconAccount /> |
||||
</template> |
||||
</AccountMenuEntry> |
||||
</ul> |
||||
</NcHeaderMenu> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import type { NextcloudUser } from '@nextcloud/auth' |
||||
|
||||
import '@nextcloud/dialogs/style.css' |
||||
import { defineComponent } from 'vue' |
||||
import { getGuestUser } from '@nextcloud/auth' |
||||
import { showGuestUserPrompt } from '@nextcloud/dialogs' |
||||
import { subscribe } from '@nextcloud/event-bus' |
||||
import { t } from '@nextcloud/l10n' |
||||
|
||||
import NcAvatar from '@nextcloud/vue/components/NcAvatar' |
||||
import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu' |
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' |
||||
import IconAccount from 'vue-material-design-icons/Account.vue' |
||||
|
||||
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue' |
||||
|
||||
export default defineComponent({ |
||||
name: 'PublicPageUserMenu', |
||||
components: { |
||||
AccountMenuEntry, |
||||
IconAccount, |
||||
NcAvatar, |
||||
NcHeaderMenu, |
||||
NcNoteCard, |
||||
}, |
||||
|
||||
setup() { |
||||
return { |
||||
t, |
||||
} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
displayName: getGuestUser().displayName, |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
avatarDescription(): string { |
||||
return t('core', 'User menu') |
||||
}, |
||||
|
||||
privacyNotice(): string { |
||||
return this.displayName |
||||
? t('core', 'You will be identified as {user} by the account owner.', { user: this.displayName }) |
||||
: t('core', 'You are currently not identified.') |
||||
}, |
||||
}, |
||||
|
||||
mounted() { |
||||
subscribe('user:info:changed', (user: NextcloudUser) => { |
||||
this.displayName = user.displayName || '' |
||||
}) |
||||
}, |
||||
|
||||
methods: { |
||||
setNickname() { |
||||
showGuestUserPrompt({ |
||||
nickname: this.displayName, |
||||
cancellable: true, |
||||
}) |
||||
}, |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
.public-page-user-menu { |
||||
box-sizing: border-box; |
||||
|
||||
// Ensure we do not waste space, as the header menu sets a default width of 350px |
||||
:deep(.header-menu__content) { |
||||
width: fit-content !important; |
||||
} |
||||
|
||||
&__list-note { |
||||
padding-block: 5px !important; |
||||
padding-inline: 5px !important; |
||||
max-width: 300px; |
||||
margin: 5px !important; |
||||
margin-bottom: 0 !important; |
||||
} |
||||
|
||||
&__list { |
||||
display: inline-flex; |
||||
flex-direction: column; |
||||
padding-block: var(--default-grid-baseline) 0; |
||||
padding-inline: 0 var(--default-grid-baseline); |
||||
|
||||
> :deep(li) { |
||||
box-sizing: border-box; |
||||
// basically "fit-content" |
||||
flex: 0 1; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,193 @@ |
||||
/*! |
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
||||
* SPDX-License-Identifier: AGPL-3.0-or-later |
||||
*/ |
||||
import type { ShareContext } from './PublicShareUtils.ts' |
||||
import { createLinkShare, setupData } from './PublicShareUtils.ts' |
||||
|
||||
/** |
||||
* This tests ensures that on public shares the header avatar menu correctly works |
||||
*/ |
||||
describe('files_sharing: Public share - header avatar menu', { testIsolation: true }, () => { |
||||
let context: ShareContext |
||||
let firstPublicShareUrl = '' |
||||
let secondPublicShareUrl = '' |
||||
|
||||
before(() => { |
||||
cy.createRandomUser() |
||||
.then((user) => { |
||||
context = { |
||||
user, |
||||
url: undefined, |
||||
} |
||||
setupData(context.user, 'public1') |
||||
setupData(context.user, 'public2') |
||||
createLinkShare(context, 'public1').then((shareUrl) => { |
||||
firstPublicShareUrl = shareUrl |
||||
cy.log(`Created first share with URL: ${shareUrl}`) |
||||
}) |
||||
createLinkShare(context, 'public2').then((shareUrl) => { |
||||
secondPublicShareUrl = shareUrl |
||||
cy.log(`Created second share with URL: ${shareUrl}`) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
beforeEach(() => { |
||||
cy.logout() |
||||
cy.visit(firstPublicShareUrl) |
||||
}) |
||||
|
||||
it('See the undefined avatar menu', () => { |
||||
cy.get('header') |
||||
.findByRole('navigation', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.findByRole('button', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.click() |
||||
cy.get('#header-menu-public-page-user-menu') |
||||
.as('headerMenu') |
||||
|
||||
// Note that current guest user is not identified
|
||||
cy.get('@headerMenu') |
||||
.should('be.visible') |
||||
.findByRole('note') |
||||
.should('be.visible') |
||||
.should('contain', 'not identified') |
||||
|
||||
// Button to set guest name
|
||||
cy.get('@headerMenu') |
||||
.findByRole('link', { name: /Set public name/i }) |
||||
.should('be.visible') |
||||
}) |
||||
|
||||
it('Can set public name', () => { |
||||
cy.get('header') |
||||
.findByRole('navigation', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.findByRole('button', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.as('userMenuButton') |
||||
|
||||
// Open the user menu
|
||||
cy.get('@userMenuButton').click() |
||||
cy.get('#header-menu-public-page-user-menu') |
||||
.as('headerMenu') |
||||
|
||||
cy.get('@headerMenu') |
||||
.findByRole('link', { name: /Set public name/i }) |
||||
.should('be.visible') |
||||
.click() |
||||
|
||||
// Check the dialog is visible
|
||||
cy.findByRole('dialog', { name: /Guest identification/i }) |
||||
.should('be.visible') |
||||
.as('guestIdentificationDialog') |
||||
|
||||
// Check the note is visible
|
||||
cy.get('@guestIdentificationDialog') |
||||
.findByRole('note') |
||||
.should('contain', 'not identified') |
||||
|
||||
// Check the input is visible
|
||||
cy.get('@guestIdentificationDialog') |
||||
.findByRole('textbox', { name: /Name/i }) |
||||
.should('be.visible') |
||||
.type('{selectAll}John Doe{enter}') |
||||
|
||||
// Check that the dialog is closed
|
||||
cy.get('@guestIdentificationDialog') |
||||
.should('not.exist') |
||||
|
||||
// Check that the avatar changed
|
||||
cy.get('@userMenuButton') |
||||
.find('img') |
||||
.invoke('attr', 'src') |
||||
.should('include', 'avatar/guest/John%20Doe') |
||||
}) |
||||
|
||||
it('Guest name us persistent and can be changed', () => { |
||||
cy.get('header') |
||||
.findByRole('navigation', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.findByRole('button', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.as('userMenuButton') |
||||
|
||||
// Open the user menu
|
||||
cy.get('@userMenuButton').click() |
||||
cy.get('#header-menu-public-page-user-menu') |
||||
.as('headerMenu') |
||||
|
||||
cy.get('@headerMenu') |
||||
.findByRole('link', { name: /Set public name/i }) |
||||
.should('be.visible') |
||||
.click() |
||||
|
||||
// Check the dialog is visible
|
||||
cy.findByRole('dialog', { name: /Guest identification/i }) |
||||
.should('be.visible') |
||||
.as('guestIdentificationDialog') |
||||
|
||||
// Set the name
|
||||
cy.get('@guestIdentificationDialog') |
||||
.findByRole('textbox', { name: /Name/i }) |
||||
.should('be.visible') |
||||
.type('{selectAll}Jane Doe{enter}') |
||||
|
||||
// Check that the dialog is closed
|
||||
cy.get('@guestIdentificationDialog') |
||||
.should('not.exist') |
||||
|
||||
// Create another share
|
||||
cy.visit(secondPublicShareUrl) |
||||
|
||||
cy.get('header') |
||||
.findByRole('navigation', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.findByRole('button', { name: /User menu/i }) |
||||
.should('be.visible') |
||||
.as('userMenuButton') |
||||
|
||||
// Open the user menu
|
||||
cy.get('@userMenuButton').click() |
||||
cy.get('#header-menu-public-page-user-menu') |
||||
.as('headerMenu') |
||||
|
||||
// See the note with the current name
|
||||
cy.get('@headerMenu') |
||||
.findByRole('note') |
||||
.should('contain', 'You will be identified as Jane Doe') |
||||
|
||||
cy.get('@headerMenu') |
||||
.findByRole('link', { name: /Change public name/i }) |
||||
.should('be.visible') |
||||
.click() |
||||
|
||||
// Check the dialog is visible
|
||||
cy.findByRole('dialog', { name: /Guest identification/i }) |
||||
.should('be.visible') |
||||
.as('guestIdentificationDialog') |
||||
|
||||
// Check that the note states the current name
|
||||
// cy.get('@guestIdentificationDialog')
|
||||
// .findByRole('note')
|
||||
// .should('contain', 'are currently identified as Jane Doe')
|
||||
|
||||
// Change the name
|
||||
cy.get('@guestIdentificationDialog') |
||||
.findByRole('textbox', { name: /Name/i }) |
||||
.should('be.visible') |
||||
.type('{selectAll}Foo Bar{enter}') |
||||
|
||||
// Check that the dialog is closed
|
||||
cy.get('@guestIdentificationDialog') |
||||
.should('not.exist') |
||||
|
||||
// Check that the avatar changed with the second name
|
||||
cy.get('@userMenuButton') |
||||
.find('img') |
||||
.invoke('attr', 'src') |
||||
.should('include', 'avatar/guest/Foo%20Bar') |
||||
}) |
||||
}) |
||||
Loading…
Reference in new issue