service backend
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful

This commit is contained in:
Stephan D
2025-11-07 18:35:26 +01:00
parent 20e8f9acc4
commit 62a6631b9a
537 changed files with 48453 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func (db *InvitationDB) Accept(ctx context.Context, invitationRef primitive.ObjectID) error {
return db.updateStatus(ctx, invitationRef, model.InvitationAccepted)
}

View File

@@ -0,0 +1,49 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
// SetArchived sets the archived status of an invitation
// Invitation supports archiving through PermissionBound embedding ArchivableBase
func (db *InvitationDB) SetArchived(ctx context.Context, accountRef, organizationRef, invitationRef primitive.ObjectID, archived, cascade bool) error {
db.DBImp.Logger.Debug("Setting invitation archived status", mzap.ObjRef("invitation_ref", invitationRef), zap.Bool("archived", archived), zap.Bool("cascade", cascade))
res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, invitationRef, model.ActionUpdate)
if err != nil {
db.DBImp.Logger.Warn("Failed to enforce archivation permission", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
return err
}
if !res {
db.DBImp.Logger.Debug("Permission denied for archivation", mzap.ObjRef("invitation_ref", invitationRef))
return merrors.AccessDenied(db.Collection, string(model.ActionUpdate), invitationRef)
}
// Get the invitation first
var invitation model.Invitation
if err := db.Get(ctx, accountRef, invitationRef, &invitation); err != nil {
db.DBImp.Logger.Warn("Error retrieving invitation for archival", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
return err
}
// Update the invitation's archived status
invitation.SetArchived(archived)
if err := db.Update(ctx, accountRef, &invitation); err != nil {
db.DBImp.Logger.Warn("Error updating invitation archived status", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
return err
}
// Note: Currently no cascade dependencies for invitations
// If cascade is enabled, we could add logic here for any future dependencies
if cascade {
db.DBImp.Logger.Debug("Cascade archiving requested but no dependencies to archive for invitation", mzap.ObjRef("invitation_ref", invitationRef))
}
db.DBImp.Logger.Debug("Successfully set invitation archived status", mzap.ObjRef("invitation_ref", invitationRef), zap.Bool("archived", archived))
return nil
}

View File

@@ -0,0 +1,24 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
// DeleteCascade deletes an invitation
// Invitations don't have cascade dependencies, so this is a simple deletion
func (db *InvitationDB) DeleteCascade(ctx context.Context, accountRef, invitationRef primitive.ObjectID) error {
db.DBImp.Logger.Debug("Starting invitation cascade deletion", mzap.ObjRef("invitation_ref", invitationRef))
// Delete the invitation itself (no dependencies to cascade delete)
if err := db.Delete(ctx, accountRef, invitationRef); err != nil {
db.DBImp.Logger.Error("Error deleting invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
return err
}
db.DBImp.Logger.Debug("Successfully deleted invitation", mzap.ObjRef("invitation_ref", invitationRef))
return nil
}

View File

@@ -0,0 +1,53 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type InvitationDB struct {
auth.ProtectedDBImp[*model.Invitation]
}
func Create(
ctx context.Context,
logger mlogger.Logger,
enforcer auth.Enforcer,
pdb policy.DB,
db *mongo.Database,
) (*InvitationDB, error) {
p, err := auth.CreateDBImp[*model.Invitation](ctx, logger, pdb, enforcer, mservice.Invitations, db)
if err != nil {
return nil, err
}
// unique email per organization
if err := p.DBImp.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: repository.OrgField().Build(), Sort: ri.Asc}, {Field: "description.email", Sort: ri.Asc}},
Unique: true,
}); err != nil {
p.DBImp.Logger.Error("Failed to create unique mnemonic index", zap.Error(err))
return nil, err
}
// ttl index
ttl := int32(0) // zero ttl means expiration on date preset when inserting data
if err := p.DBImp.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
TTL: &ttl,
}); err != nil {
p.DBImp.Logger.Warn("Failed to create ttl index in the invitations", zap.Error(err))
return nil, err
}
return &InvitationDB{ProtectedDBImp: *p}, nil
}

View File

@@ -0,0 +1,12 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func (db *InvitationDB) Decline(ctx context.Context, invitationRef primitive.ObjectID) error {
return db.updateStatus(ctx, invitationRef, model.InvitationDeclined)
}

View File

@@ -0,0 +1,121 @@
package invitationdb
import (
"context"
"fmt"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
func (db *InvitationDB) GetPublic(ctx context.Context, invitationRef primitive.ObjectID) (*model.PublicInvitation, error) {
roleField := repository.Field("role")
orgField := repository.Field("organization")
accField := repository.Field("account")
empField := repository.Field("employee")
regField := repository.Field("registrationAcc")
descEmailField := repository.Field("description").Dot("email")
pipeline := repository.Pipeline().
// 0) Filter to exactly the invitation(s) you want
Match(repository.IDFilter(invitationRef).And(repository.Filter("status", model.InvitationCreated))).
// 1) Lookup the role document
Lookup(
mservice.Roles,
repository.Field("roleRef"),
repository.IDField(),
roleField,
).
Unwind(repository.Ref(roleField)).
// 2) Lookup the organization document
Lookup(
mservice.Organizations,
repository.Field("organizationRef"),
repository.IDField(),
orgField,
).
Unwind(repository.Ref(orgField)).
// 3) Lookup the account document
Lookup(
mservice.Accounts,
repository.Field("inviterRef"),
repository.IDField(),
accField,
).
Unwind(repository.Ref(accField)).
/* 4) do we already have an account whose login == invitation.description ? */
Lookup(
mservice.Accounts,
descEmailField, // local field (invitation.description.email)
repository.Field("login"), // foreign field (account.login)
regField, // array: 0-length or ≥1
).
// 5) Projection
Project(
repository.SimpleAlias(
empField.Dot("description"),
repository.Ref(accField),
),
repository.SimpleAlias(
empField.Dot("avatarUrl"),
repository.Ref(accField.Dot("avatarUrl")),
),
repository.SimpleAlias(
orgField.Dot("description"),
repository.Ref(orgField),
),
repository.SimpleAlias(
orgField.Dot("logoUrl"),
repository.Ref(orgField.Dot("logoUrl")),
),
repository.SimpleAlias(
roleField,
repository.Ref(roleField),
),
repository.SimpleAlias(
repository.Field("invitation"), // ← left-hand side
repository.Ref(repository.Field("description")), // ← right-hand side (“$description”)
),
repository.SimpleAlias(
repository.Field("storable"), // ← left-hand side
repository.RootRef(), // ← right-hand side (“$description”)
),
repository.ProjectionExpr(
repository.Field("registrationRequired"),
repository.Eq(
repository.Size(repository.Value(repository.Ref(regField).Build())),
repository.Literal(0),
),
),
)
var res model.PublicInvitation
haveResult := false
decoder := func(cur *mongo.Cursor) error {
if haveResult {
// should never get here
db.DBImp.Logger.Warn("Unexpected extra invitation", mzap.ObjRef("invitation_ref", invitationRef))
return merrors.Internal("Unexpected extra invitation found by reference")
}
if e := cur.Decode(&res); e != nil {
db.DBImp.Logger.Warn("Failed to decode entity", zap.Error(e), zap.Any("data", cur.Current.String()))
return e
}
haveResult = true
return nil
}
if err := db.DBImp.Repository.Aggregate(ctx, pipeline, decoder); err != nil {
db.DBImp.Logger.Warn("Failed to execute aggregation pipeline", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef))
return nil, err
}
if !haveResult {
db.DBImp.Logger.Warn("No results fetched", mzap.ObjRef("invitation_ref", invitationRef))
return nil, merrors.NoData(fmt.Sprintf("Invitation %s not found", invitationRef.Hex()))
}
return &res, nil
}

View File

@@ -0,0 +1,28 @@
package invitationdb
import (
"context"
"errors"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func (db *InvitationDB) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error) {
res, err := mauth.GetProtectedObjects[model.Invitation](
ctx,
db.DBImp.Logger,
accountRef, organizationRef, model.ActionRead,
repository.OrgFilter(organizationRef),
cursor,
db.Enforcer,
db.DBImp.Repository,
)
if errors.Is(err, merrors.ErrNoData) {
return []model.Invitation{}, nil
}
return res, err
}

View File

@@ -0,0 +1,26 @@
package invitationdb
import (
"context"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (db *InvitationDB) updateStatus(ctx context.Context, invitationRef primitive.ObjectID, newStatus model.InvitationStatus) error {
// db.DBImp.Up
var inv model.Invitation
if err := db.DBImp.FindOne(ctx, repository.IDFilter(invitationRef), &inv); err != nil {
db.DBImp.Logger.Warn("Failed to fetch invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef), zap.String("new_status", string(newStatus)))
return err
}
inv.Status = newStatus
if err := db.DBImp.Update(ctx, &inv); err != nil {
db.DBImp.Logger.Warn("Failed to update invitation", zap.Error(err), mzap.ObjRef("invitation_ref", invitationRef), zap.String("new_status", string(newStatus)))
return err
}
return nil
}