@@ -181,176 +181,24 @@ func (s AccountService) VerifyEmail(ctx context.Context, token string) error {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *AccountService) AcceptInvitation(
|
||||
ctx context.Context,
|
||||
identityID gid.GID,
|
||||
invitationID gid.GID,
|
||||
) (*coredata.Invitation, *coredata.Membership, error) {
|
||||
var (
|
||||
now = time.Now()
|
||||
profile = &coredata.MembershipProfile{}
|
||||
membership = &coredata.Membership{}
|
||||
invitation = &coredata.Invitation{}
|
||||
)
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(tx pg.Conn) error {
|
||||
identity := coredata.Identity{}
|
||||
|
||||
if err := identity.LoadByID(ctx, tx, identityID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewIdentityNotFoundError(identityID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
if err := invitation.LoadByID(ctx, tx, coredata.NewNoScope(), invitationID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewInvitationNotFoundError(invitationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load invitation: %w", err)
|
||||
}
|
||||
|
||||
if invitation.Email != identity.EmailAddress {
|
||||
return NewInvitationNotFoundError(invitationID)
|
||||
}
|
||||
|
||||
if invitation.AcceptedAt != nil {
|
||||
return NewInvitationAlreadyAcceptedError(invitationID)
|
||||
}
|
||||
|
||||
if invitation.ExpiresAt.Before(now) {
|
||||
return NewInvitationExpiredError(invitationID)
|
||||
}
|
||||
|
||||
tenantID := invitation.OrganizationID.TenantID()
|
||||
scope := coredata.NewScope(invitation.OrganizationID.TenantID())
|
||||
|
||||
existingProfile := &coredata.MembershipProfile{}
|
||||
if err := existingProfile.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identityID,
|
||||
invitation.OrganizationID,
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load existing profile: %w", err)
|
||||
}
|
||||
|
||||
profile = &coredata.MembershipProfile{
|
||||
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: invitation.OrganizationID,
|
||||
Source: coredata.ProfileSourceManual,
|
||||
State: coredata.ProfileStateActive,
|
||||
FullName: identity.FullName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := profile.Insert(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot insert profile: %w", err)
|
||||
}
|
||||
} else {
|
||||
if existingProfile.State == coredata.ProfileStateInactive {
|
||||
existingProfile.State = coredata.ProfileStateActive
|
||||
|
||||
if err := existingProfile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot reactivate profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
profile = existingProfile
|
||||
}
|
||||
|
||||
existingMembership := &coredata.Membership{}
|
||||
if err := existingMembership.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identityID,
|
||||
invitation.OrganizationID,
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load existing membership: %w", err)
|
||||
}
|
||||
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(tenantID, coredata.MembershipEntityType),
|
||||
IdentityID: identityID,
|
||||
OrganizationID: invitation.OrganizationID,
|
||||
Role: invitation.Role,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := membership.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot create membership: %w", err)
|
||||
}
|
||||
} else {
|
||||
existingMembership.Role = invitation.Role
|
||||
existingMembership.UpdatedAt = now
|
||||
|
||||
if err := existingMembership.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot assign membership role: %w", err)
|
||||
}
|
||||
|
||||
membership = existingMembership
|
||||
}
|
||||
|
||||
invitation.AcceptedAt = &now
|
||||
if err := invitation.Update(ctx, tx, scope); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewInvitationNotFoundError(invitationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot update invitation: %w", err)
|
||||
}
|
||||
|
||||
// Expire other pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(invitation.OrganizationID),
|
||||
invitation.Email,
|
||||
invitation.OrganizationID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return invitation, membership, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) ListPendingInvitations(
|
||||
ctx context.Context,
|
||||
identityID gid.GID,
|
||||
userID gid.GID,
|
||||
cursor *page.Cursor[coredata.InvitationOrderField],
|
||||
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
|
||||
var invitations coredata.Invitations
|
||||
var (
|
||||
scope = coredata.NewScopeFromObjectID(userID)
|
||||
invitations coredata.Invitations
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
identity := coredata.Identity{}
|
||||
err := identity.LoadByID(ctx, conn, identityID)
|
||||
profile := coredata.MembershipProfile{}
|
||||
err := profile.LoadByID(ctx, conn, scope, userID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewIdentityNotFoundError(identityID)
|
||||
return NewIdentityNotFoundError(userID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
@@ -358,7 +206,7 @@ func (s *AccountService) ListPendingInvitations(
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
|
||||
err = invitations.LoadByIdentityID(ctx, conn, coredata.NewNoScope(), identity.EmailAddress, cursor, onlyPending)
|
||||
err = invitations.LoadByUserID(ctx, conn, scope, userID, cursor, onlyPending)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load invitations: %w", err)
|
||||
}
|
||||
@@ -374,39 +222,6 @@ func (s *AccountService) ListPendingInvitations(
|
||||
return page.NewPage(invitations, cursor), nil
|
||||
}
|
||||
|
||||
func (s *AccountService) CountPendingInvitations(
|
||||
ctx context.Context,
|
||||
identityID gid.GID,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
identity := coredata.Identity{}
|
||||
err := identity.LoadByID(ctx, conn, identityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
invitations := coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
|
||||
count, err = invitations.CountByEmail(ctx, conn, identity.EmailAddress, onlyPending)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count pending invitations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, req *ChangePasswordRequest) error {
|
||||
if err := req.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
|
||||
@@ -48,7 +48,6 @@ type (
|
||||
CreateIdentityFromInvitationRequest struct {
|
||||
InvitationToken string
|
||||
Password string
|
||||
FullName string
|
||||
}
|
||||
|
||||
LoadOrCreateIdentityRequest struct {
|
||||
@@ -93,7 +92,6 @@ func (req CreateIdentityFromInvitationRequest) Validate() error {
|
||||
v := validator.New()
|
||||
|
||||
v.Check(req.InvitationToken, "invitationToken", validator.NotEmpty())
|
||||
v.Check(req.FullName, "fullName", validator.NotEmpty(), validator.MinLen(1), validator.MaxLen(255))
|
||||
v.Check(req.Password, "password", PasswordValidator())
|
||||
|
||||
return v.Error()
|
||||
@@ -134,10 +132,10 @@ func (req CreateIdentityWithPasswordRequest) Validate() error {
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
func (s *AuthService) CreateIdentityFromInvitation(
|
||||
func (s *AuthService) ActivateAccount(
|
||||
ctx context.Context,
|
||||
req *CreateIdentityFromInvitationRequest,
|
||||
) (*coredata.Identity, *coredata.Session, error) {
|
||||
) (*coredata.MembershipProfile, *coredata.Session, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
@@ -150,8 +148,8 @@ func (s *AuthService) CreateIdentityFromInvitation(
|
||||
var (
|
||||
scope = coredata.NewScopeFromObjectID(payload.Data.InvitationID)
|
||||
invitation = &coredata.Invitation{}
|
||||
identity = &coredata.Identity{}
|
||||
session = &coredata.Session{}
|
||||
profile *coredata.MembershipProfile
|
||||
session *coredata.Session
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
@@ -180,23 +178,54 @@ func (s *AuthService) CreateIdentityFromInvitation(
|
||||
return NewInvitationExpiredError(payload.Data.InvitationID)
|
||||
}
|
||||
|
||||
identity = &coredata.Identity{
|
||||
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
|
||||
EmailAddress: invitation.Email,
|
||||
FullName: invitation.FullName,
|
||||
HashedPassword: hashedPassword,
|
||||
EmailAddressVerified: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
profile = &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByID(ctx, tx, scope, invitation.UserID); err != nil {
|
||||
return fmt.Errorf("cannot load user: %w", err)
|
||||
}
|
||||
|
||||
err = identity.Insert(ctx, tx)
|
||||
if profile.State == coredata.ProfileStateInactive {
|
||||
profile.State = coredata.ProfileStateActive
|
||||
profile.UpdatedAt = now
|
||||
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
identity := &coredata.Identity{}
|
||||
if err := identity.LoadByID(ctx, tx, profile.IdentityID); err != nil {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
identity.HashedPassword = hashedPassword
|
||||
identity.EmailAddressVerified = true
|
||||
identity.UpdatedAt = now
|
||||
|
||||
err = identity.Update(ctx, tx)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceAlreadyExists {
|
||||
return NewIdentityAlreadyExistsError(invitation.Email)
|
||||
return fmt.Errorf("cannot update identity: %w", err)
|
||||
}
|
||||
|
||||
invitation.AcceptedAt = &now
|
||||
if err := invitation.Update(ctx, tx, scope); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewInvitationNotFoundError(payload.Data.InvitationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot insert identity: %w", err)
|
||||
return fmt.Errorf("cannot update invitation: %w", err)
|
||||
}
|
||||
|
||||
// Expire other pending invitations for user
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(invitation.OrganizationID),
|
||||
invitation.UserID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations: %w", err)
|
||||
}
|
||||
|
||||
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration)
|
||||
@@ -213,7 +242,7 @@ func (s *AuthService) CreateIdentityFromInvitation(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return identity, session, nil
|
||||
return profile, session, nil
|
||||
}
|
||||
|
||||
func (s AuthService) ResetPassword(
|
||||
|
||||
@@ -333,99 +333,6 @@ func (s *OrganizationService) RemoveUser(
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OrganizationService) DeleteInvitation(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
invitationID gid.GID,
|
||||
) error {
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
func(tx pg.Conn) error {
|
||||
invitation := coredata.Invitation{}
|
||||
err := invitation.LoadByID(ctx, tx, scope, invitationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewInvitationNotFoundError(invitationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load invitation: %w", err)
|
||||
}
|
||||
|
||||
switch invitation.Status {
|
||||
case coredata.InvitationStatusAccepted:
|
||||
return NewInvitationNotDeletedError(invitationID, invitation.Status.String())
|
||||
case coredata.InvitationStatusPending, coredata.InvitationStatusExpired:
|
||||
}
|
||||
|
||||
err = invitation.Delete(ctx, tx, scope, invitationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete invitation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OrganizationService) ListInvitations(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[coredata.InvitationOrderField],
|
||||
filter *coredata.InvitationFilter,
|
||||
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
|
||||
var (
|
||||
invitations coredata.Invitations
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
err := invitations.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load invitations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page.NewPage(invitations, cursor), nil
|
||||
}
|
||||
|
||||
func (s *OrganizationService) CountInvitations(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
filter *coredata.InvitationFilter,
|
||||
) (int, error) {
|
||||
var (
|
||||
count int
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) (err error) {
|
||||
invitations := coredata.Invitations{}
|
||||
count, err = invitations.CountByOrganizationID(ctx, conn, scope, organizationID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count invitations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *OrganizationService) InviteUser(
|
||||
ctx context.Context,
|
||||
req *CreateInvitationRequest,
|
||||
@@ -436,6 +343,7 @@ func (s *OrganizationService) InviteUser(
|
||||
invitation = &coredata.Invitation{
|
||||
ID: gid.New(req.OrganizationID.TenantID(), coredata.InvitationEntityType),
|
||||
OrganizationID: req.OrganizationID,
|
||||
UserID: req.ProfileID,
|
||||
Status: coredata.InvitationStatusPending,
|
||||
ExpiresAt: now.Add(s.invitationTokenValidity),
|
||||
CreatedAt: now,
|
||||
@@ -483,7 +391,7 @@ func (s *OrganizationService) InviteUser(
|
||||
|
||||
subject, textBody, htmlBody, err := emailPresenter.RenderInvitation(
|
||||
ctx,
|
||||
"/auth/signup-from-invitation",
|
||||
"/auth/activate-account",
|
||||
invitationToken,
|
||||
organization.Name,
|
||||
)
|
||||
|
||||
@@ -352,20 +352,6 @@ func (s *Service) HandleAssertion(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership: %w", err)
|
||||
}
|
||||
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
email,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if profile.Source != coredata.ProfileSourceSCIM {
|
||||
|
||||
@@ -235,22 +235,6 @@ func (s *Service) CreateUser(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership: %w", err)
|
||||
}
|
||||
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
emailAddr,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
@@ -476,33 +460,6 @@ func (s *Service) updateUser(
|
||||
|
||||
if shouldReactivate {
|
||||
membership.Role = coredata.MembershipRoleEmployee
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
identity.EmailAddress,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email: %w", err)
|
||||
}
|
||||
} else if shouldDeactivate {
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
identity.EmailAddress,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,24 +509,6 @@ func (s *Service) DeleteUser(
|
||||
return fmt.Errorf("cannot delete membership: %w", err)
|
||||
}
|
||||
|
||||
// Expire all pending invitations for email in organization
|
||||
identity := &coredata.Identity{}
|
||||
if err := identity.LoadByID(ctx, tx, membership.IdentityID); err != nil {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
identity.EmailAddress,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user