Bootstrap copied BFF and local accounts with public Sendico contracts (#34)

This commit is contained in:
Stephan D committed 2026-09-12 13:37:48 +02:00
1 parent 67dc5b5b1e
commit 7062672a87
556 files changed
+42291 -777

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
package mutil
import (
"os"
"time"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"go.uber.org/zap"
)
func GetConfigValue(logger mlogger.Logger, varName, envVarName string, value, envValue *string) string {
if value != nil && envValue != nil {
logger.Warn("Both variable and environment variable are set, using environment variable value",
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.String("value", *value), zap.String("env_value", os.Getenv(*envValue)))
}
if envValue != nil {
return os.Getenv(*envValue)
}
if value != nil {
return *value
}
return ""
}
func GetConfigIntValue(logger mlogger.Logger, varName, envVarName string, value *int, envValue *string) int {
if value != nil && envValue != nil {
logger.Warn("Both variable and environment variable are set, using environment variable value",
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Int("value", *value), zap.String("env_value", os.Getenv(*envValue)))
}
if envValue != nil {
envStr := os.Getenv(*envValue)
if envStr != "" {
if parsed, err := time.ParseDuration(envStr + "s"); err == nil {
return int(parsed.Seconds())
}
logger.Warn("Invalid environment variable value for timeout", zap.String("environment_variable", envVarName), zap.String("value", envStr))
}
}
if value != nil {
return *value
}
return 30 // Default timeout in seconds
}
func GetConfigBoolValue(logger mlogger.Logger, varName, envVarName string, value *bool, envValue *string) bool {
if value != nil && envValue != nil {
logger.Warn("Both variable and environment variable are set, using environment variable value",
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Bool("value", *value), zap.String("env_value", os.Getenv(*envValue)))
}
if envValue != nil {
envStr := os.Getenv(*envValue)
switch envStr {
case "true", "1":
return true
case "false", "0":
return false
default:
logger.Warn("Invalid environment variable value for boolean", zap.String("environment_variable", envVarName), zap.String("value", envStr))
}
}
if value != nil {
return *value
}
return false // Default for boolean
}
+37
View File
@@ -0,0 +1,37 @@
package mutil
import (
"context"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func SetArchived[T storable.Storable](ctx context.Context, logger mlogger.Logger, newArchived bool, objectRef bson.ObjectID, repo repository.Repository) error {
objs, err := GetObjects[T](ctx, logger, repository.IDFilter(objectRef), nil, repo)
if err != nil {
logger.Warn("Failed to fetch object", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
return err
}
if len(objs) == 0 {
logger.Warn("No objects found to archive", mzap.ObjRef("object_ref", objectRef))
return nil
}
// Archive the first object found
obj := objs[0]
if archivable, ok := any(obj).(interface{ SetArchived(bool) }); ok {
archivable.SetArchived(newArchived)
if err := repo.Update(ctx, obj); err != nil {
logger.Warn("Failed to update object archived status", zap.Error(err), mzap.ObjRef("object_ref", objectRef))
return err
}
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package mutil
import (
"context"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
func GetObjects[T any](ctx context.Context, logger mlogger.Logger, filter builder.Query, cursor *model.ViewCursor, repo repository.Repository) ([]T, error) {
entities := make([]T, 0)
decoder := func(cur *mongo.Cursor) error {
var next T
if e := cur.Decode(&next); e != nil {
logger.Warn("Failed to decode entity", zap.Error(e))
return e
}
entities = append(entities, next)
return nil
}
return entities, repo.FindManyByFilter(ctx, repository.ApplyCursor(filter, cursor), decoder)
}
+89
View File
@@ -0,0 +1,89 @@
// Package mutil provides utility functions for working with account-bound objects
// with permission enforcement.
//
// Example usage:
//
// // Using the low-level repository approach
// objects, err := mutil.GetAccountBoundObjects[model.ProjectFilter](
// ctx, logger, accountRef, orgRef, model.ActionRead,
// repository.Query(), &model.ViewCursor{Limit: &limit, Offset: &offset, IsArchived: &isArchived},
// enforcer, repo)
//
// // Using the AccountBoundDB interface approach
// objects, err := mutil.GetAccountBoundObjectsFromDB[model.ProjectFilter](
// ctx, logger, accountRef, orgRef,
// repository.Query(), &model.ViewCursor{Limit: &limit, Offset: &offset, IsArchived: &isArchived},
// accountBoundDB)
package mutil
import (
"context"
"errors"
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
// GetAccountBoundObjects retrieves account-bound objects with permission enforcement.
// This function handles the complex logic of:
// 1. Finding objects where accountRef matches OR is null/absent
// 2. Checking organization-level permissions for each object
// 3. Filtering to only objects the account has permission to read
func GetAccountBoundObjects[T any](
ctx context.Context,
logger mlogger.Logger,
accountRef, organizationRef bson.ObjectID,
filter builder.Query,
cursor *model.ViewCursor,
enforcer auth.Enforcer,
repo repository.Repository,
) ([]T, error) {
// Build query to find objects where accountRef matches OR is null/absent
accountQuery := repository.WithOrg(accountRef, organizationRef)
// Get all account-bound objects that match the criteria
allObjects, err := repo.ListAccountBound(ctx, repository.ApplyCursor(accountQuery, cursor))
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch account bound objects", zap.Error(err),
mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef),
)
} else {
logger.Debug("No matching account bound objects found", zap.Error(err),
mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef),
)
}
return nil, err
}
if len(allObjects) == 0 {
return nil, merrors.NoData("no_account_bound_objects_found")
}
allowed := make([]bson.ObjectID, 0, len(allObjects))
for _, ref := range allObjects {
allowed = append(allowed, *ref.GetID())
}
if len(allowed) == 0 {
return nil, merrors.NoData("no_data_found_or_allowed")
}
logger.Debug("Successfully retrieved account bound objects",
zap.Int("total_count", len(allObjects)),
mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef),
zap.Any("objs", allObjects),
)
return mutil.GetObjects[T](ctx, logger, repository.Query().In(repository.IDField(), allowed), cursor, repo)
}
+58
View File
@@ -0,0 +1,58 @@
package mutil
import (
"context"
"errors"
"git.sendico.io/tech/sxchnge/bff/pkg/auth"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder"
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
mutil "git.sendico.io/tech/sxchnge/bff/pkg/mutil/db"
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func GetProtectedObjects[T any](
ctx context.Context,
logger mlogger.Logger,
accountRef, organizationRef bson.ObjectID,
action model.Action,
filter builder.Query,
cursor *model.ViewCursor,
enforcer auth.Enforcer,
repo repository.Repository,
) ([]T, error) {
refs, err := repo.ListPermissionBound(ctx, repository.ApplyCursor(filter, cursor))
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch object IDs", zap.Error(err), mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef), zap.String("action", string(action)))
} else {
logger.Debug("No matching IDs found", zap.Error(err), mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef), zap.String("action", string(action)))
}
return nil, err
}
res, err := enforcer.EnforceBatch(ctx, refs, accountRef, action)
if err != nil {
logger.Warn("Failed to enforce object IDs", zap.Error(err), mzap.AccRef(accountRef),
mzap.OrgRef(organizationRef), zap.String("action", string(action)))
return nil, err
}
allowed := make([]bson.ObjectID, 0, len(res))
for _, ref := range refs {
if ok := res[*ref.GetID()]; ok {
allowed = append(allowed, *ref.GetID())
}
}
if len(allowed) == 0 {
return nil, merrors.NoData("no_data_found_or_allowed")
}
return mutil.GetObjects[T](ctx, logger, repository.Query().In(repository.IDField(), allowed), cursor, repo)
}
+20
View File
@@ -0,0 +1,20 @@
package mutil
import (
"context"
"git.sendico.io/tech/sxchnge/bff/pkg/db/repository"
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func GetObjectByID(ctx context.Context, logger mlogger.Logger, id string, val storable.Storable, repo repository.Repository) error {
p, err := bson.ObjectIDFromHex(id)
if err != nil {
logger.Warn("Failed to decode object reference", zap.String("reference", id), zap.String("collection", val.Collection()))
return err
}
return repo.Get(ctx, p, val)
}
+7
View File
@@ -0,0 +1,7 @@
package mduration
import "time"
func Param2Duration(param int, d time.Duration) time.Duration {
return time.Duration(param) * d
}
+29
View File
@@ -0,0 +1,29 @@
package mask
import "strings"
func Email(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return email
}
local := parts[0]
if len(local) > 2 {
local = local[:1] + "***" + local[len(local)-1:]
} else {
local = local[:1] + "***"
}
return local + "@" + parts[1]
}
func Emails(emails []string) []string {
if emails == nil {
return nil
}
masked := make([]string, len(emails))
for i, email := range emails {
masked[i] = Email(email)
}
return masked
}
+21
View File
@@ -0,0 +1,21 @@
package mask
import (
"reflect"
"testing"
)
func TestEmailsMasksEachEmail(t *testing.T) {
got := Emails([]string{"alice@example.com", "bo@example.org", "invalid"})
want := []string{"a***e@example.com", "b***@example.org", "invalid"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Emails() = %#v, want %#v", got, want)
}
}
func TestEmailsPreservesNil(t *testing.T) {
if got := Emails(nil); got != nil {
t.Fatalf("Emails(nil) = %#v, want nil", got)
}
}
+28
View File
@@ -0,0 +1,28 @@
package mzap
import (
"git.sendico.io/tech/sxchnge/bff/pkg/model"
"git.sendico.io/tech/sxchnge/bff/pkg/mutil/mask"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func AccRef(accountRef bson.ObjectID) zap.Field {
return ObjRef("account_ref", accountRef)
}
func Email(email string) zap.Field {
return MaskEmail("email", email)
}
func MaskEmail(field, email string) zap.Field {
return zap.String(field, mask.Email(email))
}
func MaskEmails(field string, emails []string) zap.Field {
return zap.Strings(field, mask.Emails(emails))
}
func Login(account *model.Account) zap.Field {
return Email(account.Login)
}
+14
View File
@@ -0,0 +1,14 @@
package mzap
import (
"reflect"
"go.uber.org/zap"
)
func ErrorType(err error) zap.Field {
if err == nil {
return zap.String("error_type", "")
}
return zap.String("error_type", reflect.TypeOf(err).String())
}
+28
View File
@@ -0,0 +1,28 @@
package mzap
import (
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
"git.sendico.io/tech/sxchnge/bff/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func ObjRef(name string, objRef bson.ObjectID) zap.Field {
return zap.String(name, objRef.Hex())
}
func StorableRef(obj storable.Storable) zap.Field {
return ObjRef(obj.Collection()+"_ref", *obj.GetID())
}
func OrgRef(orgRef bson.ObjectID) zap.Field {
return ObjRef("organization_ref", orgRef)
}
func OrganizationRef(obj model.OrganizationBound) zap.Field {
return OrgRef(obj.GetOrganizationRef())
}
func PermissionRef(permissionRef bson.ObjectID) zap.Field {
return ObjRef("permission_ref", permissionRef)
}