mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-14 09:02:15 +00:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: deprecate user invite table * fix: handle soft deleted users flow * fix: handle edge cases for authentication and reset password flow * feat: integration tests with fixes for new flow * fix: array for grants * fix: edge cases for reset token and context api * chore: remove all code related to old invite flow * fix: openapi specs * fix: integration tests and minor naming change * fix: integration tests fmtlint * feat: improve invitation email template * fix: role tests * fix: context api * fix: openapi frontend * chore: rename countbyorgid to activecountbyorgid * fix: a deleted user cannot recycled, creating a new one * feat: migrate existing invites to user as pending invite status * fix: error from GetUsersByEmailAndOrgID * feat: add backward compatibility to existing apis using new invite flow * chore: change ordering of apis in server * chore: change ordering of apis in server * fix: filter active users in role and org id check * fix: check deleted user in reset password flow * chore: address some review comments, add back countbyorgid method * chore: move to bulk inserts for migrating existing invites * fix: wrap funcs to transactions, and fix openapi specs * fix: move reset link method to types, also move authz grants outside transation * fix: transaction issues * feat: helper method ErrIfDeleted for user * fix: error code for errifdeleted in user * fix: soft delete store method * fix: password authn tests also add old invite flow test * fix: callbackauthn tests * fix: remove extra oidc tests * fix: callback authn tests oidc * chore: address review comments and optimise bulk invite api * fix: use db ctx in various places * fix: fix duplicate email invite issue and add partial invite * fix: openapi specs * fix: errifpending * fix: user status persistence * fix: edge cases * chore: add tests for partial index too * feat: use composite unique index on users table instead of partial one * chore: move duplicate email check to unmarshaljson and query user again in accept invite * fix: make 068 migratin idempotent * chore: remove unused emails var * chore: add a temp filter to show only active users in frontend until next frontend fix * chore: remove one check from register flow testing until temp code is removed * chore: remove commented code from tests * chore: address frontend review comments * chore: address frontend review comments
278 lines
9.6 KiB
Go
278 lines
9.6 KiB
Go
package types
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
var (
|
|
ErrCodeUserNotFound = errors.MustNewCode("user_not_found")
|
|
ErrCodeAmbiguousUser = errors.MustNewCode("ambiguous_user")
|
|
ErrUserAlreadyExists = errors.MustNewCode("user_already_exists")
|
|
ErrPasswordAlreadyExists = errors.MustNewCode("password_already_exists")
|
|
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
|
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
|
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
|
ErrAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
|
|
ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found")
|
|
ErrCodeRootUserOperationUnsupported = errors.MustNewCode("root_user_operation_unsupported")
|
|
ErrCodeUserStatusDeleted = errors.MustNewCode("user_status_deleted")
|
|
ErrCodeUserStatusPendingInvite = errors.MustNewCode("user_status_pending_invite")
|
|
)
|
|
|
|
var (
|
|
UserStatusPendingInvite = valuer.NewString("pending_invite")
|
|
UserStatusActive = valuer.NewString("active")
|
|
UserStatusDeleted = valuer.NewString("deleted")
|
|
ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted}
|
|
)
|
|
|
|
type GettableUser = User
|
|
|
|
type User struct {
|
|
bun.BaseModel `bun:"table:users"`
|
|
|
|
Identifiable
|
|
DisplayName string `bun:"display_name" json:"displayName"`
|
|
Email valuer.Email `bun:"email" json:"email"`
|
|
Role Role `bun:"role" json:"role"`
|
|
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
|
|
IsRoot bool `bun:"is_root" json:"isRoot"`
|
|
Status valuer.String `bun:"status" json:"status"`
|
|
DeletedAt time.Time `bun:"deleted_at" json:"-"`
|
|
TimeAuditable
|
|
}
|
|
|
|
type PostableRegisterOrgAndAdmin struct {
|
|
Name string `json:"name"`
|
|
Email valuer.Email `json:"email"`
|
|
Password string `json:"password"`
|
|
OrgDisplayName string `json:"orgDisplayName"`
|
|
OrgName string `json:"orgName"`
|
|
}
|
|
|
|
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, error) {
|
|
if email.IsZero() {
|
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
|
}
|
|
|
|
if role == "" {
|
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required")
|
|
}
|
|
|
|
if orgID.IsZero() {
|
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
|
|
}
|
|
|
|
if !slices.Contains(ValidUserStatus, status) {
|
|
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid status: %s, allowed status are: %v", status, ValidUserStatus)
|
|
}
|
|
|
|
return &User{
|
|
Identifiable: Identifiable{
|
|
ID: valuer.GenerateUUID(),
|
|
},
|
|
DisplayName: displayName,
|
|
Email: email,
|
|
Role: role,
|
|
OrgID: orgID,
|
|
IsRoot: false,
|
|
Status: status,
|
|
TimeAuditable: TimeAuditable{
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*User, error) {
|
|
if email.IsZero() {
|
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
|
}
|
|
|
|
if orgID.IsZero() {
|
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
|
|
}
|
|
|
|
return &User{
|
|
Identifiable: Identifiable{
|
|
ID: valuer.GenerateUUID(),
|
|
},
|
|
DisplayName: displayName,
|
|
Email: email,
|
|
Role: RoleAdmin,
|
|
OrgID: orgID,
|
|
IsRoot: true,
|
|
Status: UserStatusActive,
|
|
TimeAuditable: TimeAuditable{
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Update applies mutable fields from the input to the user. Immutable fields
|
|
// (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied.
|
|
func (u *User) Update(displayName string, role Role) {
|
|
if displayName != "" {
|
|
u.DisplayName = displayName
|
|
}
|
|
if role != "" {
|
|
u.Role = role
|
|
}
|
|
u.UpdatedAt = time.Now()
|
|
}
|
|
|
|
func (u *User) UpdateStatus(status valuer.String) error {
|
|
// no updates allowed if user is in delete state
|
|
if err := u.ErrIfDeleted(); err != nil {
|
|
return errors.WithAdditionalf(err, "cannot update status of a deleted user")
|
|
}
|
|
|
|
// not udpates allowed from active to pending state
|
|
if status == UserStatusPendingInvite && u.Status == UserStatusActive {
|
|
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cannot move user to pending state from active state")
|
|
}
|
|
|
|
u.Status = status
|
|
u.UpdatedAt = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// PromoteToRoot promotes the user to a root user with admin role.
|
|
func (u *User) PromoteToRoot() {
|
|
u.IsRoot = true
|
|
u.Role = RoleAdmin
|
|
u.UpdatedAt = time.Now()
|
|
}
|
|
|
|
// UpdateEmail updates the email of the user.
|
|
func (u *User) UpdateEmail(email valuer.Email) {
|
|
u.Email = email
|
|
u.UpdatedAt = time.Now()
|
|
}
|
|
|
|
// ErrIfRoot returns an error if the user is a root user. The caller should
|
|
// enrich the error with the specific operation using errors.WithAdditionalf.
|
|
func (u *User) ErrIfRoot() error {
|
|
if u.IsRoot {
|
|
return errors.New(errors.TypeUnsupported, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ErrIfDeleted returns an error if the user is in deleted state.
|
|
// This error can be enriched with specific operation by the called using errors.WithAdditionalf
|
|
func (u *User) ErrIfDeleted() error {
|
|
if u.Status == UserStatusDeleted {
|
|
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusDeleted, "unsupported operation for deleted user")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ErrIfPending returns an error if the user is in pending invite state.
|
|
// This error can be enriched with specific operation by the called using errors.WithAdditionalf
|
|
func (u *User) ErrIfPending() error {
|
|
if u.Status == UserStatusPendingInvite {
|
|
return errors.New(errors.TypeUnsupported, ErrCodeUserStatusPendingInvite, "unsupported operation for pending user")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewTraitsFromUser(user *User) map[string]any {
|
|
return map[string]any{
|
|
"name": user.DisplayName,
|
|
"role": user.Role,
|
|
"email": user.Email.String(),
|
|
"display_name": user.DisplayName,
|
|
"status": user.Status,
|
|
"created_at": user.CreatedAt,
|
|
}
|
|
}
|
|
|
|
func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
|
|
type Alias PostableRegisterOrgAndAdmin
|
|
|
|
var temp Alias
|
|
if err := json.Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !IsPasswordValid(temp.Password) {
|
|
return ErrInvalidPassword
|
|
}
|
|
|
|
*request = PostableRegisterOrgAndAdmin(temp)
|
|
return nil
|
|
}
|
|
|
|
type UserStore interface {
|
|
// Creates a user.
|
|
CreateUser(ctx context.Context, user *User) error
|
|
|
|
// Get user by id.
|
|
GetUser(context.Context, valuer.UUID) (*User, error)
|
|
|
|
// Get user by orgID and id.
|
|
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*User, error)
|
|
|
|
// Get user by email and orgID.
|
|
GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*User, error)
|
|
|
|
// Get users by email.
|
|
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*User, error)
|
|
|
|
// Get users by role and org.
|
|
GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*User, error)
|
|
|
|
// List users by org.
|
|
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*User, error)
|
|
|
|
// List users by email and org ids.
|
|
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
|
|
|
|
// Get users for an org id using emails and statuses
|
|
GetUsersByEmailsOrgIDAndStatuses(context.Context, valuer.UUID, []string, []string) ([]*User, error)
|
|
|
|
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
|
|
DeleteUser(ctx context.Context, orgID string, id string) error
|
|
SoftDeleteUser(ctx context.Context, orgID string, id string) error
|
|
|
|
// Creates a password.
|
|
CreatePassword(ctx context.Context, password *FactorPassword) error
|
|
CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *ResetPasswordToken) error
|
|
GetPassword(ctx context.Context, id valuer.UUID) (*FactorPassword, error)
|
|
GetPasswordByUserID(ctx context.Context, userID valuer.UUID) (*FactorPassword, error)
|
|
GetResetPasswordToken(ctx context.Context, token string) (*ResetPasswordToken, error)
|
|
GetResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) (*ResetPasswordToken, error)
|
|
DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error
|
|
UpdatePassword(ctx context.Context, password *FactorPassword) error
|
|
|
|
// API KEY
|
|
CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error
|
|
UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error
|
|
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error)
|
|
RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error
|
|
GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error)
|
|
CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error)
|
|
|
|
CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error)
|
|
CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error)
|
|
|
|
// Get root user by org.
|
|
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*User, error)
|
|
|
|
// Get user by reset password token
|
|
GetUserByResetPasswordToken(ctx context.Context, token string) (*User, error)
|
|
|
|
// Transaction
|
|
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
|
|
}
|