55import Vue from 'vue'
66import type { ComponentInstance } from 'vue'
77
8- import type { AxiosInstance , InternalAxiosRequestConfig } from '@nextcloud/axios'
8+ import type { AxiosInstance } from '@nextcloud/axios'
9+ import axios from '@nextcloud/axios'
910import { getCurrentUser } from '@nextcloud/auth'
1011
1112import PasswordDialogVue from './components/PasswordDialog.vue'
@@ -14,12 +15,6 @@ import { generateUrl } from '@nextcloud/router'
1415
1516const PAGE_LOAD_TIME = Date . now ( )
1617
17- interface AuthenticatedRequestState {
18- promise : Promise < void > ,
19- resolve : ( ) => void ,
20- reject : ( ) => void ,
21- }
22-
2318/**
2419 * Check if password confirmation is required according to the last confirmation time.
2520 * Use as a replacement of deprecated `OC.PasswordConfirmation.requiresPasswordConfirmation()`.
@@ -44,55 +39,70 @@ export const isPasswordConfirmationRequired = (mode: 'reminder'|'inRequest'): bo
4439 * Confirm password if needed.
4540 * Replacement of deprecated `OC.PasswordConfirmation.requirePasswordConfirmation(callback)`
4641 *
47- * @return {Promise<void> } Promise that resolves when password is confirmed or not needded .
42+ * @return {Promise<void> } Promise that resolves when password is confirmed or not needed .
4843 * Rejects if password confirmation was cancelled
4944 * or confirmation is already in process.
5045 */
51- export const confirmPassword = ( ) : Promise < void > => {
52- if ( ! isPasswordConfirmationRequired ( ) ) {
46+ export const confirmPassword = async ( ) : Promise < void > => {
47+ if ( ! isPasswordConfirmationRequired ( 'reminder' ) ) {
5348 return Promise . resolve ( )
5449 }
5550
56- return getPasswordDialog ( )
51+ const password = await getPassword ( )
52+ return _confirmPassword ( password )
5753}
5854
5955/**
6056 *
61- * @param mode
62- * @param callback
63- * @return
57+ * @param password
6458 */
65- function getPasswordDialog ( callback : ( password : string ) => Promise < void > ) : Promise < void > {
66- const isDialogMounted = Boolean ( document . getElementById ( DIALOG_ID ) )
67- if ( isDialogMounted ) {
68- return Promise . reject ( new Error ( 'Password confirmation dialog already mounted' ) )
69- }
59+ async function _confirmPassword ( password : string ) {
60+ const url = generateUrl ( '/login/confirm' )
61+ const { data } = await axios . post ( url , { password } )
62+ window . nc_lastLogin = data . lastLogin
63+ }
64+
65+ /**
66+ *
67+ */
68+ function getDialog ( ) : Vue {
69+ const element = document . getElementById ( DIALOG_ID )
7070
71- const mountPoint = document . createElement ( 'div' )
72- mountPoint . setAttribute ( 'id' , DIALOG_ID )
71+ if ( element !== null ) {
72+ return element . __vue__ . $root
73+ } else {
74+ const mountPoint = document . createElement ( 'div' )
75+ mountPoint . setAttribute ( 'id' , DIALOG_ID )
7376
74- const modals = Array . from ( document . querySelectorAll ( `.${ MODAL_CLASS } ` ) as NodeListOf < HTMLElement > )
75- // Filter out hidden modals
76- . filter ( ( modal ) => modal . style . display !== 'none' )
77+ const modals = Array . from ( document . querySelectorAll ( `.${ MODAL_CLASS } ` ) as NodeListOf < HTMLElement > )
78+ // Filter out hidden modals
79+ . filter ( ( modal ) => modal . style . display !== 'none' )
7780
78- const isModalMounted = Boolean ( modals . length )
81+ const isModalMounted = Boolean ( modals . length )
7982
80- if ( isModalMounted ) {
81- const previousModal = modals [ modals . length - 1 ]
82- previousModal . prepend ( mountPoint )
83- } else {
84- document . body . appendChild ( mountPoint )
85- }
83+ if ( isModalMounted ) {
84+ const previousModal = modals [ modals . length - 1 ]
85+ previousModal . prepend ( mountPoint )
86+ } else {
87+ document . body . appendChild ( mountPoint )
88+ }
8689
87- const DialogClass = Vue . extend ( PasswordDialogVue )
88- // Mount point element is replaced by the component
89- const dialog = ( new DialogClass ( { propsData : { callback } } ) as ComponentInstance ) . $mount ( mountPoint )
90+ const DialogClass = Vue . extend ( PasswordDialogVue )
91+ // Mount point element is replaced by the component
92+ return ( new DialogClass ( ) as ComponentInstance ) . $mount ( mountPoint )
93+ }
94+ }
9095
96+ /**
97+ *
98+ * @param callback
99+ */
100+ function getPassword ( callback : ( ) => void ) : Promise < string > {
91101 return new Promise ( ( resolve , reject ) => {
92- dialog . $on ( 'confirmed' , ( ) => {
93- dialog . $destroy ( )
94- resolve ( )
95- } )
102+ const dialog = getDialog ( )
103+
104+ dialog . $on ( 'submit' , callback )
105+
96106 dialog . $on ( 'close' , ( ) => {
97107 dialog . $destroy ( )
98108 reject ( new Error ( 'Dialog closed' ) )
@@ -103,13 +113,10 @@ function getPasswordDialog(callback: (password: string) => Promise<void>): Promi
103113/**
104114 * Add axios interceptors to an axios instance that will ask for
105115 * password confirmation to add it as Basic Auth for every requests.
116+ * TODO: ensure we cannot register them twice
117+ * @param axios
106118 */
107119export function addPasswordConfirmationInterceptors ( axios : AxiosInstance ) : void {
108- // We should never have more than one request waiting for password confirmation
109- // but in doubt, we use a map to store the state of potential synchronous requests.
110- const requestState : Record < symbol , AuthenticatedRequestState > = { }
111- const resolveConfig : Record < symbol , ( value : InternalAxiosRequestConfig ) => void > = { }
112-
113120 axios . interceptors . request . use (
114121 async ( config ) => {
115122 if ( config . confirmPassword === undefined ) {
@@ -120,66 +127,44 @@ export function addPasswordConfirmationInterceptors(axios: AxiosInstance): void
120127 return config
121128 }
122129
123- return new Promise ( ( resolve ) => {
124- const confirmPasswordId = config . confirmPasswordId ?? Symbol ( 'authenticated-request' )
125- resolveConfig [ confirmPasswordId ] = resolve
130+ const password = await getPassword ( )
126131
127- if ( config . confirmPasswordId !== undefined ) {
128- return
132+ if ( config . confirmPassword === 'reminder' ) {
133+ const url = generateUrl ( '/login/confirm' )
134+ const { data } = await axios . post ( url , { password } )
135+ window . nc_lastLogin = data . lastLogin
136+ } else {
137+ config . auth = {
138+ username : getCurrentUser ( ) ?. uid ?? '' ,
139+ password,
129140 }
141+ }
130142
131- getPasswordDialog ( async ( password : string ) => {
132- if ( config . confirmPassword === 'reminder' ) {
133- const url = generateUrl ( '/login/confirm' )
134- const { data } = await axios . post ( url , { password } )
135- window . nc_lastLogin = data . lastLogin
136- resolveConfig [ confirmPasswordId ] ( config )
137- } else {
138- // We store all the necessary information to resolve or reject
139- // the password confirmation in the response interceptor.
140- requestState [ confirmPasswordId ] = Promise . withResolvers ( )
141-
142- // Resolving the config will trigger the request.
143- resolveConfig [ confirmPasswordId ] ( {
144- ...config ,
145- confirmPasswordId,
146- auth : {
147- username : getCurrentUser ( ) ?. uid ?? '' ,
148- password,
149- } ,
150- } )
151-
152- await requestState [ confirmPasswordId ] . promise
153- window . nc_lastLogin = Date . now ( ) / 1000
154- }
155- } )
156- } )
143+ return config
157144 } ,
158145 )
159146
160147 axios . interceptors . response . use (
161148 ( response ) => {
162- if ( response . config . confirmPasswordId !== undefined ) {
163- requestState [ response . config . confirmPasswordId ] . resolve ( )
164- delete requestState [ response . config . confirmPasswordId ]
165- }
166-
149+ window . nc_lastLogin = Date . now ( ) / 1000
150+ getDialog ( ) . $destroy ( )
167151 return response
168152 } ,
169153 ( error ) => {
170- if ( error . config . confirmPasswordId === undefined ) {
171- return error
172- }
173-
174154 if ( error . response ?. status !== 403 || error . response . data . message !== 'Password confirmation is required' ) {
175155 return error
176156 }
177157
178- // If the password confirmation failed, we reject the promise and trigger another request.
179- // That other request will go through the password confirmation flow again.
180- requestState [ error . config . confirmPasswordId ] . reject ( )
181- delete requestState [ error . config . confirmPasswordId ]
158+ // If the password confirmation failed, we trigger another request.
159+ // that will go through the password confirmation flow again.
160+ getDialog ( ) . $data . showError = true
161+ getDialog ( ) . $data . loading = false
182162 return axios . request ( error . config )
183163 } ,
164+ {
165+ runWhen ( config ) {
166+ return config . confirmPassword !== undefined
167+ } ,
168+ } ,
184169 )
185170}
0 commit comments