Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,189 @@
|
||||
package refreshtokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging/fields"
|
||||
"git.sendico.io/tech/sxchnge/shared/service"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (db *RefreshTokenDB) Create(ctx context.Context, rt *sessionmodel.RefreshToken) error {
|
||||
if rt.LastUsedAt.IsZero() {
|
||||
rt.LastUsedAt = time.Now()
|
||||
}
|
||||
|
||||
if rt.AccountRef == nil {
|
||||
return merrors.InvalidArgument("Account reference must have a valid value", "refreshToken.accountRef")
|
||||
}
|
||||
|
||||
matched, err := db.replaceActive(ctx, rt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matched {
|
||||
return nil
|
||||
}
|
||||
|
||||
db.Logger.Debug("Registering refresh token", zap.String("client_id", rt.ClientID), zap.String("device_id", rt.DeviceID))
|
||||
if err := db.DBImp.Create(ctx, rt); err != nil {
|
||||
if !errors.Is(err, merrors.ErrDataConflict) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Another login can establish the same active slot after the patch and
|
||||
// before this insert. Converge on that row instead of returning the
|
||||
// unique-index conflict to the caller.
|
||||
db.Logger.Debug("Concurrent refresh session registration detected",
|
||||
zap.String("client_id", rt.ClientID), zap.String("device_id", rt.DeviceID))
|
||||
matched, reconcileErr := db.replaceActive(ctx, rt)
|
||||
if reconcileErr != nil {
|
||||
return reconcileErr
|
||||
}
|
||||
if !matched {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) replaceActive(ctx context.Context, rt *sessionmodel.RefreshToken) (bool, error) {
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field(TokenField), rt.RefreshToken).
|
||||
Set(repository.Field(ExpiresAtField), rt.ExpiresAt).
|
||||
Set(repository.Field(UserAgentField), rt.UserAgent).
|
||||
Set(repository.Field(IPAddressField), rt.IPAddress).
|
||||
Set(repository.Field(LastUsedAtField), rt.LastUsedAt).
|
||||
Set(repository.Field(IsRevokedField), rt.IsRevoked).
|
||||
Set(repository.Field(SessionVersionField), rt.SessionVersion)
|
||||
|
||||
var stored sessionmodel.RefreshToken
|
||||
matched, err := db.Repository.PatchOneByFilter(
|
||||
ctx,
|
||||
filterSlotByAccount(*rt.AccountRef, &rt.SessionIdentifier),
|
||||
patch,
|
||||
&stored,
|
||||
)
|
||||
if err != nil {
|
||||
db.Logger.Warn("Failed to replace active refresh session", zap.Error(err),
|
||||
zap.String("client_id", rt.ClientID), zap.String("device_id", rt.DeviceID))
|
||||
return false, err
|
||||
}
|
||||
if !matched {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
db.Logger.Debug("Replaced active refresh session",
|
||||
zap.String("client_id", rt.ClientID), zap.String("device_id", rt.DeviceID))
|
||||
rt.SetID(*stored.GetID())
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) Update(ctx context.Context, rt *sessionmodel.RefreshToken) error {
|
||||
rt.LastUsedAt = time.Now()
|
||||
|
||||
// Use Patch instead of Update to avoid race conditions
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field(TokenField), rt.RefreshToken).
|
||||
Set(repository.Field(ExpiresAtField), rt.ExpiresAt).
|
||||
Set(repository.Field(UserAgentField), rt.UserAgent).
|
||||
Set(repository.Field(IPAddressField), rt.IPAddress).
|
||||
Set(repository.Field(LastUsedAtField), rt.LastUsedAt).
|
||||
Set(repository.Field(IsRevokedField), rt.IsRevoked).
|
||||
Set(repository.Field(SessionVersionField), rt.SessionVersion)
|
||||
|
||||
return db.Patch(ctx, *rt.GetID(), patch)
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) Delete(ctx context.Context, tokenRef bson.ObjectID) error {
|
||||
db.Logger.Debug("Deleting refresh token", mzap.ObjRef("refresh_token_ref", tokenRef))
|
||||
return db.DBImp.Delete(ctx, tokenRef)
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) Revoke(ctx context.Context, accountRef bson.ObjectID, session *sessionmodel.SessionIdentifier) error {
|
||||
var rt sessionmodel.RefreshToken
|
||||
f := filterByAccount(accountRef, session)
|
||||
if err := db.Repository.FindOneByFilter(ctx, f, &rt); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
db.Logger.Debug("Refresh token not found during revoke",
|
||||
mzap.AccRef(accountRef), zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Use Patch to update the revocation status
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field(IsRevokedField), true).
|
||||
Set(repository.Field(LastUsedAtField), time.Now())
|
||||
|
||||
return db.Patch(ctx, *rt.GetID(), patch)
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) RevokeToken(ctx context.Context, token *sessionmodel.ClientRefreshToken) (bool, error) {
|
||||
query := filter(&token.SessionIdentifier).
|
||||
And(repository.Query().Comparison(repository.Field(TokenField), builder.Eq, token.RefreshToken))
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field(IsRevokedField), true).
|
||||
Set(repository.Field(LastUsedAtField), time.Now())
|
||||
modified, err := db.Repository.PatchMany(ctx, query, patch)
|
||||
return modified > 0, err
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) GetSession(ctx context.Context, accountRef bson.ObjectID, session *sessionmodel.SessionIdentifier) (*sessionmodel.RefreshToken, error) {
|
||||
var rt sessionmodel.RefreshToken
|
||||
if err := db.Repository.FindOneByFilter(ctx, filterByAccount(accountRef, session), &rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !rt.ExpiresAt.After(time.Now()) {
|
||||
return nil, merrors.ErrNoData
|
||||
}
|
||||
return &rt, nil
|
||||
}
|
||||
|
||||
func (db *RefreshTokenDB) GetByCRT(ctx context.Context, t *sessionmodel.ClientRefreshToken) (*sessionmodel.RefreshToken, error) {
|
||||
var rt sessionmodel.RefreshToken
|
||||
f := filter(&t.SessionIdentifier).And(repository.Query().Filter(repository.Field("token"), t.RefreshToken))
|
||||
if err := db.Repository.FindOneByFilter(ctx, f, &rt); err != nil {
|
||||
if !errors.Is(err, merrors.ErrNoData) {
|
||||
db.Logger.Warn("Failed to fetch refresh token", zap.Error(err),
|
||||
zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if rt.ExpiresAt.Before(time.Now()) {
|
||||
db.Logger.Debug("Refresh token expired", mzap.StorableRef(&rt),
|
||||
zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID),
|
||||
zap.Time("expires_at", rt.ExpiresAt))
|
||||
return nil, merrors.AccessDenied(mservice.RefreshTokens, "read", *rt.GetID())
|
||||
}
|
||||
|
||||
// Check if token is revoked
|
||||
if rt.IsRevoked {
|
||||
db.Logger.Debug("Refresh token is revoked", mzap.StorableRef(&rt),
|
||||
zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID))
|
||||
return nil, merrors.ErrNoData
|
||||
}
|
||||
|
||||
if rt.GetID() != nil && !rt.GetID().IsZero() {
|
||||
lastUsedAt := time.Now()
|
||||
patch := repository.Patch().Set(repository.Field(LastUsedAtField), lastUsedAt)
|
||||
if err := db.Patch(ctx, *rt.GetID(), patch); err != nil {
|
||||
db.Logger.Warn("Failed to update refresh token last usage timestamp", zap.Error(err),
|
||||
zap.String("client_id", t.ClientID), zap.String("device_id", t.DeviceID))
|
||||
} else {
|
||||
rt.LastUsedAt = lastUsedAt
|
||||
}
|
||||
}
|
||||
|
||||
return &rt, nil
|
||||
}
|
||||
Reference in new issue
Block a user