users.tsx (6533B)
1 // web/src/screen/users.tsx - accounts and live sessions 2 3 import m from 'mithril'; 4 import { page, patch, post, del } from '../util/api'; 5 import { session, Empty, Failed, Loading, Pager, When } from '../component/layout'; 6 7 const LIMIT = 25; 8 9 export const Users: m.Component = { 10 oninit(vnode: any) { 11 vnode.state.offset = 0; 12 vnode.state.rows = null; 13 vnode.state.error = null; 14 vnode.state.form = { username: '', password: '', role: 'user' }; 15 16 vnode.state.load = async () => { 17 try { 18 vnode.state.rows = await page<any>('/users', { 19 query: { offset: vnode.state.offset, limit: LIMIT }, 20 }); 21 vnode.state.error = null; 22 } catch (e) { 23 vnode.state.error = e; 24 } 25 m.redraw(); 26 }; 27 vnode.state.load(); 28 }, 29 30 view(vnode: any) { 31 const { rows, error } = vnode.state; 32 if (error && !rows) return <Failed error={error} />; 33 if (!rows) return <Loading />; 34 35 const admin = session.user?.role === 'admin'; 36 37 const create = async (e: Event) => { 38 e.preventDefault(); 39 try { 40 await post('/users', vnode.state.form); 41 vnode.state.form = { username: '', password: '', role: 'user' }; 42 await vnode.state.load(); 43 } catch (err) { 44 vnode.state.error = err; 45 } 46 }; 47 48 const change = async (id: string, body: unknown) => { 49 try { 50 await patch(`/users/${id}`, body); 51 await vnode.state.load(); 52 } catch (err) { 53 vnode.state.error = err; 54 } 55 }; 56 57 return [ 58 <h1>users</h1>, 59 error ? <Failed error={error} /> : null, 60 61 admin 62 ? ( 63 <form class="panel" onsubmit={create}> 64 <label> 65 <span>username</span> 66 <input 67 required 68 value={vnode.state.form.username} 69 oninput={(e: any) => { vnode.state.form.username = e.target.value; }} 70 /> 71 </label> 72 <label> 73 <span>password</span> 74 <input 75 required 76 type="password" 77 value={vnode.state.form.password} 78 oninput={(e: any) => { vnode.state.form.password = e.target.value; }} 79 /> 80 </label> 81 <label> 82 <span>role</span> 83 <select 84 value={vnode.state.form.role} 85 onchange={(e: any) => { vnode.state.form.role = e.target.value; }} 86 > 87 <option value="user">user</option> 88 <option value="admin">admin</option> 89 </select> 90 </label> 91 <button type="submit">create</button> 92 </form> 93 ) 94 : null, 95 96 <table> 97 <thead><tr><th>username</th><th>role</th><th>source</th><th>holdings</th><th>last seen</th><th /></tr></thead> 98 <tbody> 99 {rows.rows.map((u: any) => ( 100 <tr key={u.id} class={u.disabled ? 'disabled' : ''}> 101 <td>{u.username}</td> 102 <td> 103 {admin && u.id !== session.user?.id 104 ? ( 105 <select value={u.role} onchange={(e: any) => change(u.id, { role: e.target.value })}> 106 <option value="user">user</option> 107 <option value="admin">admin</option> 108 </select> 109 ) 110 : u.role} 111 </td> 112 <td>{u.source}</td> 113 <td class="muted">{`${u.project_count ?? 0} projects, ${u.worker_count ?? 0} workers`}</td> 114 <td><When at={u.last_login_at} /></td> 115 <td> 116 {admin && u.id !== session.user?.id 117 ? ( 118 <div class="rowactions"> 119 <button class="quiet" onclick={() => change(u.id, { disabled: !u.disabled })}> 120 {u.disabled ? 'enable' : 'disable'} 121 </button> 122 <button 123 class="quiet" 124 onclick={async () => { await del(`/users/${u.id}`); await vnode.state.load(); }} 125 > 126 remove 127 </button> 128 </div> 129 ) 130 : null} 131 </td> 132 </tr> 133 ))} 134 </tbody> 135 </table>, 136 137 <Pager 138 offset={rows.offset} 139 total={rows.total} 140 limit={LIMIT} 141 onmove={(offset: number) => { vnode.state.offset = offset; vnode.state.load(); }} 142 />, 143 ]; 144 }, 145 }; 146 147 export const Sessions: m.Component = { 148 oninit(vnode: any) { 149 vnode.state.offset = 0; 150 vnode.state.rows = null; 151 vnode.state.error = null; 152 153 vnode.state.load = async () => { 154 try { 155 vnode.state.rows = await page<any>('/sessions', { 156 query: { offset: vnode.state.offset, limit: LIMIT }, 157 }); 158 vnode.state.error = null; 159 } catch (e) { 160 vnode.state.error = e; 161 } 162 m.redraw(); 163 }; 164 vnode.state.load(); 165 }, 166 167 view(vnode: any) { 168 const { rows, error } = vnode.state; 169 if (error && !rows) return <Failed error={error} />; 170 if (!rows) return <Loading />; 171 172 return [ 173 <h1>sessions</h1>, 174 <p class="muted">Revoking a session signs that browser out the next time it asks for a token.</p>, 175 error ? <Failed error={error} /> : null, 176 177 rows.rows.length === 0 ? <Empty what="sessions" /> : ( 178 <table> 179 <thead><tr><th>browser</th><th>address</th><th>signed in</th><th>last used</th><th>expires</th><th /></tr></thead> 180 <tbody> 181 {rows.rows.map((s: any) => ( 182 <tr key={s.id}> 183 <td class="ellipsis">{s.user_agent ?? 'unknown'}</td> 184 <td class="mono">{s.ip ?? ''}</td> 185 <td><When at={s.issued_at} /></td> 186 <td><When at={s.last_used_at} /></td> 187 <td><When at={s.expires_at} /></td> 188 <td> 189 <button 190 class="quiet" 191 onclick={async () => { await del(`/sessions/${s.id}`); await vnode.state.load(); }} 192 > 193 revoke 194 </button> 195 </td> 196 </tr> 197 ))} 198 </tbody> 199 </table> 200 ), 201 202 <Pager 203 offset={rows.offset} 204 total={rows.total} 205 limit={LIMIT} 206 onmove={(offset: number) => { vnode.state.offset = offset; vnode.state.load(); }} 207 />, 208 ]; 209 }, 210 };