show login before accessing protected items
Massimo Melina committed
Jun 7, 2024 at 17:42 UTC
323941065f96ca5d01c6254d781242d61bc5c94d
4 files changed
+28
-13
frontend/src/BrowseFiles.ts
+18
-6
@@ -4,13 +4,13 @@ import { Link } from 'react-router-dom'
4
import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState,
5
useId} from 'react'
6
import { useMediaQuery, useWindowSize } from 'usehooks-ts'
7
-import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac } from './misc'
7
+import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, getHFS } from './misc'
8
import { Checkbox, CustomCode, iconBtn, Spinner } from './components'
9
import { Head } from './Head'
10
import { DirEntry, state, useSnapState } from './state'
11
import { alertDialog } from './dialog'
12
import useFetchList from './useFetchList'
13
-import { useAuthorized } from './login'
13
+import { loginDialog, useAuthorized } from './login'
14
import { acceptDropFiles, enqueue } from './upload'
15
import _ from 'lodash'
16
import { t, useI18N } from './i18n'
@@ -207,6 +207,17 @@ const Entry = ({ entry, midnight, separator }: EntryProps) => {
207
const showingButton = !file_menu_on_link || isFolder && !hasHover
208
const ariaId = useId()
209
const ariaProps = { id: ariaId, 'aria-label': prefix(name + ', ', isFolder ? t`Folder` : entry.web ? t`Web page` : isLink ? t`Link` : '') }
210
+ const loginProps = entry.cantOpen && {
211
+ async onClick(ev: any) {
212
+ ev.preventDefault()
213
+ if (entry.cantOpen === DirEntry.FORBIDDEN)
214
+ return alertDialog(t`Forbidden`, 'warning')
215
+ if (!await loginDialog(true, false)) return
216
+ if (isFolder && !entry.web) // internal navigation
217
+ return setTimeout(() => getHFS().navigate(uri)) // couldn't find the reason why navigating sync is reverted back
218
+ location.href = uri
219
+ }
220
+ }
221
return h('li', { className, label: separator },
222
h(CustomCode, { name: 'entry', entry },
223
showFilter && h(Checkbox, {
@@ -220,15 +231,16 @@ const Entry = ({ entry, midnight, separator }: EntryProps) => {
231
},
232
}),
233
h('span', { className: 'link-wrapper' }, // container to handle mouse over for both children
223
- isFolder || entry.web ? h(Fragment, {}, // internal navigation, use Link component
224
- h(Link, { to: uri, reloadDocument: entry.web, ...ariaProps }, // without reloadDocument, once you enter the web page, the back button won't bring you back to the frontend
234
+ // we treat webpages as folders, with menu to comment
235
+ isFolder ? h(Fragment, {}, // internal navigation, use Link component
236
+ h(Link, { to: uri, reloadDocument: entry.web, ...ariaProps, ...loginProps }, // without reloadDocument, once you enter the web page, the back button won't bring you back to the frontend
237
ico, entry.n.slice(0, -1)), // don't use name, as we want to include whole path in case of search
238
// popup button is here to be able to detect link-wrapper:hover
239
file_menu_on_link && !showingButton && h('button', {
240
className: 'popup-menu-button',
241
onClick: fileMenu
242
}, hIcon('menu'), t`Menu`)
231
- ) : h('a', { href: uri, onClick, target: entry.target, ...ariaProps },
243
+ ) : h('a', { href: uri, onClick, target: entry.target, ...ariaProps, ...loginProps },
244
ico, h('span', { className: 'container-folder' }, containerName), name ),
245
),
246
h(CustomCode, { name: 'afterEntryName', entry }),
@@ -269,7 +281,7 @@ export const EntryDetails = memo(({ entry, midnight }: { entry: DirEntry, midnig
281
const dd = '2-digit'
282
return h('div', { className: 'entry-details' },
283
h(CustomCode, { name: 'additionalEntryDetails', entry }),
272
- entry.p?.match(entry.isFolder ? /l/i : /r/i) && hIcon('password', { className: 'miss-perm', title: t(MISSING_PERM) }),
284
+ entry.cantOpen && hIcon(entry.cantOpen === DirEntry.FORBIDDEN ? 'lock' : 'password', { className: 'miss-perm', title: t(MISSING_PERM) }),
285
h(EntrySize, { s }),
286
time && h('span', {
287
className: 'entry-ts',
frontend/src/login.ts
+4
-3
@@ -16,7 +16,6 @@ async function login(username:string, password:string) {
16
stopWorking()
17
sessionRefresher(res)
18
state.loginRequired = false
19
- reloadList()
19
return res
20
}, (err: any) => {
21
stopWorking()
@@ -43,7 +42,7 @@ export function logout() {
42
43
export let closeLoginDialog: undefined | (() => void)
44
let lastPromise: Promise<any>
46
-export async function loginDialog(closable=false) {
45
+export async function loginDialog(closable=true, reloadAfter=true) {
46
return lastPromise = new Promise(resolve => {
47
if (closeLoginDialog)
48
return lastPromise
@@ -119,6 +118,8 @@ export async function loginDialog(closable=false) {
118
if (res?.redirect)
119
setTimeout(() => // workaround: the history.back() issued by closing the dialog is messing with our navigation
120
getHFS().navigate(res.redirect), 10) // from my tests 1 was enough, 0 was not (not always). Would be nice to find a cleaner way
121
+ else if (reloadAfter)
122
+ reloadList()
123
} catch (err: any) {
124
await alertDialog(err)
125
usrRef.current?.focus()
@@ -141,7 +142,7 @@ export function useAuthorized() {
142
if (!loginRequired)
143
return closeLoginDialog?.()
144
if (!closeLoginDialog)
144
- void loginDialog()
145
+ void loginDialog(false)
146
}, [loginRequired])
147
return loginRequired ? null : true
148
}
frontend/src/menu.ts
+1
-1
@@ -180,7 +180,7 @@ function LoginButton() {
180
icon: 'login',
181
label: t`Login`,
182
onClickAnimation: false,
183
- onClick: () => loginDialog(true),
183
+ onClick: () => loginDialog(),
184
})
185
}
186
frontend/src/state.ts
+5
-3
@@ -89,6 +89,7 @@ function storeSettings() {
89
}
90
91
export class DirEntry {
92
+ static FORBIDDEN = 'FORBIDDEN'
93
public readonly n: string
94
public readonly s?: number
95
public readonly m?: string
@@ -103,9 +104,9 @@ export class DirEntry {
104
public readonly name: string
105
public readonly uri: string
106
public readonly ext: string = ''
106
- public readonly isFolder:boolean
107
+ public readonly isFolder: boolean
108
public readonly t?:Date
108
- public readonly cantOpen: boolean
109
+ public readonly cantOpen?: true | typeof DirEntry.FORBIDDEN
110
public readonly key?: string
111
112
constructor(n: string, rest?: any) {
@@ -122,7 +123,8 @@ export class DirEntry {
123
this.t = new Date(t)
124
this.name = this.isFolder ? this.n.slice(this.n.lastIndexOf('/', this.n.length - 2) + 1, -1)
125
: this.n.slice(this.n.lastIndexOf('/') + 1)
125
- this.cantOpen = Boolean(this.p?.includes(this.isFolder ? 'l' : 'r')) // to open we need list for folders and read for files
126
+ const x = this.isFolder && !this.web ? 'L' : 'R' // to open we need list for folders and read for files
127
+ this.cantOpen = this.p?.match(x) ? true : this.p?.match(x.toLowerCase()) ? DirEntry.FORBIDDEN : undefined
128
}
129
130
getNext() {