fx build fix
This commit is contained in:
409
api/server/interface/accountservice/internal/service.go
Normal file
409
api/server/interface/accountservice/internal/service.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"unicode"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/auth/management"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/organization"
|
||||
"github.com/tech/sendico/pkg/db/policy"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"github.com/tech/sendico/server/internal/mutil/flrstring"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
logger mlogger.Logger
|
||||
accountDB account.DB
|
||||
orgDB organization.DB
|
||||
enforcer auth.Enforcer
|
||||
roleManager management.Role
|
||||
config *middleware.PasswordConfig
|
||||
tf transaction.Factory
|
||||
|
||||
policyDB policy.DB
|
||||
}
|
||||
|
||||
func validateUserRequest(u *model.Account) error {
|
||||
if u.Name == "" {
|
||||
return merrors.InvalidArgument("Name must not be empty")
|
||||
}
|
||||
if u.Login == "" {
|
||||
return merrors.InvalidArgument("Login must not be empty")
|
||||
}
|
||||
if u.Password == "" {
|
||||
return merrors.InvalidArgument("Password must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ValidatePassword(
|
||||
password string,
|
||||
oldPassword *string,
|
||||
) error {
|
||||
var hasDigit, hasUpper, hasLower, hasSpecial bool
|
||||
|
||||
if oldPassword != nil {
|
||||
if *oldPassword == password {
|
||||
return merrors.InvalidArgument("New password cannot be the same as the old password")
|
||||
}
|
||||
}
|
||||
|
||||
if len(password) < s.config.Check.MinLength {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("Password must be at least %d characters long", s.config.Check.MinLength))
|
||||
}
|
||||
|
||||
// Check for digit, uppercase, lowercase, and special character
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsDigit(char):
|
||||
hasDigit = true
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.Check.Digit && !hasDigit {
|
||||
return merrors.InvalidArgument("Password must contain at least one digit")
|
||||
}
|
||||
if s.config.Check.Upper && !hasUpper {
|
||||
return merrors.InvalidArgument("Password must contain at least one uppercase letter")
|
||||
}
|
||||
if s.config.Check.Lower && !hasLower {
|
||||
return merrors.InvalidArgument("Password must contain at least one lowercase letter")
|
||||
}
|
||||
if s.config.Check.Special && !hasSpecial {
|
||||
return merrors.InvalidArgument("Password must contain at least one special character")
|
||||
}
|
||||
|
||||
// If all checks pass, return nil (no error)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ValidateAccount(acct *model.Account) error {
|
||||
if err := validateUserRequest(acct); err != nil {
|
||||
s.logger.Warn("Invalid signup acccount received", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.ValidatePassword(acct.Password, nil); err != nil {
|
||||
s.logger.Warn("Password validation failed", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := acct.HashPassword(); err != nil {
|
||||
s.logger.Warn("Failed to hash password", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) CreateAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID primitive.ObjectID,
|
||||
) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("Organization must not be nil")
|
||||
}
|
||||
if acct == nil || len(acct.Login) == 0 {
|
||||
return merrors.InvalidArgument("Account must have a non-empty login")
|
||||
}
|
||||
if roleDescID == primitive.NilObjectID {
|
||||
return merrors.InvalidArgument("Role description must be provided")
|
||||
}
|
||||
// 1) Create the account
|
||||
if err := s.accountDB.Create(ctx, acct); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
s.logger.Info("Username is already taken", zap.String("login", acct.Login))
|
||||
} else {
|
||||
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 2) Add to organization
|
||||
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
|
||||
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error {
|
||||
// Check if this is the only member in the organization
|
||||
if len(org.Members) <= 1 {
|
||||
s.logger.Warn("Cannot delete account - it's the only member in the organization",
|
||||
mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(org))
|
||||
return merrors.InvalidArgument("Cannot delete the only member of an organization")
|
||||
}
|
||||
|
||||
// 1) Remove from organization
|
||||
if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// 2) Delete the account document
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) RemoveAccountFromOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("Organization must not be nil")
|
||||
}
|
||||
roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org),
|
||||
mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
for _, role := range roles {
|
||||
if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to revoke account role", zap.Error(err),
|
||||
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, member := range org.Members {
|
||||
if member == accountRef {
|
||||
// Remove the member by slicing it out
|
||||
org.Members = append(org.Members[:i], org.Members[i+1:]...)
|
||||
if err := s.orgDB.Update(ctx, accountRef, org); err != nil {
|
||||
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) error {
|
||||
acct.ResetPasswordToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return s.accountDB.Update(ctx, acct)
|
||||
}
|
||||
|
||||
func (s *service) UpdateLogin(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) error {
|
||||
acct.EmailBackup = acct.Login
|
||||
acct.Login = newLogin
|
||||
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return s.accountDB.Update(ctx, acct)
|
||||
}
|
||||
|
||||
func (s *service) JoinOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
account *model.Account,
|
||||
roleDescID primitive.ObjectID,
|
||||
) error {
|
||||
if slices.Contains(org.Members, account.ID) {
|
||||
s.logger.Debug("Account is already a member", mzap.StorableRef(org), mzap.StorableRef(account))
|
||||
return nil
|
||||
}
|
||||
org.Members = append(org.Members, account.ID)
|
||||
if err := s.orgDB.Update(ctx, *account.GetID(), org); err != nil {
|
||||
s.logger.Warn("Failed to update organization members list", zap.Error(err), mzap.StorableRef(account))
|
||||
return err
|
||||
}
|
||||
|
||||
role := &model.Role{
|
||||
DescriptionRef: roleDescID,
|
||||
OrganizationRef: org.ID,
|
||||
AccountRef: account.ID,
|
||||
}
|
||||
if err := s.roleManager.Assign(ctx, role); err != nil {
|
||||
s.logger.Warn("Failed to assign role to account", zap.Error(err), mzap.StorableRef(account))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) deleteOrganizationRoles(ctx context.Context, orgRef primitive.ObjectID) error {
|
||||
s.logger.Info("Deleting roles for organization", mzap.ObjRef("organization_ref", orgRef))
|
||||
|
||||
// Get all roles for the organization
|
||||
roles, err := s.roleManager.List(ctx, orgRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
s.logger.Warn("Failed to fetch roles for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete each role
|
||||
for _, role := range roles {
|
||||
if err := s.roleManager.Delete(ctx, role.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete role", zap.Error(err), mzap.ObjRef("role_ref", role.ID))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Successfully deleted roles", zap.Int("count", len(roles)), mzap.ObjRef("organization_ref", orgRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) deleteOrganizationPolicies(ctx context.Context, orgRef primitive.ObjectID) error {
|
||||
s.logger.Info("Deleting policies for organization", mzap.ObjRef("organization_ref", orgRef))
|
||||
|
||||
// Get all policies for the organization
|
||||
policies, err := s.policyDB.All(ctx, orgRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
s.logger.Warn("Failed to fetch policies for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete each policy
|
||||
for _, policy := range policies {
|
||||
if err := s.policyDB.Delete(ctx, policy.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete policy", zap.Error(err), mzap.ObjRef("policy_ref", policy.ID))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Successfully deleted policies", zap.Int("count", len(policies)), mzap.ObjRef("organization_ref", orgRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
) error {
|
||||
s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
// 8. Delete all roles and role descriptions in the organization
|
||||
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 9. Delete all policies in the organization
|
||||
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 10. Finally, delete the organization itself
|
||||
if err := s.orgDB.Delete(ctx, primitive.NilObjectID, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Organization deleted successfully", mzap.StorableRef(org))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteAll(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error {
|
||||
s.logger.Info("Starting complete deletion (organization + account)",
|
||||
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
// 1. First delete the organization and all its data
|
||||
if err := s.DeleteOrganization(ctx, org); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Then delete the account
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAccountService wires in your logger plus the three dependencies.
|
||||
func NewAccountService(
|
||||
l mlogger.Logger,
|
||||
dbf db.Factory,
|
||||
enforcer auth.Enforcer,
|
||||
ra management.Role,
|
||||
config *middleware.PasswordConfig,
|
||||
) (*service, error) {
|
||||
logger := l.Named("account_service")
|
||||
|
||||
if config == nil {
|
||||
return nil, merrors.Internal("Invalid account service configuration provides")
|
||||
}
|
||||
|
||||
res := &service{
|
||||
logger: logger,
|
||||
enforcer: enforcer,
|
||||
roleManager: ra,
|
||||
config: config,
|
||||
tf: dbf.TransactionFactory(),
|
||||
}
|
||||
var err error
|
||||
if res.accountDB, err = dbf.NewAccountDB(); err != nil {
|
||||
logger.Warn("Failed to create accounts database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if res.orgDB, err = dbf.NewOrganizationDB(); err != nil {
|
||||
logger.Warn("Failed to create organizations database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize database dependencies for cascade deletion
|
||||
if res.policyDB, err = dbf.NewPoliciesDB(); err != nil {
|
||||
logger.Warn("Failed to create policies database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestDeleteAccount_Validation(t *testing.T) {
|
||||
t.Run("DeleteAccount_LastMemberFails", func(t *testing.T) {
|
||||
orgID := primitive.NewObjectID()
|
||||
accountID := primitive.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Single Member Org"},
|
||||
},
|
||||
Members: []primitive.ObjectID{accountID}, // Only one member
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should fail because it's the only member
|
||||
err := validateDeleteAccount(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Cannot delete the only member")
|
||||
})
|
||||
|
||||
t.Run("DeleteAccount_MultipleMembersSuccess", func(t *testing.T) {
|
||||
orgID := primitive.NewObjectID()
|
||||
accountID := primitive.NewObjectID()
|
||||
otherAccountID := primitive.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Multi Member Org"},
|
||||
},
|
||||
Members: []primitive.ObjectID{accountID, otherAccountID}, // Multiple members
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should succeed because there are multiple members
|
||||
err := validateDeleteAccount(org)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("DeleteAccount_EmptyMembersList", func(t *testing.T) {
|
||||
orgID := primitive.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Empty Org"},
|
||||
},
|
||||
Members: []primitive.ObjectID{}, // No members
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should fail because there are no members
|
||||
err := validateDeleteAccount(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Cannot delete the only member")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteOrganization_Validation(t *testing.T) {
|
||||
t.Run("DeleteOrganization_NilOrganization", func(t *testing.T) {
|
||||
err := validateDeleteOrganization(nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization cannot be nil")
|
||||
})
|
||||
|
||||
t.Run("DeleteOrganization_EmptyOrganization", func(t *testing.T) {
|
||||
org := &model.Organization{}
|
||||
err := validateDeleteOrganization(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization ID cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("DeleteOrganization_ValidOrganization", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = primitive.NewObjectID()
|
||||
|
||||
err := validateDeleteOrganization(org)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteAll_Validation(t *testing.T) {
|
||||
t.Run("DeleteAll_NilOrganization", func(t *testing.T) {
|
||||
accountID := primitive.NewObjectID()
|
||||
err := validateDeleteAll(nil, accountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization cannot be nil")
|
||||
})
|
||||
|
||||
t.Run("DeleteAll_EmptyAccountID", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = primitive.NewObjectID()
|
||||
|
||||
err := validateDeleteAll(org, primitive.NilObjectID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "account ID cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("DeleteAll_ValidInput", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = primitive.NewObjectID()
|
||||
accountID := primitive.NewObjectID()
|
||||
|
||||
err := validateDeleteAll(org, accountID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions that implement the validation logic from the service
|
||||
func validateDeleteAccount(org *model.Organization) error {
|
||||
if len(org.Members) <= 1 {
|
||||
return merrors.InvalidArgument("Cannot delete the only member of an organization")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDeleteOrganization(org *model.Organization) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("organization cannot be nil")
|
||||
}
|
||||
if org.ID == primitive.NilObjectID {
|
||||
return merrors.InvalidArgument("organization ID cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDeleteAll(org *model.Organization, accountRef primitive.ObjectID) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("organization cannot be nil")
|
||||
}
|
||||
if accountRef == primitive.NilObjectID {
|
||||
return merrors.InvalidArgument("account ID cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
298
api/server/interface/accountservice/internal/service_test.go
Normal file
298
api/server/interface/accountservice/internal/service_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
apiconfig "github.com/tech/sendico/server/internal/api/config"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestValidatePassword tests the password validation logic directly
|
||||
func TestValidatePassword(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 32,
|
||||
}
|
||||
|
||||
// Create a minimal service for testing password validation
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
t.Run("ValidPassword", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword123!", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("PasswordTooShort", func(t *testing.T) {
|
||||
err := service.ValidatePassword("Test1!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least 8 characters")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingDigit", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one digit")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingUppercase", func(t *testing.T) {
|
||||
err := service.ValidatePassword("testpassword123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one uppercase")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingLowercase", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TESTPASSWORD123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one lowercase")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingSpecialCharacter", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword123", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one special character")
|
||||
})
|
||||
|
||||
t.Run("PasswordSameAsOld", func(t *testing.T) {
|
||||
oldPassword := "TestPassword123!"
|
||||
err := service.ValidatePassword("TestPassword123!", &oldPassword)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot be the same as the old password")
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateAccount tests the account validation logic directly
|
||||
func TestValidateAccount(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 32,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
t.Run("ValidAccount", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
originalPassword := account.Password
|
||||
err := service.ValidateAccount(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Password should be hashed after validation
|
||||
assert.NotEqual(t, originalPassword, account.Password)
|
||||
assert.NotEmpty(t, account.VerifyToken)
|
||||
assert.Equal(t, config.TokenLength, len(account.VerifyToken))
|
||||
})
|
||||
|
||||
t.Run("AccountMissingName", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Name must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountMissingLogin", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Login must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountMissingPassword", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Password must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountInvalidPassword", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "weak", // Should fail validation
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
// Should fail on password validation
|
||||
assert.Contains(t, err.Error(), "at least 8 characters")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPasswordConfiguration verifies different password rule configurations
|
||||
func TestPasswordConfiguration(t *testing.T) {
|
||||
t.Run("MinimalRequirements", func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 4,
|
||||
Digit: false,
|
||||
Upper: false,
|
||||
Lower: false,
|
||||
Special: false,
|
||||
},
|
||||
TokenLength: 16,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Should pass with minimal requirements
|
||||
err := service.ValidatePassword("test", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("StrictRequirements", func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 12,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 64,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Should fail with shorter password
|
||||
err := service.ValidatePassword("Test123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least 12 characters")
|
||||
|
||||
// Should pass with longer password
|
||||
err = service.ValidatePassword("TestPassword123!", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTokenGeneration verifies that verification tokens are generated with correct length
|
||||
func TestTokenGeneration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tokenLength int
|
||||
}{
|
||||
{"Short", 8},
|
||||
{"Medium", 32},
|
||||
{"Long", 64},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: tc.tokenLength,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.tokenLength, len(account.VerifyToken))
|
||||
})
|
||||
}
|
||||
}
|
||||
99
api/server/interface/accountservice/types.go
Normal file
99
api/server/interface/accountservice/types.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package accountservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/auth/management"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
accountserviceimp "github.com/tech/sendico/server/interface/accountservice/internal"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// AccountService defines all account-related workflows.
|
||||
type AccountService interface {
|
||||
// ValidateAccount will:
|
||||
// 1) check it's completeness
|
||||
// 2) hash password
|
||||
// 3) prepare verification token
|
||||
ValidateAccount(
|
||||
acct *model.Account,
|
||||
) error
|
||||
|
||||
// ValidatePassword will:
|
||||
// 1) check passsword conformance
|
||||
ValidatePassword(
|
||||
password string,
|
||||
oldPassword *string,
|
||||
) error
|
||||
|
||||
// ResetPassword will:
|
||||
// 1) generate reset password token
|
||||
ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) error
|
||||
|
||||
// CreateAccount will:
|
||||
// 1) create the account
|
||||
// 2) add it to the org’s member list
|
||||
// 3) assign the given role description to it
|
||||
CreateAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID primitive.ObjectID,
|
||||
) error
|
||||
|
||||
JoinOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID primitive.ObjectID,
|
||||
) error
|
||||
|
||||
UpdateLogin(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) error
|
||||
|
||||
// DeleteAccount deletes the account and removes it from the org.
|
||||
DeleteAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error
|
||||
|
||||
// RemoveAccountFromOrganization just drops it from the member slice.
|
||||
RemoveAccountFromOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error
|
||||
|
||||
DeleteOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
) error
|
||||
|
||||
// DeleteAll deletes both the organization and the account.
|
||||
DeleteAll(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef primitive.ObjectID,
|
||||
) error
|
||||
}
|
||||
|
||||
func NewAccountService(
|
||||
logger mlogger.Logger,
|
||||
dbf db.Factory,
|
||||
enforcer auth.Enforcer,
|
||||
roleManeger management.Role,
|
||||
config *middleware.PasswordConfig,
|
||||
) (AccountService, error) {
|
||||
return accountserviceimp.NewAccountService(logger, dbf, enforcer, roleManeger, config)
|
||||
}
|
||||
20
api/server/interface/api/api.go
Normal file
20
api/server/interface/api/api.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
Logger() mlogger.Logger
|
||||
DomainProvider() domainprovider.DomainProvider
|
||||
Config() *Config
|
||||
DBFactory() db.Factory
|
||||
Permissions() auth.Provider
|
||||
Register() Register
|
||||
}
|
||||
|
||||
type MicroServiceFactoryT = func(API) (mservice.MicroService, error)
|
||||
11
api/server/interface/api/config.go
Normal file
11
api/server/interface/api/config.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
mwa "github.com/tech/sendico/server/interface/middleware"
|
||||
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mw *mwa.Config `yaml:"middleware"`
|
||||
Storage *fsc.Config `yaml:"storage"`
|
||||
}
|
||||
10
api/server/interface/api/permissions/deny.go
Normal file
10
api/server/interface/api/permissions/deny.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func Deny(_ *model.Account, _ *auth.Enforcer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
10
api/server/interface/api/permissions/donotcheck.go
Normal file
10
api/server/interface/api/permissions/donotcheck.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func DoNotCheck(_ *model.Account, _ *auth.Enforcer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
17
api/server/interface/api/register.go
Normal file
17
api/server/interface/api/register.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/api/ws"
|
||||
)
|
||||
|
||||
type Register interface {
|
||||
Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
|
||||
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
|
||||
WSHandler(messageType string, handler ws.HandlerFunc)
|
||||
|
||||
Messaging() messaging.Register
|
||||
}
|
||||
7
api/server/interface/api/srequest/acceptinvitation.go
Normal file
7
api/server/interface/api/srequest/acceptinvitation.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type AcceptInvitation struct {
|
||||
Account *model.AccountData `json:"account,omitempty"`
|
||||
}
|
||||
12
api/server/interface/api/srequest/changepolicies.go
Normal file
12
api/server/interface/api/srequest/changepolicies.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ChangePolicies struct {
|
||||
RoleRef primitive.ObjectID `json:"roleRef"`
|
||||
Add *[]model.RolePolicy `json:"add,omitempty"`
|
||||
Remove *[]model.RolePolicy `json:"remove,omitempty"`
|
||||
}
|
||||
8
api/server/interface/api/srequest/changerole.go
Normal file
8
api/server/interface/api/srequest/changerole.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type ChangeRole struct {
|
||||
AccountRef primitive.ObjectID `json:"accountRef"`
|
||||
NewRoleDescriptionRef primitive.ObjectID `json:"newRoleDescriptionRef"`
|
||||
}
|
||||
7
api/server/interface/api/srequest/file.go
Normal file
7
api/server/interface/api/srequest/file.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type FileUpload struct {
|
||||
ObjRef primitive.ObjectID `json:"objRef"`
|
||||
}
|
||||
7
api/server/interface/api/srequest/invitation.go
Normal file
7
api/server/interface/api/srequest/invitation.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type CreateInvitation = model.Invitation
|
||||
8
api/server/interface/api/srequest/login.go
Normal file
8
api/server/interface/api/srequest/login.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type Login struct {
|
||||
model.SessionIdentifier `json:",inline"`
|
||||
model.LoginData `json:"login"`
|
||||
}
|
||||
15
api/server/interface/api/srequest/password.go
Normal file
15
api/server/interface/api/srequest/password.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package srequest
|
||||
|
||||
type ChangePassword struct {
|
||||
Old string `json:"old"`
|
||||
New string `json:"new"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
}
|
||||
|
||||
type ResetPassword struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ForgotPassword struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
8
api/server/interface/api/srequest/priority.go
Normal file
8
api/server/interface/api/srequest/priority.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type CreatePriorityGroup struct {
|
||||
Description model.Describable `json:"description"`
|
||||
Priorities []model.Colorable `json:"priorities"`
|
||||
}
|
||||
31
api/server/interface/api/srequest/project.go
Normal file
31
api/server/interface/api/srequest/project.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type CreateProject struct {
|
||||
Project model.Describable `json:"project"`
|
||||
LogoURI *string `json:"logoUrl,omitempty"`
|
||||
PrioriyGroupRef primitive.ObjectID `json:"priorityGroupRef"`
|
||||
StatusGroupRef primitive.ObjectID `json:"statusGroupRef"`
|
||||
Mnemonic string `json:"mnemonic"`
|
||||
}
|
||||
|
||||
type ProjectPreview struct {
|
||||
Projects []primitive.ObjectID `json:"projects"`
|
||||
}
|
||||
|
||||
type TagFilterMode string
|
||||
|
||||
const (
|
||||
TagFilterModeNone TagFilterMode = "none"
|
||||
TagFilterModePresent TagFilterMode = "present"
|
||||
TagFilterModeMissing TagFilterMode = "missing"
|
||||
TagFilterModeIncludeAny TagFilterMode = "includeAny"
|
||||
TagFilterModeIncludeAll TagFilterMode = "includeAll"
|
||||
TagFilterModeExcludeAny TagFilterMode = "excludeAny"
|
||||
)
|
||||
|
||||
type ProjectsFilter = model.ProjectFilterBase
|
||||
11
api/server/interface/api/srequest/project_delete.go
Normal file
11
api/server/interface/api/srequest/project_delete.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// DeleteProject represents a request to delete a project
|
||||
type DeleteProject struct {
|
||||
OrganizationRef primitive.ObjectID `json:"organizationRef"` // If provided, move tasks to this project. If null, delete all tasks
|
||||
MoveTasksToProjectRef *primitive.ObjectID `json:"moveTasksToProjectRef,omitempty"` // If provided, move tasks to this project. If null, delete all tasks
|
||||
}
|
||||
5
api/server/interface/api/srequest/refresh.go
Normal file
5
api/server/interface/api/srequest/refresh.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type AccessTokenRefresh = model.ClientRefreshToken
|
||||
19
api/server/interface/api/srequest/reorder.go
Normal file
19
api/server/interface/api/srequest/reorder.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type Reorder struct {
|
||||
ParentRef primitive.ObjectID `json:"parentRef"`
|
||||
From int `json:"from"`
|
||||
To int `json:"to"`
|
||||
}
|
||||
|
||||
type ReorderX struct {
|
||||
ObjectRef primitive.ObjectID `json:"objectRef"`
|
||||
To int `json:"to"`
|
||||
}
|
||||
|
||||
type ReorderXDefault struct {
|
||||
ReorderX `json:",inline"`
|
||||
ParentRef primitive.ObjectID `json:"parentRef"`
|
||||
}
|
||||
5
api/server/interface/api/srequest/rotate.go
Normal file
5
api/server/interface/api/srequest/rotate.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type TokenRefreshRotate = model.ClientRefreshToken
|
||||
13
api/server/interface/api/srequest/sgchange.go
Normal file
13
api/server/interface/api/srequest/sgchange.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
type GroupItemChange struct {
|
||||
GroupRef primitive.ObjectID `json:"groupRef"`
|
||||
ItemRef primitive.ObjectID `json:"itemRef"`
|
||||
}
|
||||
|
||||
type RemoveItemFromGroup struct {
|
||||
GroupItemChange `json:",inline"`
|
||||
TargetItemRef primitive.ObjectID `json:"targetItemRef"`
|
||||
}
|
||||
14
api/server/interface/api/srequest/signup.go
Normal file
14
api/server/interface/api/srequest/signup.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type Signup struct {
|
||||
Account model.AccountData `json:"account"`
|
||||
OrganizationName string `json:"organizationName"`
|
||||
OrganizationTimeZone string `json:"organizationTimeZone"`
|
||||
DefaultPriorityGroup CreatePriorityGroup `json:"defaultPriorityGroup"`
|
||||
DefaultStatusGroup CreateStatusGroup `json:"defaultStatusGroup"`
|
||||
AnonymousUser model.Describable `json:"anonymousUser"`
|
||||
OwnerRole model.Describable `json:"ownerRole"`
|
||||
AnonymousRole model.Describable `json:"anonymousRole"`
|
||||
}
|
||||
312
api/server/interface/api/srequest/signup_test.go
Normal file
312
api/server/interface/api/srequest/signup_test.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package srequest_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Helper function to create string pointers
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestSignupRequest_JSONSerialization(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Name: "Test User",
|
||||
},
|
||||
OrganizationName: "Test Organization",
|
||||
OrganizationTimeZone: "UTC",
|
||||
DefaultPriorityGroup: srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "Default Priority Group",
|
||||
},
|
||||
Priorities: []model.Colorable{
|
||||
{
|
||||
Describable: model.Describable{Name: "High"},
|
||||
Color: stringPtr("#FF0000"),
|
||||
},
|
||||
{
|
||||
Describable: model.Describable{Name: "Medium"},
|
||||
Color: stringPtr("#FFFF00"),
|
||||
},
|
||||
{
|
||||
Describable: model.Describable{Name: "Low"},
|
||||
Color: stringPtr("#00FF00"),
|
||||
},
|
||||
},
|
||||
},
|
||||
AnonymousUser: model.Describable{
|
||||
Name: "Anonymous User",
|
||||
},
|
||||
OwnerRole: model.Describable{
|
||||
Name: "Owner",
|
||||
},
|
||||
AnonymousRole: model.Describable{
|
||||
Name: "Anonymous",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all fields are properly serialized/deserialized
|
||||
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
||||
assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password)
|
||||
assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName)
|
||||
assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone)
|
||||
assert.Equal(t, signup.DefaultPriorityGroup.Description.Name, unmarshaled.DefaultPriorityGroup.Description.Name)
|
||||
assert.Equal(t, len(signup.DefaultPriorityGroup.Priorities), len(unmarshaled.DefaultPriorityGroup.Priorities))
|
||||
assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name)
|
||||
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
|
||||
assert.Equal(t, signup.AnonymousRole.Name, unmarshaled.AnonymousRole.Name)
|
||||
|
||||
// Verify priorities
|
||||
for i, priority := range signup.DefaultPriorityGroup.Priorities {
|
||||
assert.Equal(t, priority.Name, unmarshaled.DefaultPriorityGroup.Priorities[i].Name)
|
||||
if priority.Color != nil && unmarshaled.DefaultPriorityGroup.Priorities[i].Color != nil {
|
||||
assert.Equal(t, *priority.Color, *unmarshaled.DefaultPriorityGroup.Priorities[i].Color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignupRequest_MinimalValidRequest(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Name: "Test User",
|
||||
},
|
||||
OrganizationName: "Test Organization",
|
||||
OrganizationTimeZone: "UTC",
|
||||
DefaultPriorityGroup: srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "Default",
|
||||
},
|
||||
Priorities: []model.Colorable{
|
||||
{
|
||||
Describable: model.Describable{Name: "Normal"},
|
||||
Color: stringPtr("#000000"),
|
||||
},
|
||||
},
|
||||
},
|
||||
AnonymousUser: model.Describable{
|
||||
Name: "Anonymous",
|
||||
},
|
||||
OwnerRole: model.Describable{
|
||||
Name: "Owner",
|
||||
},
|
||||
AnonymousRole: model.Describable{
|
||||
Name: "Anonymous",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify minimal request is valid
|
||||
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
||||
assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName)
|
||||
assert.Len(t, unmarshaled.DefaultPriorityGroup.Priorities, 1)
|
||||
}
|
||||
|
||||
func TestSignupRequest_InvalidJSON(t *testing.T) {
|
||||
invalidJSONs := []string{
|
||||
`{"account": invalid}`,
|
||||
`{"organizationName": 123}`,
|
||||
`{"organizationTimeZone": true}`,
|
||||
`{"defaultPriorityGroup": "not_an_object"}`,
|
||||
`{"anonymousUser": []}`,
|
||||
`{"anonymousRole": 456}`,
|
||||
`{invalid json}`,
|
||||
}
|
||||
|
||||
for i, invalidJSON := range invalidJSONs {
|
||||
t.Run(fmt.Sprintf("Invalid JSON %d", i), func(t *testing.T) {
|
||||
var signup srequest.Signup
|
||||
err := json.Unmarshal([]byte(invalidJSON), &signup)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignupRequest_UnicodeCharacters(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "测试@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Name: "Test 用户 Üser",
|
||||
},
|
||||
OrganizationName: "测试 Organization",
|
||||
OrganizationTimeZone: "UTC",
|
||||
DefaultPriorityGroup: srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "默认 Priority Group",
|
||||
},
|
||||
Priorities: []model.Colorable{
|
||||
{
|
||||
Describable: model.Describable{Name: "高"},
|
||||
Color: stringPtr("#FF0000"),
|
||||
},
|
||||
},
|
||||
},
|
||||
AnonymousUser: model.Describable{
|
||||
Name: "匿名 User",
|
||||
},
|
||||
OwnerRole: model.Describable{
|
||||
Name: "所有者",
|
||||
},
|
||||
AnonymousRole: model.Describable{
|
||||
Name: "匿名",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify unicode characters are properly handled
|
||||
assert.Equal(t, "测试@example.com", unmarshaled.Account.Login)
|
||||
assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name)
|
||||
assert.Equal(t, "测试 Organization", unmarshaled.OrganizationName)
|
||||
assert.Equal(t, "默认 Priority Group", unmarshaled.DefaultPriorityGroup.Description.Name)
|
||||
assert.Equal(t, "高", unmarshaled.DefaultPriorityGroup.Priorities[0].Name)
|
||||
assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name)
|
||||
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
|
||||
assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
|
||||
}
|
||||
|
||||
func TestCreatePriorityGroup_JSONSerialization(t *testing.T) {
|
||||
priorityGroup := srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "Test Priority Group",
|
||||
},
|
||||
Priorities: []model.Colorable{
|
||||
{
|
||||
Describable: model.Describable{Name: "Critical"},
|
||||
Color: stringPtr("#FF0000"),
|
||||
},
|
||||
{
|
||||
Describable: model.Describable{Name: "High"},
|
||||
Color: stringPtr("#FF8000"),
|
||||
},
|
||||
{
|
||||
Describable: model.Describable{Name: "Medium"},
|
||||
Color: stringPtr("#FFFF00"),
|
||||
},
|
||||
{
|
||||
Describable: model.Describable{Name: "Low"},
|
||||
Color: stringPtr("#00FF00"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(priorityGroup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.CreatePriorityGroup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all fields are properly serialized/deserialized
|
||||
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
|
||||
assert.Equal(t, len(priorityGroup.Priorities), len(unmarshaled.Priorities))
|
||||
|
||||
for i, priority := range priorityGroup.Priorities {
|
||||
assert.Equal(t, priority.Name, unmarshaled.Priorities[i].Name)
|
||||
if priority.Color != nil && unmarshaled.Priorities[i].Color != nil {
|
||||
assert.Equal(t, *priority.Color, *unmarshaled.Priorities[i].Color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePriorityGroup_EmptyPriorities(t *testing.T) {
|
||||
priorityGroup := srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "Empty Priority Group",
|
||||
},
|
||||
Priorities: []model.Colorable{},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(priorityGroup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.CreatePriorityGroup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify empty priorities array is handled correctly
|
||||
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
|
||||
assert.Empty(t, unmarshaled.Priorities)
|
||||
}
|
||||
|
||||
func TestCreatePriorityGroup_NilPriorities(t *testing.T) {
|
||||
priorityGroup := srequest.CreatePriorityGroup{
|
||||
Description: model.Describable{
|
||||
Name: "Nil Priority Group",
|
||||
},
|
||||
Priorities: nil,
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(priorityGroup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.CreatePriorityGroup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify nil priorities is handled correctly
|
||||
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
|
||||
assert.Nil(t, unmarshaled.Priorities)
|
||||
}
|
||||
16
api/server/interface/api/srequest/status.go
Normal file
16
api/server/interface/api/srequest/status.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type CreateStatus struct {
|
||||
model.Colorable `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
IsFinal bool `json:"isFinal"`
|
||||
}
|
||||
|
||||
type CreateStatusGroup struct {
|
||||
Description model.Describable `json:"description"`
|
||||
Statuses []CreateStatus `json:"statuses"`
|
||||
}
|
||||
20
api/server/interface/api/srequest/taggable.go
Normal file
20
api/server/interface/api/srequest/taggable.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
// TaggableSingle is used for single tag operations (add/remove tag)
|
||||
type TaggableSingle struct {
|
||||
ObjectRef primitive.ObjectID `json:"objectRef"`
|
||||
TagRef primitive.ObjectID `json:"tagRef"`
|
||||
}
|
||||
|
||||
// TaggableMultiple is used for multiple tag operations (add tags, set tags)
|
||||
type TaggableMultiple struct {
|
||||
ObjectRef primitive.ObjectID `json:"objectRef"`
|
||||
TagRefs []primitive.ObjectID `json:"tagRefs"`
|
||||
}
|
||||
|
||||
// TaggableObject is used for object-only operations (remove all tags, get tags)
|
||||
type TaggableObject struct {
|
||||
ObjectRef primitive.ObjectID `json:"objectRef"`
|
||||
}
|
||||
62
api/server/interface/api/sresponse/account.go
Normal file
62
api/server/interface/api/sresponse/account.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type accountData struct {
|
||||
model.AccountPublic `json:",inline"`
|
||||
IsAnonymous bool `json:"isAnonymous"`
|
||||
}
|
||||
|
||||
type accountResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Account accountData `json:"account"`
|
||||
}
|
||||
|
||||
func _createAccount(account *model.Account, isAnonymous bool) *accountData {
|
||||
return &accountData{
|
||||
AccountPublic: account.AccountPublic,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
func _toAccount(account *model.Account, orgRef primitive.ObjectID) *accountData {
|
||||
return _createAccount(account, model.AccountIsAnonymous(&account.UserDataBase, orgRef))
|
||||
}
|
||||
|
||||
func Account(logger mlogger.Logger, account *model.Account, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountResponse{
|
||||
Account: *_createAccount(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type accountsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Accounts []accountData `json:"accounts"`
|
||||
}
|
||||
|
||||
func Accounts(logger mlogger.Logger, accounts []model.Account, orgRef primitive.ObjectID, accessToken *TokenData) http.HandlerFunc {
|
||||
// Convert each account to its public representation.
|
||||
publicAccounts := make([]accountData, len(accounts))
|
||||
for i, a := range accounts {
|
||||
publicAccounts[i] = *_toAccount(&a, orgRef)
|
||||
}
|
||||
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountsResponse{
|
||||
Accounts: publicAccounts,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
5
api/server/interface/api/sresponse/authresp.go
Normal file
5
api/server/interface/api/sresponse/authresp.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package sresponse
|
||||
|
||||
type authResponse struct {
|
||||
AccessToken TokenData `json:"accessToken"`
|
||||
}
|
||||
15
api/server/interface/api/sresponse/badpassword.go
Normal file
15
api/server/interface/api/sresponse/badpassword.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func BadRPassword(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
|
||||
logger.Info("Failed password validation check", zap.Error(err))
|
||||
return response.BadRequest(logger, source, "invalid_request", err.Error())
|
||||
}
|
||||
24
api/server/interface/api/sresponse/commentp.go
Normal file
24
api/server/interface/api/sresponse/commentp.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type commentPreviewResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Comments []model.CommentPreview `json:"comments"`
|
||||
}
|
||||
|
||||
func CommentPreview(logger mlogger.Logger, accessToken *TokenData, comments []model.CommentPreview) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&commentPreviewResponse{
|
||||
Comments: comments,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
24
api/server/interface/api/sresponse/dzone.go
Normal file
24
api/server/interface/api/sresponse/dzone.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type dzoneResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
DZone model.DZone `json:"dzone"`
|
||||
}
|
||||
|
||||
func DZone(logger mlogger.Logger, dzone *model.DZone, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&dzoneResponse{
|
||||
DZone: *dzone,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
16
api/server/interface/api/sresponse/file.go
Normal file
16
api/server/interface/api/sresponse/file.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type fileUpladed struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func FileUploaded(logger mlogger.Logger, url string) http.HandlerFunc {
|
||||
return response.Ok(logger, &fileUpladed{URL: url})
|
||||
}
|
||||
21
api/server/interface/api/sresponse/invitation.go
Normal file
21
api/server/interface/api/sresponse/invitation.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type invitationResp struct {
|
||||
Invitation model.PublicInvitation `json:"invitation"`
|
||||
}
|
||||
|
||||
func Invitation(logger mlogger.Logger, invitation *model.PublicInvitation) http.HandlerFunc {
|
||||
return response.Ok(logger, &invitationResp{Invitation: *invitation})
|
||||
}
|
||||
|
||||
func Invitations(logger mlogger.Logger, invitations []model.Invitation) http.HandlerFunc {
|
||||
return response.Ok(logger, invitations)
|
||||
}
|
||||
27
api/server/interface/api/sresponse/login.go
Normal file
27
api/server/interface/api/sresponse/login.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type loginResponse struct {
|
||||
accountResponse
|
||||
RefreshToken TokenData `json:"refreshToken"`
|
||||
}
|
||||
|
||||
func Login(logger mlogger.Logger, account *model.Account, accessToken, refreshToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&loginResponse{
|
||||
accountResponse: accountResponse{
|
||||
Account: *_createAccount(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
RefreshToken: *refreshToken,
|
||||
},
|
||||
)
|
||||
}
|
||||
49
api/server/interface/api/sresponse/objects.go
Normal file
49
api/server/interface/api/sresponse/objects.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type DynamicResponse[T any] struct {
|
||||
authResponse `json:",inline"`
|
||||
Items []T
|
||||
// FieldName is the JSON key to use for the items.
|
||||
FieldName string
|
||||
}
|
||||
|
||||
func (dr DynamicResponse[T]) MarshalJSON() ([]byte, error) {
|
||||
// Create a temporary map to hold the keys and values.
|
||||
m := map[string]any{
|
||||
dr.FieldName: dr.Items,
|
||||
"accessToken": dr.AccessToken,
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
type handler = func(logger mlogger.Logger, data any) http.HandlerFunc
|
||||
|
||||
func objectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type, handler handler) http.HandlerFunc {
|
||||
resp := &DynamicResponse[T]{
|
||||
Items: items,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
FieldName: resource,
|
||||
}
|
||||
return handler(logger, resp)
|
||||
}
|
||||
|
||||
func ObjectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return objectsAuth(logger, items, accessToken, resource, response.Ok)
|
||||
}
|
||||
|
||||
func ObjectAuth[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return ObjectsAuth(logger, []T{*item}, accessToken, resource)
|
||||
}
|
||||
|
||||
func ObjectAuthCreated[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return objectsAuth(logger, []T{*item}, accessToken, resource, response.Created)
|
||||
}
|
||||
35
api/server/interface/api/sresponse/orgnization.go
Normal file
35
api/server/interface/api/sresponse/orgnization.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type organizationsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Organizations []model.Organization `json:"organizations"`
|
||||
}
|
||||
|
||||
func Organization(logger mlogger.Logger, organization *model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return Organizations(logger, []model.Organization{*organization}, accessToken)
|
||||
}
|
||||
|
||||
func Organizations(logger mlogger.Logger, organizations []model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationsResponse{
|
||||
Organizations: organizations,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
type organizationPublicResponse struct {
|
||||
Organizations []model.OrganizationBase `json:"organizations"`
|
||||
}
|
||||
|
||||
func OrganizationPublic(logger mlogger.Logger, organization *model.OrganizationBase) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationPublicResponse{
|
||||
[]model.OrganizationBase{*organization},
|
||||
})
|
||||
}
|
||||
45
api/server/interface/api/sresponse/permissions.go
Normal file
45
api/server/interface/api/sresponse/permissions.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type permissionsDescription struct {
|
||||
Roles []model.RoleDescription `json:"roles"`
|
||||
Policies []model.PolicyDescription `json:"policies"`
|
||||
}
|
||||
|
||||
type permissionsData struct {
|
||||
Roles []model.Role `json:"roles"`
|
||||
Policies []model.RolePolicy `json:"policies"`
|
||||
Permissions []model.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
type permissionsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Descriptions permissionsDescription `json:"descriptions"`
|
||||
Permissions permissionsData `json:"permissions"`
|
||||
}
|
||||
|
||||
func Permisssions(logger mlogger.Logger,
|
||||
rolesDescs []model.RoleDescription, policiesDescs []model.PolicyDescription,
|
||||
roles []model.Role, policies []model.RolePolicy, permissions []model.Permission,
|
||||
accessToken *TokenData,
|
||||
) http.HandlerFunc {
|
||||
return response.Ok(logger, permissionsResponse{
|
||||
Descriptions: permissionsDescription{
|
||||
Roles: rolesDescs,
|
||||
Policies: policiesDescs,
|
||||
},
|
||||
Permissions: permissionsData{
|
||||
Roles: roles,
|
||||
Policies: policies,
|
||||
Permissions: permissions,
|
||||
},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
37
api/server/interface/api/sresponse/projects.go
Normal file
37
api/server/interface/api/sresponse/projects.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type projectsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Projects []model.Project `json:"projects"`
|
||||
}
|
||||
|
||||
func Projects(logger mlogger.Logger, projects []model.Project, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, projectsResponse{
|
||||
Projects: projects,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func Project(logger mlogger.Logger, project *model.Project, accessToken *TokenData) http.HandlerFunc {
|
||||
return Projects(logger, []model.Project{*project}, accessToken)
|
||||
}
|
||||
|
||||
type projectPreviewsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Previews []model.ProjectPreview `json:"previews"`
|
||||
}
|
||||
|
||||
func ProjectsPreviews(logger mlogger.Logger, previews []model.ProjectPreview, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, &projectPreviewsResponse{
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
Previews: previews,
|
||||
})
|
||||
}
|
||||
12
api/server/interface/api/sresponse/response.go
Normal file
12
api/server/interface/api/sresponse/response.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type (
|
||||
HandlerFunc = func(r *http.Request) http.HandlerFunc
|
||||
AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc
|
||||
)
|
||||
27
api/server/interface/api/sresponse/result.go
Normal file
27
api/server/interface/api/sresponse/result.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type resultAuth struct {
|
||||
authResponse `json:",inline"`
|
||||
response.Result `json:",inline"`
|
||||
}
|
||||
|
||||
func Success(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, &resultAuth{
|
||||
Result: response.Result{Result: true},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func Failed(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Accepted(logger, &resultAuth{
|
||||
Result: response.Result{Result: false},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
16
api/server/interface/api/sresponse/signup.go
Normal file
16
api/server/interface/api/sresponse/signup.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func SignUp(logger mlogger.Logger, account *model.Account) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&account.AccountBase,
|
||||
)
|
||||
}
|
||||
25
api/server/interface/api/sresponse/statuses.go
Normal file
25
api/server/interface/api/sresponse/statuses.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type statusesResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Statuses []model.Status `json:"statuses"`
|
||||
}
|
||||
|
||||
func Statuses(logger mlogger.Logger, statuses []model.Status, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, statusesResponse{
|
||||
Statuses: statuses,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func Status(logger mlogger.Logger, status *model.Status, accessToken *TokenData) http.HandlerFunc {
|
||||
return Statuses(logger, []model.Status{*status}, accessToken)
|
||||
}
|
||||
8
api/server/interface/api/sresponse/token.go
Normal file
8
api/server/interface/api/sresponse/token.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package sresponse
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenData struct {
|
||||
Token string `json:"token"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
}
|
||||
57
api/server/interface/api/sresponse/ws/response.go
Normal file
57
api/server/interface/api/sresponse/ws/response.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
r "github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/server/interface/api/ws"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
func respond(logger mlogger.Logger, conn *websocket.Conn, messageType, apiStatus, requestID string, data any) {
|
||||
message := ws.Message{
|
||||
BaseResponse: r.BaseResponse{
|
||||
Status: apiStatus,
|
||||
Data: data,
|
||||
},
|
||||
ID: requestID,
|
||||
MessageType: messageType,
|
||||
}
|
||||
|
||||
if err := websocket.JSON.Send(conn, message); err != nil {
|
||||
logger.Warn("Failed to send error message", zap.Error(err), zap.Any("message", message))
|
||||
}
|
||||
}
|
||||
|
||||
func errorf(logger mlogger.Logger, messageType, requestID string, conn *websocket.Conn, resp r.ErrorResponse) {
|
||||
logger.Debug(
|
||||
"Writing error sresponse",
|
||||
zap.String("error", resp.Error),
|
||||
zap.String("details", resp.Details),
|
||||
zap.Int("code", resp.Code),
|
||||
)
|
||||
respond(logger, conn, messageType, api.MSError, requestID, &resp)
|
||||
}
|
||||
|
||||
func Ok(logger mlogger.Logger, requestID string, data any) ws.ResponseHandler {
|
||||
res := func(messageType string, conn *websocket.Conn) {
|
||||
logger.Debug("Successfully executed request", zap.Any("sresponse", data))
|
||||
respond(logger, conn, messageType, api.MSSuccess, requestID, data)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func Internal(logger mlogger.Logger, requestID string, err error) ws.ResponseHandler {
|
||||
res := func(messageType string, conn *websocket.Conn) {
|
||||
errorf(logger, messageType, requestID, conn,
|
||||
r.ErrorResponse{
|
||||
Error: "internal_error",
|
||||
Details: err.Error(),
|
||||
Code: http.StatusInternalServerError,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
9
api/server/interface/api/ws/config.go
Normal file
9
api/server/interface/api/ws/config.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
ac "github.com/tech/sendico/server/internal/api/config"
|
||||
)
|
||||
|
||||
type (
|
||||
Config = ac.WebSocketConfig
|
||||
)
|
||||
12
api/server/interface/api/ws/handler.go
Normal file
12
api/server/interface/api/ws/handler.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
ResponseHandler func(messageType string, conn *websocket.Conn)
|
||||
HandlerFunc func(ctx context.Context, msg Message) ResponseHandler
|
||||
)
|
||||
9
api/server/interface/api/ws/message.go
Normal file
9
api/server/interface/api/ws/message.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import "github.com/tech/sendico/pkg/api/http/response"
|
||||
|
||||
type Message struct {
|
||||
response.BaseResponse
|
||||
ID string `json:"id"`
|
||||
MessageType string `json:"messageType"`
|
||||
}
|
||||
31
api/server/interface/middleware/middleware.go
Normal file
31
api/server/interface/middleware/middleware.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
ai "github.com/tech/sendico/server/internal/api/config"
|
||||
)
|
||||
|
||||
type (
|
||||
TokenConfig = ai.TokenConfig
|
||||
Config = ai.Config
|
||||
Signature = ai.SignatureConf
|
||||
PasswordConfig = ai.PasswordConfig
|
||||
)
|
||||
|
||||
type MapClaims = ai.MapClaims
|
||||
|
||||
func getKey(osEnv string) any {
|
||||
if len(osEnv) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []byte(os.Getenv(osEnv))
|
||||
}
|
||||
|
||||
func SignatureConf(conf *Config) Signature {
|
||||
return Signature{
|
||||
PrivateKey: []byte(os.Getenv(conf.Signature.PrivateKeyEnv)),
|
||||
PublicKey: getKey(conf.Signature.PublicKeyEnv),
|
||||
Algorithm: conf.Signature.Algorithm,
|
||||
}
|
||||
}
|
||||
94
api/server/interface/model/token.go
Normal file
94
api/server/interface/model/token.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
mduration "github.com/tech/sendico/pkg/mutil/duration"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AccountToken struct {
|
||||
AccountRef primitive.ObjectID
|
||||
Login string
|
||||
Name string
|
||||
Locale string
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
func createAccountToken(a *model.Account, expiration int) AccountToken {
|
||||
return AccountToken{
|
||||
AccountRef: *a.GetID(),
|
||||
Login: a.Login,
|
||||
Name: a.Name,
|
||||
Locale: a.Locale,
|
||||
Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
|
||||
}
|
||||
}
|
||||
|
||||
func getTokenParam(claims middleware.MapClaims, param string) (string, error) {
|
||||
id, ok := claims[param].(string)
|
||||
if !ok {
|
||||
return "", merrors.NoData(fmt.Sprintf("param '%s' not found", param))
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const (
|
||||
paramNameID = "id"
|
||||
paramNameName = "name"
|
||||
paramNameLocale = "locale"
|
||||
paramNameLogin = "login"
|
||||
paramNameExpiration = "exp"
|
||||
)
|
||||
|
||||
func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
|
||||
var at AccountToken
|
||||
var err error
|
||||
var account string
|
||||
if account, err = getTokenParam(claims, paramNameID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.AccountRef, err = primitive.ObjectIDFromHex(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Login, err = getTokenParam(claims, paramNameLogin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Name, err = getTokenParam(claims, paramNameName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expValue, ok := claims[paramNameExpiration]; ok {
|
||||
switch exp := expValue.(type) {
|
||||
case time.Time:
|
||||
at.Expiration = exp
|
||||
case float64:
|
||||
at.Expiration = time.Unix(int64(exp), 0)
|
||||
case int64:
|
||||
at.Expiration = time.Unix(exp, 0)
|
||||
default:
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
} else {
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
return &at, nil
|
||||
}
|
||||
|
||||
func Account2Claims(a *model.Account, expiration int) middleware.MapClaims {
|
||||
t := createAccountToken(a, expiration)
|
||||
return middleware.MapClaims{
|
||||
paramNameID: t.AccountRef.Hex(),
|
||||
paramNameLogin: t.Login,
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameExpiration: int64(t.Expiration.Unix()),
|
||||
}
|
||||
}
|
||||
11
api/server/interface/services/account/account.go
Normal file
11
api/server/interface/services/account/account.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/accountapiimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return accountapiimp.CreateAPI(a)
|
||||
}
|
||||
12
api/server/interface/services/fileservice/config/config.go
Normal file
12
api/server/interface/services/fileservice/config/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package fileservice
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type StorageType string
|
||||
|
||||
const (
|
||||
LocalFS StorageType = "local_fs"
|
||||
AwsS3 StorageType = "aws_s3"
|
||||
)
|
||||
|
||||
type Config = model.DriverConfig[StorageType]
|
||||
11
api/server/interface/services/fileservice/fileservice.go
Normal file
11
api/server/interface/services/fileservice/fileservice.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package fileservice
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/fileserviceimp"
|
||||
)
|
||||
|
||||
func CreateAPI(a api.API, directory string) (mservice.MicroService, error) {
|
||||
return fileserviceimp.CreateAPI(a, directory)
|
||||
}
|
||||
11
api/server/interface/services/invitation/invitation.go
Normal file
11
api/server/interface/services/invitation/invitation.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package invitation
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/invitationimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return invitationimp.CreateAPI(a)
|
||||
}
|
||||
11
api/server/interface/services/logo/logo.go
Normal file
11
api/server/interface/services/logo/logo.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package logo
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/logoimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return logoimp.CreateAPI(a)
|
||||
}
|
||||
11
api/server/interface/services/organization/organization.go
Normal file
11
api/server/interface/services/organization/organization.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package organization
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/organizationimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return organizationimp.CreateAPI(a)
|
||||
}
|
||||
11
api/server/interface/services/permission/permission.go
Normal file
11
api/server/interface/services/permission/permission.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package permission
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/permissionsimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return permissionsimp.CreateAPI(a)
|
||||
}
|
||||
Reference in New Issue
Block a user