diff --git a/authentication/pkg.go b/authentication/pkg.go index bcd84ed..9eb6245 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -2,8 +2,9 @@ package authentication import ( // External - _ "git.gibonuddevalla.se/go/wrappederror" + werr "git.gibonuddevalla.se/go/wrappederror" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/lib/pq" @@ -146,6 +147,14 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques data["uid"] = user.ID data["login"] = user.Username data["name"] = user.Name + + data["cid"], err = mngr.NewClientUUID(user) + if err != nil { + mngr.log.Error("authentication", "error", err) + httpError(w, err) + return + } + token, err = mngr.GenerateToken(data) if err != nil { mngr.log.Error("authentication", "error", err) @@ -269,3 +278,31 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin changed = (rowsAffected == 1) return } // }}} +func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{ + // Each client session has its own UUID. + // Loop through until a unique one is established. + var proposedClientUUID string + var numSessions int + for { + proposedClientUUID = uuid.NewString() + row := mngr.db.QueryRow("SELECT COUNT(id) FROM public.client WHERE client_uuid = $1", proposedClientUUID) + err = row.Scan(&numSessions) + if err != nil { + err = werr.Wrap(err).WithData(proposedClientUUID) + return + } + + if numSessions > 0 { + continue + } + + _, err = mngr.db.Exec(`INSERT INTO public.client(user_id, client_uuid) VALUES($1, $2)`, user.ID, proposedClientUUID) + if err != nil { + err = werr.Wrap(err).WithData(proposedClientUUID) + return + } + clientUUID = proposedClientUUID + break + } + return +} // }}} diff --git a/main.go b/main.go index 7e63d9e..5bd0f5e 100644 --- a/main.go +++ b/main.go @@ -167,7 +167,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon user := NewUser(claims) r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) - Log.Info("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username) + Log.Debug("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username, "client", user.ClientUUID) fn(w, r) } } // }}} @@ -246,22 +246,11 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - request := struct { - ClientUUID string - }{} - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &request) - if err != nil { - Log.Error("/node/tree", "error", err) - httpError(w, err) - return - } - user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) offset, _ := strconv.Atoi(r.PathValue("offset")) - nodes, maxSeq, moreRowsExist, err := Nodes(user.ID, offset, uint64(changedFrom), request.ClientUUID) + nodes, maxSeq, moreRowsExist, err := Nodes(user.UserID, offset, uint64(changedFrom), user.ClientUUID) if err != nil { Log.Error("/node/tree", "error", err) httpError(w, err) @@ -285,7 +274,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ var err error uuid := r.PathValue("uuid") - node, err := RetrieveNode(user.ID, uuid) + node, err := RetrieveNode(user.UserID, uuid) if err != nil { responseError(w, err) return @@ -301,7 +290,6 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ body, _ := io.ReadAll(r.Body) var request struct { - ClientUUID string NodeData string } err := json.Unmarshal(body, &request) @@ -310,7 +298,7 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ return } - db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.ID, request.ClientUUID, request.NodeData) + db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) responseData(w, map[string]interface{}{ "OK": true, @@ -359,7 +347,7 @@ func changePassword(username string) { // {{{ fmt.Printf("\nPassword changed\n") } // }}} -func getUser(r *http.Request) User { // {{{ - user, _ := r.Context().Value(CONTEXT_USER).(User) +func getUser(r *http.Request) UserSession { // {{{ + user, _ := r.Context().Value(CONTEXT_USER).(UserSession) return user } // }}} diff --git a/sql/00010.sql b/sql/00010.sql new file mode 100644 index 0000000..c0f14ee --- /dev/null +++ b/sql/00010.sql @@ -0,0 +1,10 @@ +CREATE TABLE public.client ( + id serial NOT NULL, + user_id int4 NOT NULL, + client_uuid bpchar(36) DEFAULT '' NOT NULL, + created timestamptz DEFAULT NOW() NOT NULL, + description varchar DEFAULT '' NOT NULL, + CONSTRAINT client_pk PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX client_uuid_idx ON public.client (client_uuid); diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index c640cc6..e1253a4 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -70,7 +70,6 @@ export class NodeStore { this.sendQueue = new SimpleNodeStore(this.db, 'send_queue') this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history') this.initializeRootNode() - .then(() => this.initializeClientUUID()) .then(() => resolve()) } @@ -110,13 +109,6 @@ export class NodeStore { getRequest.onerror = (event) => reject(event.target.error) }) }//}}} - async initializeClientUUID() {//{{{ - let clientUUID = await this.getAppState('client_uuid') - if (clientUUID !== null) - return - clientUUID = crypto.randomUUID() - return this.setAppState('client_uuid', clientUUID) - }//}}} node(uuid, dataIfUndefined, newLevel) {//{{{ let n = this.nodes[uuid] diff --git a/static/js/sync.mjs b/static/js/sync.mjs index 95d554d..6321db6 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -13,7 +13,6 @@ export class Sync { // The latest sync node value is used to retrieve the changes // from the backend. const state = await nodeStore.getAppState('latest_sync_node') - const clientUUID = await nodeStore.getAppState('client_uuid') const oldMax = (state?.value ? state.value : 0) let currMax = oldMax @@ -22,7 +21,7 @@ export class Sync { let batch = 0 do { batch++ - res = await API.query('POST', `/sync/from_server/${oldMax}/${offset}`, { ClientUUID: clientUUID.value }) + res = await API.query('POST', `/sync/from_server/${oldMax}/${offset}`) if (res.Nodes.length > 0) console.log(`Node sync batch #${batch}`) offset += res.Nodes.length @@ -96,10 +95,8 @@ export class Sync { break console.debug(`Sending ${nodesToSend.length} node(s) to server`) - const clientUUID = await nodeStore.getAppState('client_uuid') const request = { NodeData: JSON.stringify(nodesToSend), - ClientUUID: clientUUID.value, } const res = await API.query('POST', '/sync/to_server', request) if (!res.OK) { diff --git a/user.go b/user.go index fcd1cb9..b1c2abf 100644 --- a/user.go +++ b/user.go @@ -5,20 +5,23 @@ import ( "github.com/golang-jwt/jwt/v5" ) -type User struct { - ID int - Username string - Password string - Name string +type UserSession struct { + UserID int + Username string + Password string + Name string + ClientUUID string } -func NewUser(claims jwt.MapClaims) (u User) { +func NewUser(claims jwt.MapClaims) (u UserSession) { uid, _ := claims["uid"].(float64) name, _ := claims["name"].(string) username, _ := claims["login"].(string) + clientUUID, _ := claims["cid"].(string) - u.ID = int(uid) + u.UserID = int(uid) u.Username = username u.Name = name + u.ClientUUID = clientUUID return }