Skip to content

Commit

Permalink
fix(files_external): broken credentials dialog
Browse files Browse the repository at this point in the history
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv authored and backportbot[bot] committed Sep 13, 2024
1 parent 91533fb commit 4fd32df
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 70 deletions.
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>

0 comments on commit 4fd32df

Please sign in to comment.