Yimaru-BackEnd/internal/repository/team_invitations.go
Yared Yemane 31bd1e3814 Add team member email invitations for admin panel onboarding
Introduces invite, verify, accept, resend, and revoke flows using team_members and invitation tokens, sends the branded invitation template, and requires account activation before team login.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 03:43:00 -07:00

153 lines
4.6 KiB
Go

package repository
import (
"context"
"errors"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
func mapDBTeamInvitation(row dbgen.TeamInvitation) domain.TeamInvitation {
inv := domain.TeamInvitation{
ID: row.ID,
TeamMemberID: row.TeamMemberID,
Token: row.Token,
Status: domain.TeamInvitationStatus(row.Status),
ExpiresAt: row.ExpiresAt.Time,
CreatedAt: row.CreatedAt.Time,
}
if row.InvitedBy.Valid {
inv.InvitedBy = &row.InvitedBy.Int64
}
if row.AcceptedAt.Valid {
t := row.AcceptedAt.Time
inv.AcceptedAt = &t
}
if row.UpdatedAt.Valid {
t := row.UpdatedAt.Time
inv.UpdatedAt = &t
}
return inv
}
func (s *Store) CreateTeamInvitation(ctx context.Context, invitation domain.TeamInvitation) (domain.TeamInvitation, error) {
var invitedBy pgtype.Int8
if invitation.InvitedBy != nil {
invitedBy = pgtype.Int8{Int64: *invitation.InvitedBy, Valid: true}
}
row, err := s.queries.CreateTeamInvitation(ctx, dbgen.CreateTeamInvitationParams{
TeamMemberID: invitation.TeamMemberID,
Token: invitation.Token,
ExpiresAt: pgtype.Timestamptz{Time: invitation.ExpiresAt, Valid: true},
InvitedBy: invitedBy,
})
if err != nil {
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) GetTeamInvitationByToken(ctx context.Context, token string) (domain.TeamInvitation, error) {
row, err := s.queries.GetTeamInvitationByToken(ctx, token)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamInvitation{}, domain.ErrTeamInvitationNotFound
}
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) GetTeamInvitationByID(ctx context.Context, id int64) (domain.TeamInvitation, error) {
row, err := s.queries.GetTeamInvitationByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamInvitation{}, domain.ErrTeamInvitationNotFound
}
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) GetPendingTeamInvitationByMemberID(ctx context.Context, memberID int64) (domain.TeamInvitation, error) {
row, err := s.queries.GetPendingTeamInvitationByMemberID(ctx, memberID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamInvitation{}, domain.ErrTeamInvitationNotFound
}
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) RevokePendingTeamInvitationsForMember(ctx context.Context, memberID int64) error {
return s.queries.RevokePendingTeamInvitationsForMember(ctx, memberID)
}
func (s *Store) AcceptTeamInvitation(ctx context.Context, invitationID int64) (domain.TeamInvitation, error) {
row, err := s.queries.AcceptTeamInvitation(ctx, invitationID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamInvitation{}, domain.ErrTeamInvitationNotFound
}
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) RevokeTeamInvitation(ctx context.Context, invitationID int64) (domain.TeamInvitation, error) {
row, err := s.queries.RevokeTeamInvitation(ctx, invitationID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TeamInvitation{}, domain.ErrTeamInvitationNotFound
}
return domain.TeamInvitation{}, err
}
return mapDBTeamInvitation(row), nil
}
func (s *Store) ExpireTeamInvitation(ctx context.Context, invitationID int64) error {
return s.queries.ExpireTeamInvitation(ctx, invitationID)
}
func (s *Store) ListTeamInvitations(ctx context.Context, status *string, limit, offset int32) ([]domain.TeamInvitationWithMember, int64, error) {
rows, err := s.queries.ListTeamInvitations(ctx, dbgen.ListTeamInvitationsParams{
Status: toPgText(status),
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return nil, 0, err
}
out := make([]domain.TeamInvitationWithMember, 0, len(rows))
var total int64
for _, row := range rows {
inv := mapDBTeamInvitation(dbgen.TeamInvitation{
ID: row.ID,
TeamMemberID: row.TeamMemberID,
Token: row.Token,
Status: row.Status,
ExpiresAt: row.ExpiresAt,
InvitedBy: row.InvitedBy,
AcceptedAt: row.AcceptedAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
})
out = append(out, domain.TeamInvitationWithMember{
TeamInvitation: inv,
Email: row.Email,
FirstName: row.FirstName,
LastName: row.LastName,
TeamRole: domain.TeamRole(row.TeamRole),
})
total = row.TotalCount
}
return out, total, nil
}