-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Louis Chemineau <louis@chmn.me>
- Loading branch information
Showing
4 changed files
with
112 additions
and
120 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,8 @@ | |
import Vue from 'vue' | ||
import type { ComponentInstance } from 'vue' | ||
|
||
import type { AxiosInstance, InternalAxiosRequestConfig } from '@nextcloud/axios' | ||
import type { AxiosInstance } from '@nextcloud/axios' | ||
import axios from '@nextcloud/axios' | ||
import { getCurrentUser } from '@nextcloud/auth' | ||
|
||
import PasswordDialogVue from './components/PasswordDialog.vue' | ||
|
@@ -14,12 +15,6 @@ import { generateUrl } from '@nextcloud/router' | |
|
||
const PAGE_LOAD_TIME = Date.now() | ||
|
||
interface AuthenticatedRequestState { | ||
promise: Promise<void>, | ||
resolve: () => void, | ||
reject: () => void, | ||
} | ||
|
||
/** | ||
* Check if password confirmation is required according to the last confirmation time. | ||
* Use as a replacement of deprecated `OC.PasswordConfirmation.requiresPasswordConfirmation()`. | ||
|
@@ -44,55 +39,70 @@ export const isPasswordConfirmationRequired = (mode: 'reminder'|'inRequest'): bo | |
* Confirm password if needed. | ||
* Replacement of deprecated `OC.PasswordConfirmation.requirePasswordConfirmation(callback)` | ||
* | ||
* @return {Promise<void>} Promise that resolves when password is confirmed or not needded. | ||
* @return {Promise<void>} Promise that resolves when password is confirmed or not needed. | ||
* Rejects if password confirmation was cancelled | ||
* or confirmation is already in process. | ||
*/ | ||
export const confirmPassword = (): Promise<void> => { | ||
if (!isPasswordConfirmationRequired()) { | ||
export const confirmPassword = async (): Promise<void> => { | ||
if (!isPasswordConfirmationRequired('reminder')) { | ||
return Promise.resolve() | ||
} | ||
|
||
return getPasswordDialog() | ||
const password = await getPassword() | ||
return _confirmPassword(password) | ||
} | ||
|
||
/** | ||
* | ||
* @param mode | ||
* @param callback | ||
* @return | ||
* @param password | ||
Check warning on line 57 in src/main.ts GitHub Actions / NPM lint
|
||
*/ | ||
function getPasswordDialog(callback: (password: string) => Promise<void>): Promise<void> { | ||
const isDialogMounted = Boolean(document.getElementById(DIALOG_ID)) | ||
if (isDialogMounted) { | ||
return Promise.reject(new Error('Password confirmation dialog already mounted')) | ||
} | ||
async function _confirmPassword(password: string) { | ||
const url = generateUrl('/login/confirm') | ||
const { data } = await axios.post(url, { password }) | ||
window.nc_lastLogin = data.lastLogin | ||
} | ||
|
||
/** | ||
* | ||
*/ | ||
function getDialog(): Vue { | ||
const element = document.getElementById(DIALOG_ID) | ||
|
||
const mountPoint = document.createElement('div') | ||
mountPoint.setAttribute('id', DIALOG_ID) | ||
if (element !== null) { | ||
return element.__vue__.$root | ||
} else { | ||
const mountPoint = document.createElement('div') | ||
mountPoint.setAttribute('id', DIALOG_ID) | ||
|
||
const modals = Array.from(document.querySelectorAll(`.${MODAL_CLASS}`) as NodeListOf<HTMLElement>) | ||
// Filter out hidden modals | ||
.filter((modal) => modal.style.display !== 'none') | ||
const modals = Array.from(document.querySelectorAll(`.${MODAL_CLASS}`) as NodeListOf<HTMLElement>) | ||
// Filter out hidden modals | ||
.filter((modal) => modal.style.display !== 'none') | ||
|
||
const isModalMounted = Boolean(modals.length) | ||
const isModalMounted = Boolean(modals.length) | ||
|
||
if (isModalMounted) { | ||
const previousModal = modals[modals.length - 1] | ||
previousModal.prepend(mountPoint) | ||
} else { | ||
document.body.appendChild(mountPoint) | ||
} | ||
if (isModalMounted) { | ||
const previousModal = modals[modals.length - 1] | ||
previousModal.prepend(mountPoint) | ||
} else { | ||
document.body.appendChild(mountPoint) | ||
} | ||
|
||
const DialogClass = Vue.extend(PasswordDialogVue) | ||
// Mount point element is replaced by the component | ||
const dialog = (new DialogClass({ propsData: { callback } }) as ComponentInstance).$mount(mountPoint) | ||
const DialogClass = Vue.extend(PasswordDialogVue) | ||
// Mount point element is replaced by the component | ||
return (new DialogClass() as ComponentInstance).$mount(mountPoint) | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param callback | ||
Check warning on line 98 in src/main.ts GitHub Actions / NPM lint
|
||
*/ | ||
function getPassword(callback: () => void): Promise<string> { | ||
return new Promise((resolve, reject) => { | ||
dialog.$on('confirmed', () => { | ||
dialog.$destroy() | ||
resolve() | ||
}) | ||
const dialog = getDialog() | ||
|
||
dialog.$on('submit', callback) | ||
|
||
dialog.$on('close', () => { | ||
dialog.$destroy() | ||
reject(new Error('Dialog closed')) | ||
|
@@ -103,13 +113,10 @@ function getPasswordDialog(callback: (password: string) => Promise<void>): Promi | |
/** | ||
* Add axios interceptors to an axios instance that will ask for | ||
* password confirmation to add it as Basic Auth for every requests. | ||
* TODO: ensure we cannot register them twice | ||
* @param axios | ||
Check warning on line 117 in src/main.ts GitHub Actions / NPM lint
|
||
*/ | ||
export function addPasswordConfirmationInterceptors(axios: AxiosInstance): void { | ||
// We should never have more than one request waiting for password confirmation | ||
// but in doubt, we use a map to store the state of potential synchronous requests. | ||
const requestState: Record<symbol, AuthenticatedRequestState> = {} | ||
const resolveConfig: Record<symbol, (value: InternalAxiosRequestConfig) => void> = {} | ||
|
||
axios.interceptors.request.use( | ||
async (config) => { | ||
if (config.confirmPassword === undefined) { | ||
|
@@ -120,66 +127,44 @@ export function addPasswordConfirmationInterceptors(axios: AxiosInstance): void | |
return config | ||
} | ||
|
||
return new Promise((resolve) => { | ||
const confirmPasswordId = config.confirmPasswordId ?? Symbol('authenticated-request') | ||
resolveConfig[confirmPasswordId] = resolve | ||
const password = await getPassword() | ||
|
||
if (config.confirmPasswordId !== undefined) { | ||
return | ||
if (config.confirmPassword === 'reminder') { | ||
const url = generateUrl('/login/confirm') | ||
const { data } = await axios.post(url, { password }) | ||
window.nc_lastLogin = data.lastLogin | ||
} else { | ||
config.auth = { | ||
username: getCurrentUser()?.uid ?? '', | ||
password, | ||
} | ||
} | ||
|
||
getPasswordDialog(async (password: string) => { | ||
if (config.confirmPassword === 'reminder') { | ||
const url = generateUrl('/login/confirm') | ||
const { data } = await axios.post(url, { password }) | ||
window.nc_lastLogin = data.lastLogin | ||
resolveConfig[confirmPasswordId](config) | ||
} else { | ||
// We store all the necessary information to resolve or reject | ||
// the password confirmation in the response interceptor. | ||
requestState[confirmPasswordId] = Promise.withResolvers() | ||
|
||
// Resolving the config will trigger the request. | ||
resolveConfig[confirmPasswordId]({ | ||
...config, | ||
confirmPasswordId, | ||
auth: { | ||
username: getCurrentUser()?.uid ?? '', | ||
password, | ||
}, | ||
}) | ||
|
||
await requestState[confirmPasswordId].promise | ||
window.nc_lastLogin = Date.now() / 1000 | ||
} | ||
}) | ||
}) | ||
return config | ||
}, | ||
) | ||
|
||
axios.interceptors.response.use( | ||
(response) => { | ||
if (response.config.confirmPasswordId !== undefined) { | ||
requestState[response.config.confirmPasswordId].resolve() | ||
delete requestState[response.config.confirmPasswordId] | ||
} | ||
|
||
window.nc_lastLogin = Date.now() / 1000 | ||
getDialog().$destroy() | ||
return response | ||
}, | ||
(error) => { | ||
if (error.config.confirmPasswordId === undefined) { | ||
return error | ||
} | ||
|
||
if (error.response?.status !== 403 || error.response.data.message !== 'Password confirmation is required') { | ||
return error | ||
} | ||
|
||
// If the password confirmation failed, we reject the promise and trigger another request. | ||
// That other request will go through the password confirmation flow again. | ||
requestState[error.config.confirmPasswordId].reject() | ||
delete requestState[error.config.confirmPasswordId] | ||
// If the password confirmation failed, we trigger another request. | ||
// that will go through the password confirmation flow again. | ||
getDialog().$data.showError = true | ||
getDialog().$data.loading = false | ||
return axios.request(error.config) | ||
}, | ||
{ | ||
runWhen(config) { | ||
return config.confirmPassword !== undefined | ||
}, | ||
}, | ||
) | ||
} |