Simplify team invite to email and role; collect profile on accept
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0ad7f094cf
commit
215a4bd1dc
|
|
@ -43,30 +43,45 @@ type TeamInvitationWithMember struct {
|
|||
TeamRole TeamRole
|
||||
}
|
||||
|
||||
const (
|
||||
TeamInvitePlaceholderFirstName = "Pending"
|
||||
TeamInvitePlaceholderLastName = "Invite"
|
||||
)
|
||||
|
||||
func IsTeamInvitePlaceholderProfile(firstName, lastName string) bool {
|
||||
return firstName == TeamInvitePlaceholderFirstName && lastName == TeamInvitePlaceholderLastName
|
||||
}
|
||||
|
||||
// InviteTeamMemberReq only requires email and role; profile fields are collected on accept.
|
||||
type InviteTeamMemberReq struct {
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
TeamRole string `json:"team_role" validate:"required"`
|
||||
Department string `json:"department"`
|
||||
JobTitle string `json:"job_title"`
|
||||
EmploymentType string `json:"employment_type"`
|
||||
HireDate string `json:"hire_date"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
type AcceptTeamInvitationReq struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
|
||||
Department string `json:"department"`
|
||||
JobTitle string `json:"job_title"`
|
||||
EmploymentType string `json:"employment_type"`
|
||||
HireDate string `json:"hire_date"` // YYYY-MM-DD
|
||||
|
||||
ProfilePictureURL string `json:"profile_picture_url"`
|
||||
Bio string `json:"bio"`
|
||||
WorkPhone string `json:"work_phone"`
|
||||
EmergencyContact string `json:"emergency_contact"`
|
||||
}
|
||||
|
||||
type VerifyTeamInvitationRes struct {
|
||||
Valid bool `json:"valid"`
|
||||
Email string `json:"email,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
TeamRole string `json:"team_role,omitempty"`
|
||||
NeedsProfileSetup bool `json:"needs_profile_setup,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ func (s *Service) InviteTeamMember(ctx context.Context, req domain.InviteTeamMem
|
|||
if !domain.TeamRole(req.TeamRole).IsValid() {
|
||||
return domain.InviteTeamMemberRes{}, domain.ErrInvalidTeamRole
|
||||
}
|
||||
if req.EmploymentType != "" && !domain.EmploymentType(req.EmploymentType).IsValid() {
|
||||
return domain.InviteTeamMemberRes{}, domain.ErrInvalidEmploymentType
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
exists, err := s.teamStore.CheckTeamMemberEmailExists(ctx, email)
|
||||
|
|
@ -43,29 +40,14 @@ func (s *Service) InviteTeamMember(ctx context.Context, req domain.InviteTeamMem
|
|||
return domain.InviteTeamMemberRes{}, err
|
||||
}
|
||||
|
||||
var hireDate *time.Time
|
||||
if req.HireDate != "" {
|
||||
parsed, err := time.Parse("2006-01-02", req.HireDate)
|
||||
if err != nil {
|
||||
return domain.InviteTeamMemberRes{}, err
|
||||
}
|
||||
hireDate = &parsed
|
||||
}
|
||||
|
||||
member := domain.TeamMember{
|
||||
FirstName: strings.TrimSpace(req.FirstName),
|
||||
LastName: strings.TrimSpace(req.LastName),
|
||||
FirstName: domain.TeamInvitePlaceholderFirstName,
|
||||
LastName: domain.TeamInvitePlaceholderLastName,
|
||||
Email: email,
|
||||
PhoneNumber: strings.TrimSpace(req.PhoneNumber),
|
||||
Password: hashedPassword,
|
||||
TeamRole: domain.TeamRole(req.TeamRole),
|
||||
Department: strings.TrimSpace(req.Department),
|
||||
JobTitle: strings.TrimSpace(req.JobTitle),
|
||||
EmploymentType: domain.EmploymentType(req.EmploymentType),
|
||||
HireDate: hireDate,
|
||||
Status: domain.TeamMemberStatusInactive,
|
||||
EmailVerified: false,
|
||||
Permissions: req.Permissions,
|
||||
CreatedBy: invitedBy,
|
||||
}
|
||||
|
||||
|
|
@ -160,15 +142,17 @@ func (s *Service) VerifyTeamInvitation(ctx context.Context, token string) (domai
|
|||
return domain.VerifyTeamInvitationRes{Valid: false, Status: string(domain.TeamInvitationStatusExpired)}, nil
|
||||
}
|
||||
|
||||
return domain.VerifyTeamInvitationRes{
|
||||
res := domain.VerifyTeamInvitationRes{
|
||||
Valid: true,
|
||||
Email: member.Email,
|
||||
FirstName: member.FirstName,
|
||||
LastName: member.LastName,
|
||||
TeamRole: string(member.TeamRole),
|
||||
ExpiresAt: inv.ExpiresAt,
|
||||
Status: string(inv.Status),
|
||||
}, nil
|
||||
}
|
||||
if domain.IsTeamInvitePlaceholderProfile(member.FirstName, member.LastName) {
|
||||
res.NeedsProfileSetup = true
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) AcceptTeamInvitation(ctx context.Context, req domain.AcceptTeamInvitationReq) (domain.TeamMember, error) {
|
||||
|
|
@ -177,6 +161,29 @@ func (s *Service) AcceptTeamInvitation(ctx context.Context, req domain.AcceptTea
|
|||
return domain.TeamMember{}, err
|
||||
}
|
||||
|
||||
if req.EmploymentType != "" && !domain.EmploymentType(req.EmploymentType).IsValid() {
|
||||
return domain.TeamMember{}, domain.ErrInvalidEmploymentType
|
||||
}
|
||||
|
||||
updateReq := domain.UpdateTeamMemberReq{
|
||||
TeamMemberID: member.ID,
|
||||
UpdatedBy: member.ID,
|
||||
FirstName: strings.TrimSpace(req.FirstName),
|
||||
LastName: strings.TrimSpace(req.LastName),
|
||||
PhoneNumber: strings.TrimSpace(req.PhoneNumber),
|
||||
Department: strings.TrimSpace(req.Department),
|
||||
JobTitle: strings.TrimSpace(req.JobTitle),
|
||||
EmploymentType: strings.TrimSpace(req.EmploymentType),
|
||||
HireDate: strings.TrimSpace(req.HireDate),
|
||||
ProfilePictureURL: strings.TrimSpace(req.ProfilePictureURL),
|
||||
Bio: strings.TrimSpace(req.Bio),
|
||||
WorkPhone: strings.TrimSpace(req.WorkPhone),
|
||||
EmergencyContact: strings.TrimSpace(req.EmergencyContact),
|
||||
}
|
||||
if err := s.UpdateTeamMember(ctx, updateReq); err != nil {
|
||||
return domain.TeamMember{}, err
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)
|
||||
if err != nil {
|
||||
return domain.TeamMember{}, err
|
||||
|
|
@ -279,8 +286,13 @@ func (s *Service) sendInvitationEmail(ctx context.Context, member domain.TeamMem
|
|||
return fmt.Errorf("email services are not configured")
|
||||
}
|
||||
|
||||
var firstName string
|
||||
if !domain.IsTeamInvitePlaceholderProfile(member.FirstName, member.LastName) {
|
||||
firstName = member.FirstName
|
||||
}
|
||||
|
||||
rendered, err := s.emailTemplateSvc.Render(ctx, domain.EmailTemplateSlugInvitation, map[string]any{
|
||||
"FirstName": member.FirstName,
|
||||
"FirstName": firstName,
|
||||
"InviterName": inviterName,
|
||||
"InviteLink": inviteLink,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,12 +31,16 @@ type listTeamInvitationsRes struct {
|
|||
}
|
||||
|
||||
func mapTeamInvitationListItem(row domain.TeamInvitationWithMember) teamInvitationListItem {
|
||||
firstName, lastName := row.FirstName, row.LastName
|
||||
if domain.IsTeamInvitePlaceholderProfile(firstName, lastName) {
|
||||
firstName, lastName = "", ""
|
||||
}
|
||||
return teamInvitationListItem{
|
||||
ID: row.ID,
|
||||
TeamMemberID: row.TeamMemberID,
|
||||
Email: row.Email,
|
||||
FirstName: row.FirstName,
|
||||
LastName: row.LastName,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
TeamRole: string(row.TeamRole),
|
||||
Status: string(row.Status),
|
||||
ExpiresAt: row.ExpiresAt.Format(time.RFC3339),
|
||||
|
|
@ -46,7 +50,7 @@ func mapTeamInvitationListItem(row domain.TeamInvitationWithMember) teamInvitati
|
|||
|
||||
// InviteTeamMember godoc
|
||||
// @Summary Invite a team member by email
|
||||
// @Description Creates a pending team member and sends an invitation email with a setup link
|
||||
// @Description Creates a pending team member (email + team_role only) and sends an invitation email; profile is completed on accept
|
||||
// @Tags team
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
@ -236,8 +240,8 @@ func (h *Handler) VerifyTeamInvitation(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
// AcceptTeamInvitation godoc
|
||||
// @Summary Accept team invitation and set password
|
||||
// @Description Public endpoint to activate a team member account after following the invite link
|
||||
// @Summary Accept team invitation and complete account setup
|
||||
// @Description Public endpoint to set password and profile details after following the invite link
|
||||
// @Tags team
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user