Skip to content

Commit ff25ba4

Browse files
committed
Update Satori client to comply to latest API.
Include Nakama session token id as SessionID in Satori generated auth tokens.
1 parent 6b3a71a commit ff25ba4

File tree

9 files changed

+507
-32
lines changed

9 files changed

+507
-32
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,5 @@ require (
7373
golang.org/x/text v0.16.0 // indirect
7474
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
7575
)
76+
77+
replace github.com/heroiclabs/nakama-common => ../nakama-common

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
128128
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
129129
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
130130
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
131-
github.com/heroiclabs/nakama-common v1.33.0 h1:ojGPgVZ/goyAH6jrm3Emg7034yGDdqB/f5vTa6AJ5n0=
132-
github.com/heroiclabs/nakama-common v1.33.0/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
133131
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a h1:tuL2ZPaeCbNw8rXmV9ywd00nXRv95V4/FmbIGKLQJAE=
134132
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a/go.mod h1:hzCTPoEi/oml2BllVydJcNP63S7b56e5DzeQeLGvw1U=
135133
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=

internal/satori/satori.go

+174-12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net"
2525
"net/http"
2626
"net/url"
27+
"strconv"
2728
"strings"
2829
"time"
2930

@@ -34,6 +35,8 @@ import (
3435

3536
var _ runtime.Satori = &SatoriClient{}
3637

38+
type CtxTokenIDKey struct{}
39+
3740
type SatoriClient struct {
3841
logger *zap.Logger
3942
httpc *http.Client
@@ -117,10 +120,11 @@ func (stc *sessionTokenClaims) Valid() error {
117120
return nil
118121
}
119122

120-
func (s *SatoriClient) generateToken(id string) (string, error) {
123+
func (s *SatoriClient) generateToken(ctx context.Context, id string) (string, error) {
124+
tid := ctx.Value(CtxTokenIDKey{}).(string)
121125
timestamp := time.Now().UTC()
122126
claims := sessionTokenClaims{
123-
SessionID: "",
127+
SessionID: tid,
124128
IdentityId: id,
125129
ExpiresAt: timestamp.Add(time.Duration(s.tokenExpirySec) * time.Second).Unix(),
126130
IssuedAt: timestamp.Unix(),
@@ -135,23 +139,27 @@ func (s *SatoriClient) generateToken(id string) (string, error) {
135139
}
136140

137141
type authenticateBody struct {
138-
Id string `json:"id"`
142+
Id string `json:"id"`
143+
Default map[string]string `json:"default,omitempty"`
144+
Custom map[string]string `json:"custom,omitempty"`
139145
}
140146

141147
// @group satori
142148
// @summary Create a new identity.
143149
// @param ctx(type=context.Context) The context object represents information about the server and requester.
144150
// @param id(type=string) The identifier of the identity.
145-
// @param ipAddress(type=string, optional=true) An optional client IP address to pass on to Satori for geo-IP lookup.
151+
// @param default(type=map[string]string, optional=true, default=nil) Default properties to update with this call. Set to nil to leave them as they are on the server.
152+
// @param custom(type=map[string]string, optional=true, default=nil) Custom properties to update with this call. Set to nil to leave them as they are on the server.
153+
// @param ipAddress(type=string, optional=true, default="") An optional client IP address to pass on to Satori for geo-IP lookup.
146154
// @return error(error) An optional error value if an error occurred.
147-
func (s *SatoriClient) Authenticate(ctx context.Context, id string, ipAddress ...string) error {
155+
func (s *SatoriClient) Authenticate(ctx context.Context, id string, defaultProperties, customProperties map[string]string, ipAddress ...string) error {
148156
if s.invalidConfig {
149157
return runtime.ErrSatoriConfigurationInvalid
150158
}
151159

152160
url := s.url.String() + "/v1/authenticate"
153161

154-
body := &authenticateBody{Id: id}
162+
body := &authenticateBody{Id: id, Default: defaultProperties, Custom: customProperties}
155163

156164
json, err := json.Marshal(body)
157165
if err != nil {
@@ -200,7 +208,7 @@ func (s *SatoriClient) PropertiesGet(ctx context.Context, id string) (*runtime.P
200208

201209
url := s.url.String() + "/v1/properties"
202210

203-
sessionToken, err := s.generateToken(id)
211+
sessionToken, err := s.generateToken(ctx, id)
204212
if err != nil {
205213
return nil, err
206214
}
@@ -249,7 +257,7 @@ func (s *SatoriClient) PropertiesUpdate(ctx context.Context, id string, properti
249257

250258
url := s.url.String() + "/v1/properties"
251259

252-
sessionToken, err := s.generateToken(id)
260+
sessionToken, err := s.generateToken(ctx, id)
253261
if err != nil {
254262
return err
255263
}
@@ -315,7 +323,7 @@ func (s *SatoriClient) EventsPublish(ctx context.Context, id string, events []*r
315323
evts[i].setTimestamp()
316324
}
317325

318-
sessionToken, err := s.generateToken(id)
326+
sessionToken, err := s.generateToken(ctx, id)
319327
if err != nil {
320328
return err
321329
}
@@ -361,7 +369,7 @@ func (s *SatoriClient) ExperimentsList(ctx context.Context, id string, names ...
361369

362370
url := s.url.String() + "/v1/experiment"
363371

364-
sessionToken, err := s.generateToken(id)
372+
sessionToken, err := s.generateToken(ctx, id)
365373
if err != nil {
366374
return nil, err
367375
}
@@ -419,7 +427,7 @@ func (s *SatoriClient) FlagsList(ctx context.Context, id string, names ...string
419427

420428
url := s.url.String() + "/v1/flag"
421429

422-
sessionToken, err := s.generateToken(id)
430+
sessionToken, err := s.generateToken(ctx, id)
423431
if err != nil {
424432
return nil, err
425433
}
@@ -477,7 +485,7 @@ func (s *SatoriClient) LiveEventsList(ctx context.Context, id string, names ...s
477485

478486
url := s.url.String() + "/v1/live-event"
479487

480-
sessionToken, err := s.generateToken(id)
488+
sessionToken, err := s.generateToken(ctx, id)
481489
if err != nil {
482490
return nil, err
483491
}
@@ -519,3 +527,157 @@ func (s *SatoriClient) LiveEventsList(ctx context.Context, id string, names ...s
519527
return nil, fmt.Errorf("%d status code", res.StatusCode)
520528
}
521529
}
530+
531+
// @group satori
532+
// @summary List messages.
533+
// @param ctx(type=context.Context) The context object represents information about the server and requester.
534+
// @param id(type=string) The identifier of the identity.
535+
// @param limit(type=int) The max number of messages to return.
536+
// @param forward(type=bool) True if listing should be older messages to newer, false if reverse.
537+
// @param cursor(type=string) A pagination cursor, if any.
538+
// @return messages(*runtime.SatoriMessageList) The messages list.
539+
// @return error(error) An optional error value if an error occurred.
540+
func (s *SatoriClient) MessagesList(ctx context.Context, id string, limit int, forward bool, cursor string) (*runtime.SatoriMessageList, error) {
541+
if s.invalidConfig {
542+
return nil, runtime.ErrSatoriConfigurationInvalid
543+
}
544+
545+
if limit < 1 {
546+
return nil, errors.New("limit must be greater than zero")
547+
}
548+
549+
url := s.url.String() + "/v1/message"
550+
551+
sessionToken, err := s.generateToken(ctx, id)
552+
if err != nil {
553+
return nil, err
554+
}
555+
556+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
557+
if err != nil {
558+
return nil, err
559+
}
560+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sessionToken))
561+
q := req.URL.Query()
562+
q.Set("limit", strconv.Itoa(limit))
563+
q.Set("forward", strconv.FormatBool(forward))
564+
if cursor != "" {
565+
q.Set("cursor", cursor)
566+
}
567+
req.URL.RawQuery = q.Encode()
568+
569+
res, err := s.httpc.Do(req)
570+
if err != nil {
571+
return nil, err
572+
}
573+
574+
defer res.Body.Close()
575+
576+
switch res.StatusCode {
577+
case 200:
578+
resBody, err := io.ReadAll(res.Body)
579+
if err != nil {
580+
return nil, err
581+
}
582+
var messages runtime.SatoriMessageList
583+
if err = json.Unmarshal(resBody, &messages); err != nil {
584+
return nil, err
585+
}
586+
587+
return &messages, nil
588+
default:
589+
return nil, fmt.Errorf("%d status code", res.StatusCode)
590+
}
591+
}
592+
593+
// @group satori
594+
// @summary Update message.
595+
// @param ctx(type=context.Context) The context object represents information about the server and requester.
596+
// @param id(type=string) The identifier of the identity.
597+
// @param readTime(type=int64) The time the message was read at the client.
598+
// @param consumeTime(type=int64) The time the message was consumed by the identity.
599+
// @return error(error) An optional error value if an error occurred.
600+
func (s *SatoriClient) MessageUpdate(ctx context.Context, id, messageId string, readTime, consumeTime int64) error {
601+
if s.invalidConfig {
602+
return runtime.ErrSatoriConfigurationInvalid
603+
}
604+
605+
url := s.url.String() + fmt.Sprintf("/v1/message/%s", messageId)
606+
607+
sessionToken, err := s.generateToken(ctx, id)
608+
if err != nil {
609+
return err
610+
}
611+
612+
json, err := json.Marshal(&runtime.SatoriMessageUpdate{
613+
ReadTime: readTime,
614+
ConsumeTime: consumeTime,
615+
})
616+
if err != nil {
617+
return err
618+
}
619+
620+
req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(json))
621+
if err != nil {
622+
return err
623+
}
624+
req.Header.Set("Content-Type", "application/json")
625+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sessionToken))
626+
627+
res, err := s.httpc.Do(req)
628+
if err != nil {
629+
return err
630+
}
631+
632+
defer res.Body.Close()
633+
634+
switch res.StatusCode {
635+
case 200:
636+
return nil
637+
default:
638+
return fmt.Errorf("%d status code", res.StatusCode)
639+
}
640+
}
641+
642+
// @group satori
643+
// @summary Delete message.
644+
// @param ctx(type=context.Context) The context object represents information about the server and requester.
645+
// @param id(type=string) The identifier of the identity.
646+
// @param messageId(type=string) The identifier of the message.
647+
// @return error(error) An optional error value if an error occurred.
648+
func (s *SatoriClient) MessageDelete(ctx context.Context, id, messageId string) error {
649+
if s.invalidConfig {
650+
return runtime.ErrSatoriConfigurationInvalid
651+
}
652+
653+
if messageId == "" {
654+
return errors.New("message id cannot be an empty string")
655+
}
656+
657+
url := s.url.String() + fmt.Sprintf("/v1/message/%s", messageId)
658+
659+
sessionToken, err := s.generateToken(ctx, id)
660+
if err != nil {
661+
return err
662+
}
663+
664+
req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil)
665+
if err != nil {
666+
return err
667+
}
668+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sessionToken))
669+
670+
res, err := s.httpc.Do(req)
671+
if err != nil {
672+
return err
673+
}
674+
675+
defer res.Body.Close()
676+
677+
switch res.StatusCode {
678+
case 200:
679+
return nil
680+
default:
681+
return fmt.Errorf("%d status code", res.StatusCode)
682+
}
683+
}

server/api.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"database/sql"
2525
"encoding/base64"
2626
"fmt"
27+
"github.com/heroiclabs/nakama/v3/internal/satori"
2728
"google.golang.org/grpc/grpclog"
2829
"math"
2930
"net"
@@ -63,6 +64,7 @@ type ctxUserIDKey struct{}
6364
type ctxUsernameKey struct{}
6465
type ctxVarsKey struct{}
6566
type ctxExpiryKey struct{}
67+
type ctxTokenIDKey = satori.CtxTokenIDKey
6668

6769
type ctxFullMethodKey struct{}
6870

@@ -425,7 +427,7 @@ func securityInterceptorFunc(logger *zap.Logger, config Config, sessionCache Ses
425427
if !sessionCache.IsValidSession(userID, exp, tokenId) {
426428
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
427429
}
428-
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp)
430+
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId)
429431
default:
430432
// Unless explicitly defined above, handlers require full user authentication.
431433
md, ok := metadata.FromIncomingContext(ctx)
@@ -453,7 +455,7 @@ func securityInterceptorFunc(logger *zap.Logger, config Config, sessionCache Ses
453455
if !sessionCache.IsValidSession(userID, exp, tokenId) {
454456
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
455457
}
456-
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp)
458+
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId)
457459
}
458460
return context.WithValue(ctx, ctxFullMethodKey{}, info.FullMethod), nil
459461
}

server/runtime_go_nakama.go

+3
Original file line numberDiff line numberDiff line change
@@ -4282,6 +4282,9 @@ func (n *RuntimeGoNakamaModule) GetSatori() runtime.Satori {
42824282
return n.satori
42834283
}
42844284

4285+
// @group fleetmanager
4286+
// @symmary Get the Fleet Manager client.
4287+
// @return fleetManager(runtime.FleetManager) The Fleet Manager client.
42854288
func (n *RuntimeGoNakamaModule) GetFleetManager() runtime.FleetManager {
42864289
return n.fleetManager
42874290
}

0 commit comments

Comments
 (0)