Client UUID added to JWT

This commit is contained in:
Magnus Åhall 2025-01-12 17:35:29 +01:00
parent dfd6260a7a
commit dc010df448
6 changed files with 65 additions and 38 deletions

View file

@ -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
} // }}}

24
main.go
View file

@ -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
} // }}}

10
sql/00010.sql Normal file
View file

@ -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);

View file

@ -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]

View file

@ -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) {

17
user.go
View file

@ -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
}