@samitouri / QOSami-HFS / commits / 32394106

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() {