Skip to content

Commit 6707df4

Browse files
committed
WIP
Signed-off-by: Louis Chemineau <[email protected]>
1 parent cea8fba commit 6707df4

File tree

4 files changed

+112
-120
lines changed

4 files changed

+112
-120
lines changed

package-lock.json

Lines changed: 36 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"bugs": "https://github.com/nextcloud-libraries/nextcloud-password-confirmation/issues",
3939
"license": "MIT",
4040
"dependencies": {
41+
"@nextcloud/auth": "^2.4.0",
4142
"@nextcloud/axios": "^2.5.0",
4243
"@nextcloud/l10n": "^3.1.0",
4344
"@nextcloud/router": "^3.0.1"

src/components/PasswordDialog.vue

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@update:open="close">
1212
<!-- Dialog content -->
1313
<p>{{ t('This action needs authentication') }}</p>
14-
<form class="vue-password-confirmation__form" @submit.prevent="confirm">
14+
<form class="vue-password-confirmation__form" @submit.prevent="submit">
1515
<NcPasswordField ref="field"
1616
:value.sync="password"
1717
:label="t('Password')"
@@ -58,13 +58,6 @@ export default defineComponent({
5858
NcPasswordField,
5959
},
6060
61-
props: {
62-
callback: {
63-
type: Function,
64-
required: true,
65-
},
66-
},
67-
6861
setup() {
6962
// non reactive props
7063
return {
@@ -99,7 +92,7 @@ export default defineComponent({
9992
methods: {
10093
t,
10194
102-
async confirm(): Promise<void> {
95+
async submit(): Promise<void> {
10396
this.showError = false
10497
this.loading = true
10598
@@ -108,15 +101,7 @@ export default defineComponent({
108101
return
109102
}
110103
111-
try {
112-
await this.callback(this.password)
113-
this.$emit('confirmed')
114-
} catch (e) {
115-
this.showError = true
116-
this.selectPasswordField()
117-
} finally {
118-
this.loading = false
119-
}
104+
this.$emit('submit', this.password)
120105
},
121106
122107
close(open: boolean): void {

src/main.ts

Lines changed: 72 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import Vue from 'vue'
66
import 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'
910
import { getCurrentUser } from '@nextcloud/auth'
1011

1112
import PasswordDialogVue from './components/PasswordDialog.vue'
@@ -14,12 +15,6 @@ import { generateUrl } from '@nextcloud/router'
1415

1516
const 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
*/
107119
export 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

Comments
 (0)