let user change password
Massimo Melina committed
Dec 26, 2021 at 19:08 UTC
3325bcd207271fced0a7994a2be8d78bc259cabf
9 files changed
+86
-34
frontend/src/Head.ts
+31
-13
@@ -6,7 +6,8 @@ import { formatBytes, hIcon, prefix } from './misc'
6
import { Spinner } from './components'
7
import { state, useSnapState } from './state'
8
import { useDebounce } from 'use-debounce'
9
-import { closeDialog, newDialog, promptDialog } from './dialog'
9
+import { alertDialog, closeDialog, newDialog, promptDialog } from './dialog'
10
+import { apiCall } from './api'
11
12
export function Head() {
13
return h('header', {},
@@ -82,18 +83,7 @@ function LoginButton() {
83
icon: 'user',
84
label: snap.username,
85
onClick(){
85
- newDialog({
86
- content: ()=> h('div',{ id:'user-panel' },
87
- h('div',{}, 'User: '+snap.username),
88
- h(MenuButton,{
89
- icon: 'logout',
90
- label: 'Logout',
91
- onClick(){
92
- logout().then(closeDialog)
93
- }
94
- })
95
- )
96
- })
86
+ newDialog({ content: UserPanel })
87
},
88
} : {
89
icon: 'login',
@@ -108,6 +98,34 @@ function LoginButton() {
98
})
99
}
100
101
+function UserPanel() {
102
+ const snap = useSnapState()
103
+ return h('div',{ id:'user-panel' },
104
+ h('div',{}, 'User: '+snap.username),
105
+ h(MenuButton,{
106
+ icon: 'key',
107
+ label: 'Change password',
108
+ async onClick(){
109
+ const pwd = await promptDialog('Enter new password', { type:'password' })
110
+ if (!pwd) return
111
+ const check = await promptDialog('RE-enter new password', { type:'password' })
112
+ if (!check) return
113
+ if (check !== pwd)
114
+ return alertDialog('The second password you entered did not match the first. Procedure aborted.', 'warning')
115
+ await apiCall('change_pwd', { newPassword: pwd })
116
+ return alertDialog('Password changed')
117
+ }
118
+ }),
119
+ h(MenuButton,{
120
+ icon: 'logout',
121
+ label: 'Logout',
122
+ onClick(){
123
+ logout().then(closeDialog)
124
+ }
125
+ })
126
+ )
127
+}
128
+
129
function FolderStats() {
130
const { list, loading } = useContext(ListContext)
131
const stats = useMemo(() =>{
frontend/src/api.ts
+5
-1
@@ -10,12 +10,16 @@ export function apiCall(cmd: string, params?: object) : Promise<any> {
10
headers: { 'content-type': 'application/json' },
11
body: params && JSON.stringify(params),
12
}).then(res => {
13
+ stop()
14
if (res.ok)
15
return res.json()
16
const msg = 'Failed API ' + cmd
17
console.warn(msg + (params ? ' ' + JSON.stringify(params) : ''))
18
throw new ApiError(res.status, msg)
18
- }).finally(stop)
19
+ }, err => {
20
+ stop()
21
+ throw err
22
+ })
23
}
24
25
export class ApiError extends Error {
frontend/src/dialog.ts
+10
-2
@@ -8,7 +8,8 @@ interface DialogOptions {
8
onClose?: (v?:any)=> any,
9
className?: string,
10
icon?: string | FunctionComponent,
11
- closableContent?: string | ReactNode
11
+ closableContent?: string | ReactNode,
12
+ reserveClosing?: true
13
}
14
15
const dialogs = proxy<DialogOptions[]>([])
@@ -57,7 +58,14 @@ export function newDialog(options: DialogOptions) {
58
}
59
60
export function closeDialog(v?:any) {
60
- dialogs.pop()?.onClose?.(v)
61
+ let i = dialogs.length
62
+ while (i--) {
63
+ const d = dialogs[i]
64
+ if (d.reserveClosing)
65
+ continue
66
+ dialogs.splice(i,1)
67
+ d.onClose?.(v)
68
+ }
69
}
70
71
interface PromptOptions { def?:string, type?:string }
frontend/src/index.scss
+4
-4
@@ -138,10 +138,6 @@ ul.dir {
138
margin: 0.1em;
139
font-size: 1.1em;
140
}
141
- & button label {
142
- cursor: inherit;
143
- margin-left: 0.8em;
144
- }
141
}
142
#searched {
143
margin: .2em;
@@ -152,6 +148,10 @@ ul.dir {
148
gap: 1em;
149
}
150
151
+button label {
152
+ cursor: inherit;
153
+ margin-left: 0.5em;
154
+}
155
.dialog.working {
156
background: none;
157
font-size: 5em;
frontend/src/misc.ts
+1
@@ -62,6 +62,7 @@ export function working() {
62
return newDialog({
63
closable: false,
64
content: Spinner,
65
+ reserveClosing: true,
66
className: 'working',
67
})
68
}
src/apis.ts
+8
-2
@@ -2,7 +2,7 @@ import Koa from 'koa'
2
import { vfs, VfsNode, walkNode } from './vfs'
3
import { stat } from 'fs/promises'
4
import _ from 'lodash'
5
-import { getCurrentUserExpanded, verifyLogin } from './perm'
5
+import { getCurrentUsername, getCurrentUsernameExpanded, updateAccount, verifyLogin } from './perm'
6
import { sessions } from './sessions'
7
import createSSE from './sse'
8
import { basename } from 'path'
@@ -51,7 +51,7 @@ export const frontEndApis: ApiHandlers = {
51
limit = Number(limit)
52
const re = new RegExp(_.escapeRegExp(search),'i')
53
const match = (s?:string) => !s || !search || re.test(s)
54
- const who = await getCurrentUserExpanded(ctx) // cache value
54
+ const who = await getCurrentUsernameExpanded(ctx) // cache value
55
const walker = walkNode(node, who, search ? Infinity : 0)
56
const sseSrv = sse ? createSSE(ctx) : null
57
const res = produceEntries()
@@ -129,6 +129,12 @@ export const frontEndApis: ApiHandlers = {
129
return sess
130
},
131
132
+ async change_pwd({ newPassword }, ctx) {
133
+ await updateAccount(await getCurrentUsername(ctx), account => {
134
+ account.password = newPassword
135
+ })
136
+ return true
137
+ }
138
}
139
140
async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
src/perm.ts
+25
-8
@@ -11,23 +11,23 @@ import Koa from 'koa'
11
12
const PATH = argv.accounts || 'accounts.yaml'
13
14
-interface UserDetails {
14
+interface Account {
15
user: string, // we'll have user in it, so we don't need to pass it separately
16
password?: string
17
hashedPassword?: string
18
belongs?: string[]
19
}
20
-interface Accounts { [username:string]: UserDetails }
20
+interface Accounts { [username:string]: Account }
21
22
let accounts: Accounts = {}
23
24
-export async function getCurrentUser(ctx: Koa.Context) {
24
+export async function getCurrentUsername(ctx: Koa.Context) {
25
const id = ctx.cookies.get(SESSION_COOKIE)
26
return id && sessions.get(id)?.user || ''
27
}
28
29
-export async function getCurrentUserExpanded(ctx: Koa.Context) {
30
- const who = await getCurrentUser(ctx)
29
+export async function getCurrentUsernameExpanded(ctx: Koa.Context) {
30
+ const who = await getCurrentUsername(ctx)
31
if (!who)
32
return []
33
const ret = [who]
@@ -46,8 +46,25 @@ export async function verifyLogin(user:string, password: string) {
46
return h && verifyPassword(h, password)
47
}
48
49
-export function getAccount(user:string) : UserDetails {
50
- return accounts[user]
49
+export function getAccount(username:string) : Account {
50
+ return accounts[username]
51
+}
52
+
53
+type Changer = (account:Account)=> void | Promise<void>
54
+export async function updateAccount(username: string, changer:Changer) {
55
+ const account = getAccount(username)
56
+ await changer(account)
57
+ if (account.password) {
58
+ account.hashedPassword = await hashPassword(account.password)
59
+ delete account.password
60
+ }
61
+ saveAccountsAsap()
62
+}
63
+
64
+const saveAccountsAsap = _.debounce(saveAccounts)
65
+
66
+function saveAccounts() {
67
+ return fs.writeFile(PATH, yaml.stringify({ accounts }))
68
}
69
70
let doing = false
@@ -87,7 +104,7 @@ async function load() {
104
}
105
}))
106
if (changed)
90
- await fs.writeFile(PATH, yaml.stringify(res))
107
+ await saveAccountsAsap()
108
}
109
finally { doing = false }
110
}
src/vfs.ts
+2
-2
@@ -5,7 +5,7 @@ import { FSWatcher, watch } from 'fs'
5
import { dirname, basename } from 'path'
6
import { isMatch } from 'micromatch'
7
import { complySlashes, enforceFinal, prefix, readFileBusy } from './misc'
8
-import { getCurrentUserExpanded } from './perm'
8
+import { getCurrentUsernameExpanded } from './perm'
9
import Koa from 'koa'
10
import glob from 'fast-glob'
11
import _ from 'lodash'
@@ -83,7 +83,7 @@ export class Vfs {
83
}
84
85
async urlToNode(url: string, ctx: Koa.Context) : Promise<VfsNode | undefined> {
86
- const users = await getCurrentUserExpanded(ctx)
86
+ const users = await getCurrentUsernameExpanded(ctx)
87
let run = this.root
88
const rest = url.split('/').filter(Boolean).map(decodeURIComponent)
89
if (forbidden(run, users)) return
todo.md
-2
@@ -1,6 +1,5 @@
1
# To do
2
- file sorting
3
-- let user change password (need dialogs)
3
- folders before?
4
- upload
5
- log file
@@ -14,7 +13,6 @@
13
- user.redirect
14
- config: bans
15
- config: min disk space
17
-- frontend: dialogs
16
- frontend: don't depend on cdn
17
- webdav?
18
- vfs: ability to remove/hide/rename files deep in a source