stillbox/pkg/shares/share.go
2025-01-19 21:51:39 -05:00

143 lines
3 KiB
Go

package shares
import (
"context"
"errors"
"time"
"dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/calls/callstore"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid"
"github.com/matoous/go-nanoid"
)
const (
SlugLength = 20
)
var (
ErrExpirationTooSoon = errors.New("expiration too soon")
ErrBadType = errors.New("bad share type")
)
type EntityType string
const (
EntityIncident EntityType = "incident"
EntityCall EntityType = "call"
)
func (et EntityType) IsValid() bool {
switch et {
case EntityCall, EntityIncident:
return true
}
return false
}
// If an incident is shared, all calls that are part of it must be shared too, but this can be through the incident share (/share/bLaH/callID[.mp3])
type Share struct {
ID string `json:"id"`
Type EntityType `json:"entityType"`
Date *jsontypes.Time `json:"-"` // we handle this for the user
Owner users.UserID `json:"owner"`
EntityID uuid.UUID `json:"entityID"`
Expiration *jsontypes.Time `json:"expiration"`
}
func (s *Share) GetName() string {
return "SHARE:" + s.ID
}
func (s *Share) GetRoles() []string {
return []string{rbac.RoleShareGuest}
}
func (s *Share) GetResourceName() string {
return rbac.ResourceShare
}
type CreateShareParams struct {
Type EntityType `json:"entityType"`
EntityID uuid.UUID `json:"entityID"`
Expiration *jsontypes.Duration `json:"expiration"`
}
func (s *service) checkEntity(ctx context.Context, sh *CreateShareParams) (*time.Time, error) {
var t *time.Time
switch sh.Type {
case EntityCall:
cs := callstore.FromCtx(ctx)
// Call does RBAC for us
call, err := cs.Call(ctx, sh.EntityID)
if err != nil {
return nil, err
}
t = &call.DateTime
case EntityIncident:
is := incstore.FromCtx(ctx)
i, err := is.Owner(ctx, sh.EntityID)
if err != nil {
return nil, err
}
_, err = rbac.Check(ctx, &i, rbac.WithActions(rbac.ActionShare))
if err != nil {
return nil, err
}
}
return t, nil
}
// NewShare creates a new share.
func (s *service) NewShare(ctx context.Context, sh CreateShareParams) (*Share, error) {
if !sh.Type.IsValid() {
return nil, ErrBadType
}
// entTime is only meaningful if we are a call
entTime, err := s.checkEntity(ctx, &sh)
if err != nil {
return nil, err
}
u, err := users.From(ctx)
if err != nil {
return nil, err
}
id, err := gonanoid.ID(SlugLength)
if err != nil {
return nil, err
}
share := &Share{
ID: id,
Type: sh.Type,
Date: (*jsontypes.Time)(entTime),
Owner: u.ID,
EntityID: sh.EntityID,
}
if sh.Expiration != nil {
if sh.Expiration.Duration() < time.Minute {
return nil, ErrExpirationTooSoon
}
share.Expiration = common.PtrTo(jsontypes.Time(time.Now().Add(sh.Expiration.Duration())))
}
err = s.Create(ctx, share)
if err != nil {
return nil, err
}
return share, nil
}