Small refactor for user preferences
This commit is contained in:
parent
1a712fb7a9
commit
81d02b82dc
13 changed files with 202 additions and 112 deletions
|
|
@ -8,6 +8,9 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
|
||||
// Internal
|
||||
appUser "notes2/user"
|
||||
|
||||
// Standard
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
|
|
@ -27,12 +30,6 @@ type Manager struct {
|
|||
ExpireDays int
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Username string
|
||||
Name string
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, err error) { // {{{
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
|
|
@ -165,16 +162,16 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques
|
|||
mngr.log.Info("authentication", "username", request.Username, "status", "accepted")
|
||||
j, _ := json.Marshal(struct {
|
||||
OK bool
|
||||
User User
|
||||
User appUser.User
|
||||
Token string
|
||||
}{true, user, token})
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{
|
||||
func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user appUser.User, err error) { // {{{
|
||||
var row *sql.Row
|
||||
row = mngr.db.QueryRow(`
|
||||
SELECT id, username, name
|
||||
SELECT id, username, name, preferences
|
||||
FROM public.user
|
||||
WHERE
|
||||
LOWER(username) = LOWER($1) AND
|
||||
|
|
@ -183,13 +180,21 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool
|
|||
username,
|
||||
password,
|
||||
)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.Name)
|
||||
var data []byte
|
||||
err = row.Scan(&user.ID, &user.Username, &user.Name, &data)
|
||||
if err != nil && err.Error() == "sql: no rows in result set" {
|
||||
err = nil
|
||||
authenticated = false
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
authenticated = false
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &user.Preferences)
|
||||
if err != nil {
|
||||
authenticated = false
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +283,7 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin
|
|||
changed = (rowsAffected == 1)
|
||||
return
|
||||
} // }}}
|
||||
func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{
|
||||
func (mngr *Manager) NewClientUUID(user appUser.User) (clientUUID string, err error) { // {{{
|
||||
// Each client session has its own UUID.
|
||||
// Loop through until a unique one is established.
|
||||
var proposedClientUUID string
|
||||
|
|
|
|||
27
main.go
27
main.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
// Internal
|
||||
"notes2/authentication"
|
||||
"notes2/html_template"
|
||||
appUser "notes2/user"
|
||||
"os"
|
||||
|
||||
// Standard
|
||||
|
|
@ -134,6 +135,7 @@ func main() { // {{{
|
|||
http.HandleFunc("/offline", pageOffline)
|
||||
|
||||
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
|
||||
http.HandleFunc("/user/preferences", authenticated(actionUserPreferences))
|
||||
|
||||
http.HandleFunc("/sync/from_server/count/{sequence}", authenticated(actionSyncFromServerCount))
|
||||
http.HandleFunc("/sync/from_server/{sequence}/{offset}", authenticated(actionSyncFromServer))
|
||||
|
|
@ -178,7 +180,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon
|
|||
}
|
||||
|
||||
// User object is added to the context for the next handler.
|
||||
user := NewUser(claims)
|
||||
user := appUser.NewUser(claims)
|
||||
r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user))
|
||||
|
||||
Log.Debug("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username, "client", user.ClientUUID)
|
||||
|
|
@ -360,8 +362,8 @@ func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
responseData(w, map[string]any{
|
||||
"OK": true,
|
||||
"Count": count,
|
||||
"OK": true,
|
||||
"Count": count,
|
||||
})
|
||||
} // }}}
|
||||
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
|
|
@ -389,6 +391,20 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
})
|
||||
} // }}}
|
||||
|
||||
func actionUserPreferences(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
user := getUser(r)
|
||||
prefs, err := user.Preferences()
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]any{
|
||||
"OK": true,
|
||||
"Preferences": prefs,
|
||||
})
|
||||
} // }}}
|
||||
|
||||
func createNewUser(username string) { // {{{
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
|
|
@ -431,7 +447,8 @@ func changePassword(username string) { // {{{
|
|||
|
||||
fmt.Printf("\nPassword changed\n")
|
||||
} // }}}
|
||||
func getUser(r *http.Request) UserSession { // {{{
|
||||
user, _ := r.Context().Value(CONTEXT_USER).(UserSession)
|
||||
func getUser(r *http.Request) appUser.UserSession { // {{{
|
||||
user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession)
|
||||
user.Db = db
|
||||
return user
|
||||
} // }}}
|
||||
|
|
|
|||
1
sql/00010.sql
Normal file
1
sql/00010.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL;
|
||||
|
|
@ -73,9 +73,10 @@ button {
|
|||
1fr;
|
||||
}
|
||||
|
||||
&.page-history {
|
||||
/* The other pages just gets the whole page without dividing it up. */
|
||||
&:not(.page-node) {
|
||||
grid-template-areas:
|
||||
"tree-expander tree pad1 n2-pagehistory pad2"
|
||||
"tree-expander tree pad1 n2-page pad2"
|
||||
;
|
||||
|
||||
grid-template-columns:
|
||||
|
|
@ -245,7 +246,6 @@ button {
|
|||
|
||||
#notes2 {
|
||||
&.page-node {
|
||||
|
||||
#page-root {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -260,7 +260,7 @@ button {
|
|||
display: contents;
|
||||
|
||||
n2-pagestorage {
|
||||
grid-area: content;
|
||||
grid-area: n2-page;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,9 +268,14 @@ button {
|
|||
&.page-history {
|
||||
#page-history {
|
||||
display: grid;
|
||||
grid-area: n2-pagehistory;
|
||||
grid-area: n2-page;
|
||||
}
|
||||
}
|
||||
|
||||
n2-pagehistory {}
|
||||
&.page-preferences {
|
||||
#page-preferences {
|
||||
display: grid !important;
|
||||
grid-area: n2-page;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +287,6 @@ button {
|
|||
#page-root {
|
||||
display: contents !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="26.666645"
|
||||
height="24"
|
||||
viewBox="0 0 7.0555498 6.35"
|
||||
width="12"
|
||||
height="23.999981"
|
||||
viewBox="0 0 3.1750001 6.349995"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
sodipodi:docname="icon_menu.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
|
|
@ -23,29 +23,34 @@
|
|||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2.096401"
|
||||
inkscape:cx="10.255672"
|
||||
inkscape:cy="9.0631517"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="1080"
|
||||
inkscape:zoom="11.859035"
|
||||
inkscape:cx="8.6010372"
|
||||
inkscape:cy="17.32856"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1401"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-146.57917,-92.339583)">
|
||||
<title
|
||||
id="title1">menu</title>
|
||||
<title
|
||||
id="title1-6">hamburger</title>
|
||||
<path
|
||||
d="m 153.63472,95.867362 c 0,0.391582 -0.31398,0.705554 -0.70555,0.705554 h -5.64445 c -0.38806,0 -0.70555,-0.313972 -0.70555,-0.705554 0,-0.391584 0.31749,-0.705556 0.70555,-0.705556 h 3.175 l 0.88194,0.705556 0.88195,-0.705556 h 0.70556 c 0.39157,0 0.70555,0.3175 0.70555,0.705556 m -3.52778,-3.527779 c -3.175,0 -3.175,2.116667 -3.175,2.116667 h 6.35 c 0,0 0,-2.116667 -3.175,-2.116667 m -3.175,5.291667 c 0,0.585612 0.47272,1.058333 1.05834,1.058333 h 4.23333 c 0.58561,0 1.05833,-0.472721 1.05833,-1.058333 v -0.352778 h -6.35 z"
|
||||
id="path1-2"
|
||||
style="stroke-width:0.352777" />
|
||||
</g>
|
||||
</svg>
|
||||
transform="translate(-147.15925,-92.339586)"><title
|
||||
id="title1">menu</title><title
|
||||
id="title1-6">hamburger</title><circle
|
||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
id="path3"
|
||||
cx="149.55338"
|
||||
cy="93.120461"
|
||||
r="0.78087437" /><circle
|
||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
id="circle4"
|
||||
cx="149.55338"
|
||||
cy="97.908707"
|
||||
r="0.78087437" /><circle
|
||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
id="circle5"
|
||||
cx="149.55338"
|
||||
cy="95.514587"
|
||||
r="0.78087437" /></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
export class API {
|
||||
// query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties.
|
||||
static async query(method, path, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const body = JSON.stringify(request)
|
||||
const headers = {}
|
||||
|
||||
|
|
@ -12,33 +12,22 @@ export class API {
|
|||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
fetch(path, { method, headers, body })
|
||||
.then(response => {
|
||||
// An HTTP communication level error occured.
|
||||
if (!response.ok || response.status != 200)
|
||||
return reject({
|
||||
type: 'http',
|
||||
error: response,
|
||||
})
|
||||
return response.json()
|
||||
})
|
||||
.then(json => {
|
||||
// Application level response are handled here.
|
||||
if (!json.OK)
|
||||
return reject({
|
||||
type: 'application',
|
||||
error: json.Error,
|
||||
application: json,
|
||||
})
|
||||
resolve(json)
|
||||
})
|
||||
.catch(err =>
|
||||
// Catch any other errors from fetch.
|
||||
reject({
|
||||
type: 'http',
|
||||
error: err,
|
||||
}))
|
||||
})
|
||||
const res = await fetch(path, { method, headers, body })
|
||||
// An HTTP communication level error occured.
|
||||
if (!res.ok || res.status != 200)
|
||||
throw new Error('HTTP error', { cause: { type: 'http', error: res, }})
|
||||
|
||||
// Application level response are handled here.
|
||||
const json = await res.json()
|
||||
if (!json.OK)
|
||||
throw new Error(json.Error, { cause: { type: 'application', application: json, }})
|
||||
|
||||
return json
|
||||
|
||||
} catch (err) {
|
||||
// Catch any other errors from fetch.
|
||||
throw new Error(err.message, { cause: { type: 'http', error: err, }})
|
||||
}
|
||||
}
|
||||
|
||||
static hasAuthenticationToken() {//{{{
|
||||
|
|
|
|||
32
static/js/page_preferences.mjs
Normal file
32
static/js/page_preferences.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { CustomHTMLElement } from "./lib/custom_html_element.mjs"
|
||||
import { API } from './api.mjs'
|
||||
|
||||
export class N2PagePreferences extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<h1>Preferences</h1>
|
||||
`
|
||||
}// }}}
|
||||
constructor() {// {{{
|
||||
super()
|
||||
window._mbus.subscribe('SHOW_PAGE', event => {
|
||||
if (event.detail.data?.page == 'preferences')
|
||||
this.render()
|
||||
})
|
||||
}// }}}
|
||||
async render() {// {{{
|
||||
}// }}}
|
||||
getPreferences() {
|
||||
API.query('GET', '/user/preferences')
|
||||
}
|
||||
}
|
||||
customElements.define('n2-pagepreferences', N2PagePreferences)
|
||||
|
||||
// Preferences is a set of preferences, of which there can be many named.
|
||||
class Preferences {
|
||||
constructor(name, data) {
|
||||
this.name = name
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,10 @@ export class N2PageStorage extends CustomHTMLElement {
|
|||
constructor() {
|
||||
super()
|
||||
|
||||
window._mbus.subscribe('SHOW_PAGE', () => this.render())
|
||||
window._mbus.subscribe('SHOW_PAGE', event => {
|
||||
if (event.detail.data?.page == 'storage')
|
||||
this.render()
|
||||
})
|
||||
}
|
||||
async render() {
|
||||
const countNodes = await globalThis.nodeStore.nodeCount()
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export class N2Sidebar extends CustomHTMLElement {
|
|||
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
|
||||
this.elSync.addEventListener('click', () => _sync.run())
|
||||
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
||||
this.elSettings.addEventListener('click', ()=> _mbus.dispatch('SHOW_PAGE', { page: 'preferences' }))
|
||||
this.elHideTree.addEventListener('click', event => {
|
||||
event.stopPropagation()
|
||||
_mbus.dispatch('TREE_EXPANSION', { expand: false })
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export class Sync {
|
|||
nodeStore.setAppState('latest_sync_node', currMax)
|
||||
} catch (e) {
|
||||
console.error('sync node tree', e)
|
||||
alert(e.message)
|
||||
} finally {
|
||||
syncEnd = Date.now()
|
||||
const duration = (syncEnd - syncStart) / 1000
|
||||
|
|
@ -157,8 +158,8 @@ export class Sync {
|
|||
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
|
||||
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
alert(e.error)
|
||||
console.error(e)
|
||||
alert(e.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
user.go
27
user.go
|
|
@ -1,27 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// External
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type UserSession struct {
|
||||
UserID int
|
||||
Username string
|
||||
Password string
|
||||
Name string
|
||||
ClientUUID string
|
||||
}
|
||||
|
||||
func NewUser(claims jwt.MapClaims) (u UserSession) {
|
||||
uid, _ := claims["uid"].(float64)
|
||||
name, _ := claims["name"].(string)
|
||||
username, _ := claims["login"].(string)
|
||||
clientUUID, _ := claims["cid"].(string)
|
||||
|
||||
u.UserID = int(uid)
|
||||
u.Username = username
|
||||
u.Name = name
|
||||
u.ClientUUID = clientUUID
|
||||
return
|
||||
}
|
||||
57
user/pkg.go
Normal file
57
user/pkg.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
// External
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Username string
|
||||
Name string
|
||||
Preferences map[string]UserPreferences
|
||||
}
|
||||
|
||||
type UserSession struct {
|
||||
UserID int
|
||||
Username string
|
||||
Password string
|
||||
Name string
|
||||
ClientUUID string
|
||||
Db *sqlx.DB
|
||||
}
|
||||
|
||||
type UserPreferences struct {
|
||||
DownloadImages bool
|
||||
DownloadFiles bool
|
||||
}
|
||||
|
||||
func NewUser(claims jwt.MapClaims) (u UserSession) {
|
||||
uid, _ := claims["uid"].(float64)
|
||||
name, _ := claims["name"].(string)
|
||||
username, _ := claims["login"].(string)
|
||||
clientUUID, _ := claims["cid"].(string)
|
||||
|
||||
u.UserID = int(uid)
|
||||
u.Username = username
|
||||
u.Name = name
|
||||
u.ClientUUID = clientUUID
|
||||
return
|
||||
}
|
||||
|
||||
func (u UserSession) Preferences() (prefs map[string]UserPreferences, err error) {
|
||||
row := u.Db.QueryRow(`SELECT preferences FROM public.user WHERE id=$1`, u.UserID)
|
||||
|
||||
var data []byte
|
||||
err = row.Scan(&data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &prefs)
|
||||
return
|
||||
}
|
||||
|
|
@ -32,9 +32,10 @@
|
|||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div id="page-history">
|
||||
<n2-pagehistory></n2-pagehistory>
|
||||
</div>
|
||||
<n2-pagehistory id="page-history"></n2-pagehistory>
|
||||
|
||||
<!-- Preferences -->
|
||||
<n2-pagepreferences id="page-preferences"></n2-pagepreferences>
|
||||
</div>
|
||||
|
||||
<n2-syncprogress></n2-syncprogress>
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
||||
import {API} from 'api'
|
||||
import {Sync} from 'sync'
|
||||
import { } from '/js/{{ .VERSION }}/page_preferences.mjs'
|
||||
import { } from '/js/{{ .VERSION }}/page_storage.mjs'
|
||||
import { } from '/js/{{ .VERSION }}/page_history.mjs'
|
||||
import { } from '/js/{{ .VERSION }}/file.mjs'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue