639 lines
22 KiB
Go
639 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/pkg/crypto"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// ProfileService implements business logic for user profile operations.
|
|
// Reference: Chatwoot app/controllers/api/v1/profile_controller.rb
|
|
type ProfileService struct {
|
|
userRepo *repository.UserRepo
|
|
accountUserRepo *repository.AccountUserRepo
|
|
accessTokenRepo *repository.AccessTokenRepo
|
|
installationConfigRepo *repository.InstallationConfigRepo
|
|
confirmationMailer ProfileConfirmationMailer
|
|
}
|
|
|
|
// NewProfileService creates a new Profile service.
|
|
func NewProfileService(userRepo *repository.UserRepo, accountUserRepo *repository.AccountUserRepo, extras ...any) *ProfileService {
|
|
svc := &ProfileService{userRepo: userRepo, accountUserRepo: accountUserRepo}
|
|
for _, extra := range extras {
|
|
switch repo := extra.(type) {
|
|
case *repository.AccessTokenRepo:
|
|
svc.accessTokenRepo = repo
|
|
case *repository.InstallationConfigRepo:
|
|
svc.installationConfigRepo = repo
|
|
case ProfileConfirmationMailer:
|
|
svc.confirmationMailer = repo
|
|
}
|
|
}
|
|
return svc
|
|
}
|
|
|
|
func (s *ProfileService) SetConfirmationMailer(mailer ProfileConfirmationMailer) {
|
|
s.confirmationMailer = mailer
|
|
}
|
|
|
|
// ProfileUserResponse matches Chatwoot app/views/api/v1/models/_user.json.jbuilder.
|
|
type ProfileUserResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
AccountID *uint `json:"account_id"`
|
|
AvailableName string `json:"available_name"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
Confirmed bool `json:"confirmed"`
|
|
DisplayName string `json:"display_name"`
|
|
MessageSignature string `json:"message_signature"`
|
|
Email string `json:"email"`
|
|
HMACIdentifier string `json:"hmac_identifier,omitempty"`
|
|
ID uint `json:"id"`
|
|
InviterID *uint `json:"inviter_id"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
PubsubToken string `json:"pubsub_token"`
|
|
CustomAttributes map[string]any `json:"custom_attributes,omitempty"`
|
|
Role string `json:"role"`
|
|
UISettings map[string]any `json:"ui_settings"`
|
|
UID string `json:"uid"`
|
|
Type string `json:"type"`
|
|
Accounts []ProfileAccountResponse `json:"accounts"`
|
|
}
|
|
|
|
// ProfileAccountResponse is the nested account_user entry rendered by Chatwoot's user serializer.
|
|
type ProfileAccountResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
OnboardingStep string `json:"onboarding_step"`
|
|
ActiveAt *string `json:"active_at"`
|
|
Role string `json:"role"`
|
|
Permissions []string `json:"permissions"`
|
|
Availability string `json:"availability"`
|
|
AvailabilityStatus string `json:"availability_status"`
|
|
AutoOffline bool `json:"auto_offline"`
|
|
CustomRoleID *uint `json:"custom_role_id"`
|
|
CustomRole any `json:"custom_role"`
|
|
}
|
|
|
|
// UpdateProfileRequest is the DTO for updating user profile.
|
|
// Reference: Chatwoot profiles_controller#update — params wrapped in "profile" key
|
|
type UpdateProfileRequest struct {
|
|
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
|
|
Email string `json:"email,omitempty" validate:"omitempty,email"`
|
|
DisplayName *string `json:"display_name,omitempty"`
|
|
AvatarURL string `json:"avatar_url,omitempty"`
|
|
MessageSignature *string `json:"message_signature,omitempty"`
|
|
AccountID uint `json:"account_id,omitempty"`
|
|
UISettings map[string]any `json:"ui_settings,omitempty"`
|
|
PhoneNumber *string `json:"phone_number,omitempty"`
|
|
Availability string `json:"availability,omitempty" validate:"omitempty,oneof=online offline busy"`
|
|
CurrentPassword string `json:"current_password,omitempty"`
|
|
Password string `json:"password,omitempty" validate:"omitempty,min=6"`
|
|
PasswordConfirmation string `json:"password_confirmation,omitempty"`
|
|
}
|
|
|
|
// ProfileUpdatePayload wraps UpdateProfileRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:name, :email, ...)
|
|
type ProfileUpdatePayload struct {
|
|
Profile UpdateProfileRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// UpdateAvatarRequest is the DTO for updating user avatar.
|
|
type UpdateAvatarRequest struct {
|
|
AvatarURL string `json:"avatar_url" validate:"required"`
|
|
}
|
|
|
|
// AvailabilityRequest is the DTO for updating user availability per account.
|
|
// Reference: Chatwoot profiles_controller#availability — params wrapped in "profile" key
|
|
type AvailabilityRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
Availability string `json:"availability" validate:"required,oneof=online offline busy"`
|
|
}
|
|
|
|
// ProfileAvailabilityPayload wraps AvailabilityRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id, :availability)
|
|
type ProfileAvailabilityPayload struct {
|
|
Profile AvailabilityRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// AutoOfflineRequest is the DTO for updating auto_offline setting per account.
|
|
// Reference: Chatwoot profiles_controller#auto_offline — params wrapped in "profile" key
|
|
type AutoOfflineRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
AutoOffline bool `json:"auto_offline"`
|
|
}
|
|
|
|
// ProfileAutoOfflinePayload wraps AutoOfflineRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id, :auto_offline)
|
|
type ProfileAutoOfflinePayload struct {
|
|
Profile AutoOfflineRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// SetActiveAccountRequest is the DTO for setting the active account.
|
|
// Reference: Chatwoot profiles_controller#set_active_account — params wrapped in "profile" key
|
|
type SetActiveAccountRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
}
|
|
|
|
// ProfileSetActiveAccountPayload wraps SetActiveAccountRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id)
|
|
type ProfileSetActiveAccountPayload struct {
|
|
Profile SetActiveAccountRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// Get retrieves the current user's profile.
|
|
func (s *ProfileService) Get(ctx context.Context, userID uint, accountID uint) (*ProfileUserResponse, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, accountID)
|
|
}
|
|
|
|
// Update updates the current user's profile.
|
|
func (s *ProfileService) Update(ctx context.Context, userID uint, accountID uint, req UpdateProfileRequest) (*ProfileUserResponse, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
if req.Name != "" {
|
|
user.Name = req.Name
|
|
}
|
|
if req.Email != "" {
|
|
user.Email = req.Email
|
|
}
|
|
if req.DisplayName != nil {
|
|
user.DisplayName = *req.DisplayName
|
|
}
|
|
if req.AvatarURL != "" {
|
|
user.AvatarURL = req.AvatarURL
|
|
}
|
|
if req.MessageSignature != nil {
|
|
user.MessageSignature = *req.MessageSignature
|
|
}
|
|
if req.UISettings != nil {
|
|
encoded, err := json.Marshal(req.UISettings)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid ui_settings: %w", err)
|
|
}
|
|
user.UISettings = datatypes.JSON(encoded)
|
|
}
|
|
if req.PhoneNumber != nil {
|
|
attrs := jsonObject(user.CustomAttributes)
|
|
attrs["phone_number"] = *req.PhoneNumber
|
|
encoded, err := json.Marshal(attrs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid custom attributes: %w", err)
|
|
}
|
|
user.CustomAttributes = datatypes.JSON(encoded)
|
|
}
|
|
if req.Password != "" {
|
|
if req.Password != req.PasswordConfirmation {
|
|
return nil, fmt.Errorf("invalid password confirmation")
|
|
}
|
|
if !crypto.CheckPassword(req.CurrentPassword, user.PasswordDigest) && !crypto.CheckPassword(req.CurrentPassword, user.Password) {
|
|
return nil, fmt.Errorf("invalid current password")
|
|
}
|
|
passwordDigest, err := crypto.HashPassword(req.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
user.PasswordDigest = passwordDigest
|
|
user.Password = passwordDigest
|
|
}
|
|
// Map availability to user.Available boolean
|
|
if req.Availability == "online" {
|
|
user.Available = true
|
|
} else if req.Availability == "offline" || req.Availability == "busy" {
|
|
user.Available = false
|
|
}
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
applogger.L().Errorf("failed to update profile: %v", err)
|
|
return nil, fmt.Errorf("failed to update profile: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, accountID)
|
|
}
|
|
|
|
// UpdateAvatar updates the current user's avatar URL.
|
|
func (s *ProfileService) UpdateAvatar(ctx context.Context, userID uint, accountID uint, req UpdateAvatarRequest) (*ProfileUserResponse, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
user.AvatarURL = req.AvatarURL
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
applogger.L().Errorf("failed to update avatar: %v", err)
|
|
return nil, fmt.Errorf("failed to update avatar: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, accountID)
|
|
}
|
|
|
|
// SetAvailability updates the user's availability status for a specific account.
|
|
// Reference: Chatwoot profiles_controller#availability — POST /api/v1/profile/availability
|
|
func (s *ProfileService) SetAvailability(ctx context.Context, userID uint, req AvailabilityRequest) (*ProfileUserResponse, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.accountUserRepo.UpdateAvailability(ctx, req.AccountID, userID, req.Availability); err != nil {
|
|
return nil, fmt.Errorf("failed to update availability: %w", err)
|
|
}
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, req.AccountID)
|
|
}
|
|
|
|
// SetAutoOffline updates the user's auto_offline setting for a specific account.
|
|
// Reference: Chatwoot profiles_controller#auto_offline — POST /api/v1/profile/auto_offline
|
|
func (s *ProfileService) SetAutoOffline(ctx context.Context, userID uint, req AutoOfflineRequest) (*ProfileUserResponse, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.accountUserRepo.UpdateAutoOffline(ctx, req.AccountID, userID, req.AutoOffline); err != nil {
|
|
return nil, fmt.Errorf("failed to update auto_offline: %w", err)
|
|
}
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, req.AccountID)
|
|
}
|
|
|
|
// SetActiveAccount updates the active_at timestamp for the user's account membership.
|
|
// Reference: Chatwoot profiles_controller#set_active_account — PUT /api/v1/profile/set_active_account
|
|
func (s *ProfileService) SetActiveAccount(ctx context.Context, userID uint, req SetActiveAccountRequest) error {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return err
|
|
}
|
|
if err := s.accountUserRepo.UpdateActiveAt(ctx, req.AccountID, userID); err != nil {
|
|
return fmt.Errorf("failed to set active account: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResendConfirmation sends a confirmation email to the user if not yet confirmed.
|
|
// Reference: Chatwoot auth/resend_confirmations_controller#create — POST /api/v1/resend_confirmation
|
|
func (s *ProfileService) ResendConfirmation(ctx context.Context, userID uint) error {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return fmt.Errorf("user not found: %w", err)
|
|
}
|
|
if user.ConfirmedAt != nil {
|
|
// Already confirmed, do nothing (per chatwoot: skip if confirmed)
|
|
return nil
|
|
}
|
|
return s.sendConfirmationInstructions(ctx, user)
|
|
}
|
|
|
|
func (s *ProfileService) sendConfirmationInstructions(ctx context.Context, user *model.User) error {
|
|
account, inviter, err := s.confirmationAccountContext(ctx, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
confirmationToken := ""
|
|
resetPasswordToken := ""
|
|
now := time.Now().UTC()
|
|
if inviter != nil && strings.TrimSpace(user.UnconfirmedEmail) == "" {
|
|
resetPasswordToken, err = generateAuthToken()
|
|
if err != nil {
|
|
return fmt.Errorf("generate reset password token: %w", err)
|
|
}
|
|
user.ResetPasswordToken = digestAuthToken(resetPasswordToken)
|
|
user.ResetPasswordSentAt = &now
|
|
} else {
|
|
confirmationToken, err = generateAuthToken()
|
|
if err != nil {
|
|
return fmt.Errorf("generate confirmation token: %w", err)
|
|
}
|
|
user.ConfirmationToken = confirmationToken
|
|
user.ConfirmationSentAt = &now
|
|
}
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
return fmt.Errorf("persist confirmation tokens: %w", err)
|
|
}
|
|
|
|
req := buildProfileConfirmationMailRequest(user, account, inviter, s.confirmationBrandName(ctx), envConfirmationFrontendURL(), confirmationToken, resetPasswordToken)
|
|
if s.confirmationMailer == nil {
|
|
applogger.L().Infof("confirmation mailer not configured for user %d (%s)", user.ID, user.Email)
|
|
return nil
|
|
}
|
|
return s.confirmationMailer.SendConfirmationInstructions(ctx, req)
|
|
}
|
|
|
|
func (s *ProfileService) confirmationAccountContext(ctx context.Context, user *model.User) (*model.Account, *model.User, error) {
|
|
if s.accountUserRepo == nil || user == nil {
|
|
return nil, nil, nil
|
|
}
|
|
accountUsers, err := s.accountUserRepo.FindByUserWithAccounts(ctx, user.ID)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("load account memberships: %w", err)
|
|
}
|
|
if len(accountUsers) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
accountUser := accountUsers[0]
|
|
var inviter *model.User
|
|
if accountUser.InvitedBy != 0 && strings.TrimSpace(user.UnconfirmedEmail) == "" {
|
|
inviter, err = s.userRepo.FindByID(ctx, accountUser.InvitedBy)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("load inviter: %w", err)
|
|
}
|
|
}
|
|
return &accountUser.Account, inviter, nil
|
|
}
|
|
|
|
func (s *ProfileService) confirmationBrandName(ctx context.Context) string {
|
|
if s.installationConfigRepo == nil {
|
|
return "Chatwoot"
|
|
}
|
|
cfg, err := s.installationConfigRepo.FindByName(ctx, "BRAND_NAME")
|
|
if err != nil {
|
|
return "Chatwoot"
|
|
}
|
|
return cfg.Value
|
|
}
|
|
|
|
// ResetAccessToken regenerates the user's access token.
|
|
// Reference: Chatwoot profiles_controller#reset_access_token — POST /api/v1/profile/reset_access_token
|
|
// In chatwoot this regenerates the Doorkeeper OAuth token. In gochat with JWT auth,
|
|
// the "reset" means the current JWT is invalidated and a new one must be obtained.
|
|
func (s *ProfileService) ResetAccessToken(ctx context.Context, userID uint, accountID uint) (*ProfileUserResponse, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
if _, err := s.regenerateAccessToken(ctx, user.ID); err != nil {
|
|
return nil, fmt.Errorf("failed to reset access token: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, accountID)
|
|
}
|
|
|
|
// DeleteAvatar removes the user's avatar image.
|
|
// Reference: Chatwoot ProfilesController#destroy_avatar (DELETE :avatar on: :collection)
|
|
func (s *ProfileService) DeleteAvatar(ctx context.Context, userID uint, accountID uint) (*ProfileUserResponse, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
user.AvatarURL = ""
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
return nil, fmt.Errorf("failed to remove avatar: %w", err)
|
|
}
|
|
return s.serializeUser(ctx, user, accountID)
|
|
}
|
|
|
|
func (s *ProfileService) serializeUser(ctx context.Context, user *model.User, activeAccountID uint) (*ProfileUserResponse, error) {
|
|
accountUsers, err := s.accountUserRepo.FindByUserWithAccounts(ctx, user.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load account memberships: %w", err)
|
|
}
|
|
|
|
active := selectActiveAccountUser(accountUsers, activeAccountID)
|
|
accounts := make([]ProfileAccountResponse, 0, len(accountUsers))
|
|
for _, accountUser := range accountUsers {
|
|
accounts = append(accounts, profileAccountResponse(accountUser))
|
|
}
|
|
|
|
accessToken, err := s.currentAccessToken(ctx, user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hmacIdentifier, err := s.hmacIdentifier(ctx, user.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
displayName := user.DisplayName
|
|
availableName := user.Name
|
|
if displayName != "" {
|
|
availableName = displayName
|
|
}
|
|
|
|
userType := user.Type
|
|
if userType == "" {
|
|
userType = "User"
|
|
}
|
|
|
|
var accountID *uint
|
|
var inviterID *uint
|
|
role := ""
|
|
if active != nil {
|
|
id := active.AccountID
|
|
accountID = &id
|
|
role = active.Role
|
|
if active.InvitedBy != 0 {
|
|
inviter := active.InvitedBy
|
|
inviterID = &inviter
|
|
}
|
|
}
|
|
|
|
return &ProfileUserResponse{
|
|
AccessToken: accessToken,
|
|
AccountID: accountID,
|
|
AvailableName: availableName,
|
|
AvatarURL: user.AvatarURL,
|
|
Confirmed: user.ConfirmedAt != nil,
|
|
DisplayName: displayName,
|
|
MessageSignature: user.MessageSignature,
|
|
Email: user.Email,
|
|
HMACIdentifier: hmacIdentifier,
|
|
ID: user.ID,
|
|
InviterID: inviterID,
|
|
Name: user.Name,
|
|
Provider: defaultString(user.Provider, "email"),
|
|
PubsubToken: user.PubsubToken,
|
|
CustomAttributes: jsonObject(user.CustomAttributes),
|
|
Role: role,
|
|
UISettings: jsonObject(user.UISettings),
|
|
UID: user.UID,
|
|
Type: userType,
|
|
Accounts: accounts,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ProfileService) hmacIdentifier(ctx context.Context, email string) (string, error) {
|
|
if s.installationConfigRepo == nil {
|
|
return "", nil
|
|
}
|
|
cfg, err := s.installationConfigRepo.FindByName(ctx, "CHATWOOT_INBOX_HMAC_KEY")
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
if cfg.Value == "" {
|
|
return "", nil
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(cfg.Value))
|
|
_, _ = mac.Write([]byte(email))
|
|
return hex.EncodeToString(mac.Sum(nil)), nil
|
|
}
|
|
|
|
func selectActiveAccountUser(accountUsers []model.AccountUser, accountID uint) *model.AccountUser {
|
|
if len(accountUsers) == 0 {
|
|
return nil
|
|
}
|
|
if accountID != 0 {
|
|
for i := range accountUsers {
|
|
if accountUsers[i].AccountID == accountID {
|
|
return &accountUsers[i]
|
|
}
|
|
}
|
|
}
|
|
return &accountUsers[0]
|
|
}
|
|
|
|
func profileAccountResponse(accountUser model.AccountUser) ProfileAccountResponse {
|
|
activeAt := timeStringPtr(accountUser.ActiveAt)
|
|
availability := defaultString(accountUser.Availability, "offline")
|
|
status := defaultString(accountUser.Account.Status, "active")
|
|
role := defaultString(accountUser.Role, "agent")
|
|
permissions := []string{role}
|
|
var customRole any
|
|
var customRoleID *uint
|
|
if accountUser.CustomRole != nil && accountUser.CustomRoleID > 0 {
|
|
id := accountUser.CustomRoleID
|
|
customRoleID = &id
|
|
keys, err := accountUser.CustomRole.GetPermissionKeys()
|
|
if err == nil {
|
|
permissions = make([]string, 0, len(keys)+1)
|
|
for _, key := range keys {
|
|
permissions = append(permissions, string(key))
|
|
}
|
|
permissions = append(permissions, "custom_role")
|
|
}
|
|
customRole = map[string]any{
|
|
"id": accountUser.CustomRole.ID,
|
|
"name": accountUser.CustomRole.Name,
|
|
"description": accountUser.CustomRole.Description,
|
|
"permissions": permissionsWithoutMarker(permissions),
|
|
}
|
|
}
|
|
return ProfileAccountResponse{
|
|
ID: accountUser.AccountID,
|
|
Name: accountUser.Account.Name,
|
|
Status: status,
|
|
OnboardingStep: accountUser.Account.OnboardingStep,
|
|
ActiveAt: activeAt,
|
|
Role: role,
|
|
Permissions: permissions,
|
|
Availability: availability,
|
|
AvailabilityStatus: availability,
|
|
AutoOffline: accountUser.AutoOffline,
|
|
CustomRoleID: customRoleID,
|
|
CustomRole: customRole,
|
|
}
|
|
}
|
|
|
|
func permissionsWithoutMarker(permissions []string) []string {
|
|
keys := make([]string, 0, len(permissions))
|
|
for _, permission := range permissions {
|
|
if permission == "custom_role" {
|
|
continue
|
|
}
|
|
keys = append(keys, permission)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func timeStringPtr(t *time.Time) *string {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
formatted := t.UTC().Format(time.RFC3339Nano)
|
|
return &formatted
|
|
}
|
|
|
|
func jsonObject(raw []byte) map[string]any {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return map[string]any{}
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(raw, &out); err != nil || out == nil {
|
|
return map[string]any{}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func defaultString(value, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (s *ProfileService) currentAccessToken(ctx context.Context, userID uint) (string, error) {
|
|
if s.accessTokenRepo == nil {
|
|
return "", nil
|
|
}
|
|
tokens, err := s.accessTokenRepo.FindActiveByOwner(ctx, model.AccessTokenOwnerTypeUser, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to load access token: %w", err)
|
|
}
|
|
if len(tokens) > 0 {
|
|
return tokens[0].Token, nil
|
|
}
|
|
return s.regenerateAccessToken(ctx, userID)
|
|
}
|
|
|
|
func (s *ProfileService) regenerateAccessToken(ctx context.Context, userID uint) (string, error) {
|
|
if s.accessTokenRepo == nil {
|
|
return "", nil
|
|
}
|
|
tokens, err := s.accessTokenRepo.FindByOwner(ctx, model.AccessTokenOwnerTypeUser, userID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, token := range tokens {
|
|
if err := s.accessTokenRepo.Delete(ctx, token.ID); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
plainToken, err := generatePlatformAccessToken()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
accessToken := &model.AccessToken{
|
|
OwnerType: model.AccessTokenOwnerTypeUser,
|
|
OwnerID: userID,
|
|
Token: plainToken,
|
|
TokenPrefix: tokenPrefix(plainToken),
|
|
Name: "Personal Access Token",
|
|
}
|
|
if err := s.accessTokenRepo.Create(ctx, accessToken); err != nil {
|
|
return "", err
|
|
}
|
|
return plainToken, nil
|
|
}
|