diff --git a/authentication/pkg.go b/authentication/pkg.go index 9eb6245..c0b9a2e 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -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 diff --git a/main.go b/main.go index 213d1ed..86e52e9 100644 --- a/main.go +++ b/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 } // }}} diff --git a/sql/00010.sql b/sql/00010.sql new file mode 100644 index 0000000..ecd8ab4 --- /dev/null +++ b/sql/00010.sql @@ -0,0 +1 @@ +ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL; diff --git a/static/css/notes2.css b/static/css/notes2.css index a48a319..f8d46eb 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -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; } - } } diff --git a/static/images/icon_menu.svg b/static/images/icon_menu.svg index e60bdee..cfdd1e8 100644 --- a/static/images/icon_menu.svg +++ b/static/images/icon_menu.svg @@ -2,18 +2,18 @@ - - - - menu - hamburger - - - + transform="translate(-147.15925,-92.339586)">menuhamburger diff --git a/static/js/api.mjs b/static/js/api.mjs index 3fff10a..26a19de 100644 --- a/static/js/api.mjs +++ b/static/js/api.mjs @@ -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() {//{{{ diff --git a/static/js/page_preferences.mjs b/static/js/page_preferences.mjs new file mode 100644 index 0000000..273523c --- /dev/null +++ b/static/js/page_preferences.mjs @@ -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 = ` +

Preferences

+ ` + }// }}} + 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 + } +} diff --git a/static/js/page_storage.mjs b/static/js/page_storage.mjs index 931a718..a007130 100644 --- a/static/js/page_storage.mjs +++ b/static/js/page_storage.mjs @@ -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() diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index 07e8678..6cd5814 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -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 }) diff --git a/static/js/sync.mjs b/static/js/sync.mjs index fe72c3f..daa603f 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -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 } } diff --git a/user.go b/user.go deleted file mode 100644 index b1c2abf..0000000 --- a/user.go +++ /dev/null @@ -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 -} diff --git a/user/pkg.go b/user/pkg.go new file mode 100644 index 0000000..4810df0 --- /dev/null +++ b/user/pkg.go @@ -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 +} diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index d54c71d..2755aea 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -32,9 +32,10 @@ -
- -
+ + + + @@ -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'