Client UUID added to JWT
This commit is contained in:
parent
dfd6260a7a
commit
dc010df448
6 changed files with 65 additions and 38 deletions
|
@ -2,8 +2,9 @@ package authentication
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// External
|
// External
|
||||||
_ "git.gibonuddevalla.se/go/wrappederror"
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
@ -146,6 +147,14 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques
|
||||||
data["uid"] = user.ID
|
data["uid"] = user.ID
|
||||||
data["login"] = user.Username
|
data["login"] = user.Username
|
||||||
data["name"] = user.Name
|
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)
|
token, err = mngr.GenerateToken(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mngr.log.Error("authentication", "error", err)
|
mngr.log.Error("authentication", "error", err)
|
||||||
|
@ -269,3 +278,31 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin
|
||||||
changed = (rowsAffected == 1)
|
changed = (rowsAffected == 1)
|
||||||
return
|
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
24
main.go
|
@ -167,7 +167,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon
|
||||||
user := NewUser(claims)
|
user := NewUser(claims)
|
||||||
r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user))
|
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)
|
fn(w, r)
|
||||||
}
|
}
|
||||||
} // }}}
|
} // }}}
|
||||||
|
@ -246,22 +246,11 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
// The purpose of the Client UUID is to avoid
|
// The purpose of the Client UUID is to avoid
|
||||||
// sending nodes back once again to a client that
|
// sending nodes back once again to a client that
|
||||||
// just created or modified it.
|
// 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)
|
user := getUser(r)
|
||||||
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
||||||
offset, _ := strconv.Atoi(r.PathValue("offset"))
|
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 {
|
if err != nil {
|
||||||
Log.Error("/node/tree", "error", err)
|
Log.Error("/node/tree", "error", err)
|
||||||
httpError(w, err)
|
httpError(w, err)
|
||||||
|
@ -285,7 +274,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
uuid := r.PathValue("uuid")
|
||||||
node, err := RetrieveNode(user.ID, uuid)
|
node, err := RetrieveNode(user.UserID, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responseError(w, err)
|
responseError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -301,7 +290,6 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
body, _ := io.ReadAll(r.Body)
|
||||||
var request struct {
|
var request struct {
|
||||||
ClientUUID string
|
|
||||||
NodeData string
|
NodeData string
|
||||||
}
|
}
|
||||||
err := json.Unmarshal(body, &request)
|
err := json.Unmarshal(body, &request)
|
||||||
|
@ -310,7 +298,7 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
return
|
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{}{
|
responseData(w, map[string]interface{}{
|
||||||
"OK": true,
|
"OK": true,
|
||||||
|
@ -359,7 +347,7 @@ func changePassword(username string) { // {{{
|
||||||
|
|
||||||
fmt.Printf("\nPassword changed\n")
|
fmt.Printf("\nPassword changed\n")
|
||||||
} // }}}
|
} // }}}
|
||||||
func getUser(r *http.Request) User { // {{{
|
func getUser(r *http.Request) UserSession { // {{{
|
||||||
user, _ := r.Context().Value(CONTEXT_USER).(User)
|
user, _ := r.Context().Value(CONTEXT_USER).(UserSession)
|
||||||
return user
|
return user
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
10
sql/00010.sql
Normal file
10
sql/00010.sql
Normal 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);
|
|
@ -70,7 +70,6 @@ export class NodeStore {
|
||||||
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
||||||
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
|
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
|
||||||
this.initializeRootNode()
|
this.initializeRootNode()
|
||||||
.then(() => this.initializeClientUUID())
|
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,13 +109,6 @@ export class NodeStore {
|
||||||
getRequest.onerror = (event) => reject(event.target.error)
|
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) {//{{{
|
node(uuid, dataIfUndefined, newLevel) {//{{{
|
||||||
let n = this.nodes[uuid]
|
let n = this.nodes[uuid]
|
||||||
|
|
|
@ -13,7 +13,6 @@ export class Sync {
|
||||||
// The latest sync node value is used to retrieve the changes
|
// The latest sync node value is used to retrieve the changes
|
||||||
// from the backend.
|
// from the backend.
|
||||||
const state = await nodeStore.getAppState('latest_sync_node')
|
const state = await nodeStore.getAppState('latest_sync_node')
|
||||||
const clientUUID = await nodeStore.getAppState('client_uuid')
|
|
||||||
const oldMax = (state?.value ? state.value : 0)
|
const oldMax = (state?.value ? state.value : 0)
|
||||||
let currMax = oldMax
|
let currMax = oldMax
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ export class Sync {
|
||||||
let batch = 0
|
let batch = 0
|
||||||
do {
|
do {
|
||||||
batch++
|
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)
|
if (res.Nodes.length > 0)
|
||||||
console.log(`Node sync batch #${batch}`)
|
console.log(`Node sync batch #${batch}`)
|
||||||
offset += res.Nodes.length
|
offset += res.Nodes.length
|
||||||
|
@ -96,10 +95,8 @@ export class Sync {
|
||||||
break
|
break
|
||||||
console.debug(`Sending ${nodesToSend.length} node(s) to server`)
|
console.debug(`Sending ${nodesToSend.length} node(s) to server`)
|
||||||
|
|
||||||
const clientUUID = await nodeStore.getAppState('client_uuid')
|
|
||||||
const request = {
|
const request = {
|
||||||
NodeData: JSON.stringify(nodesToSend),
|
NodeData: JSON.stringify(nodesToSend),
|
||||||
ClientUUID: clientUUID.value,
|
|
||||||
}
|
}
|
||||||
const res = await API.query('POST', '/sync/to_server', request)
|
const res = await API.query('POST', '/sync/to_server', request)
|
||||||
if (!res.OK) {
|
if (!res.OK) {
|
||||||
|
|
17
user.go
17
user.go
|
@ -5,20 +5,23 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type UserSession struct {
|
||||||
ID int
|
UserID int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Name string
|
Name string
|
||||||
|
ClientUUID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(claims jwt.MapClaims) (u User) {
|
func NewUser(claims jwt.MapClaims) (u UserSession) {
|
||||||
uid, _ := claims["uid"].(float64)
|
uid, _ := claims["uid"].(float64)
|
||||||
name, _ := claims["name"].(string)
|
name, _ := claims["name"].(string)
|
||||||
username, _ := claims["login"].(string)
|
username, _ := claims["login"].(string)
|
||||||
|
clientUUID, _ := claims["cid"].(string)
|
||||||
|
|
||||||
u.ID = int(uid)
|
u.UserID = int(uid)
|
||||||
u.Username = username
|
u.Username = username
|
||||||
u.Name = name
|
u.Name = name
|
||||||
|
u.ClientUUID = clientUUID
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue