Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable30] fix(files_external): broken credentials dialog #47951

Merged
merged 2 commits into from
Sep 13, 2024
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
5 changes: 0 additions & 5 deletions apps/files_external/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,5 @@
'url' => '/api/v1/mounts',
'verb' => 'GET',
],
[
'name' => 'Api#askNativeAuth',
'url' => '/api/v1/auth',
'verb' => 'GET',
],
],
];
28 changes: 0 additions & 28 deletions apps/files_external/lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use OCA\Files_External\Service\UserStoragesService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
Expand Down Expand Up @@ -103,31 +102,4 @@ public function getUserMounts(): DataResponse {

return new DataResponse($entries);
}

/**
* Ask for credentials using a browser's native basic auth prompt
* Then returns it if provided
*/
#[NoAdminRequired]
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function askNativeAuth(): DataResponse {
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
$response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
$response->addHeader('WWW-Authenticate', 'Basic realm="Storage authentification needed"');
return $response;
}

$user = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];

// Reset auth
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);

// Using 401 again to ensure we clear any cached Authorization
return new DataResponse([
'user' => $user,
'password' => $password,
], Http::STATUS_UNAUTHORIZED);
}
}
70 changes: 33 additions & 37 deletions apps/files_external/src/actions/enterCredentialsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,39 @@ import type { AxiosResponse } from '@nextcloud/axios'
import type { Node } from '@nextcloud/files'
import type { StorageConfig } from '../services/externalStorage'

import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { showError, showSuccess, spawnDialog } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import LoginSvg from '@mdi/svg/svg/login.svg?raw'
import Vue from 'vue'
import Vue, { defineAsyncComponent } from 'vue'

import { FileAction, DefaultType } from '@nextcloud/files'
import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils'
import { isNodeExternalStorage } from '../utils/externalStorageUtils'

type OCSAuthResponse = {
ocs: {
meta: {
status: string
statuscode: number
message: string
},
data: {
user?: string,
password?: string,
}
type CredentialResponse = {
login?: string,
password?: string,
}

async function setCredentials(node: Node, login: string, password: string): Promise<null|true> {
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), {
backendOptions: { user: login, password },
}) as AxiosResponse<StorageConfig>

const config = configResponse.data
if (config.status !== STORAGE_STATUS.SUCCESS) {
showError(t('files_external', 'Unable to update this external storage config. {statusMessage}', {
statusMessage: config?.statusMessage || '',
}))
return null
}

// Success update config attribute
showSuccess(t('files_external', 'New configuration successfully saved'))
Vue.set(node.attributes, 'config', config)
return true
}

export const action = new FileAction({
Expand Down Expand Up @@ -57,30 +67,16 @@ export const action = new FileAction({
},

async exec(node: Node) {
// always resolve auth request, we'll process the data afterwards
// Using fetch as axios have integrated auth handling and X-Requested-With header
const response = await fetch(generateOcsUrl('/apps/files_external/api/v1/auth'), {
headers: new Headers({ Accept: 'application/json' }),
credentials: 'include',
})

const data = (await response?.json() || {}) as OCSAuthResponse
if (data.ocs.data.user && data.ocs.data.password) {
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), {
backendOptions: data.ocs.data,
}) as AxiosResponse<StorageConfig>

const config = configResponse.data
if (config.status !== STORAGE_STATUS.SUCCESS) {
showError(t('files_external', 'Unable to update this external storage config. {statusMessage}', {
statusMessage: config?.statusMessage || '',
}))
return null
}
const { login, password } = await new Promise<CredentialResponse>(resolve => spawnDialog(
defineAsyncComponent(() => import('../views/CredentialsDialog.vue')),
{},
(args) => {
resolve(args as CredentialResponse)
},
))

// Success update config attribute
showSuccess(t('files_external', 'New configuration successfully saved'))
Vue.set(node.attributes, 'config', config)
if (login && password) {
return await setCredentials(node, login, password)
}

return null
Expand Down
86 changes: 86 additions & 0 deletions apps/files_external/src/views/CredentialsDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<NcDialog :buttons="dialogButtons"
class="external-storage-auth"
close-on-click-outside
data-cy-external-storage-auth
is-form
:name="t('files_external', 'Storage credentials')"
out-transition
@submit="$emit('close', {login, password})"
@update:open="$emit('close')">
<!-- Header -->
<NcNoteCard class="external-storage-auth__header"
:text="t('files_external', 'To access the storage, you need to provide the authentification informations.')"
type="info" />

<!-- Login -->
<NcTextField ref="login"
class="external-storage-auth__login"
data-cy-external-storage-auth-dialog-login
:label="t('files_external', 'Login')"
:placeholder="t('files_external', 'Enter the storage login')"
minlength="2"
name="login"
required
:value.sync="login" />

<!-- Password -->
<NcPasswordField ref="password"
class="external-storage-auth__password"
data-cy-external-storage-auth-dialog-password
:label="t('files_external', 'Password')"
:placeholder="t('files_external', 'Enter the storage password')"
name="password"
required
:value.sync="password" />
</NcDialog>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { t } from '@nextcloud/l10n'

import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'

export default defineComponent({
name: 'CredentialsDialog',

components: {
NcDialog,
NcNoteCard,
NcTextField,
NcPasswordField,
},

setup() {
return {
t,
}
},

data() {
return {
login: '',
password: '',
}
},

computed: {
dialogButtons() {
return [{
label: t('files_external', 'Submit'),
type: 'primary',
nativeType: 'submit',
}]
},
},
})
</script>
2 changes: 2 additions & 0 deletions dist/4076-4076.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading