Plug trust center part 1

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-09 19:14:17 +01:00
committed by Bryan Frimin
parent 663400c4f6
commit 7322201dab
41 changed files with 1959 additions and 1033 deletions

View File

@@ -16,6 +16,7 @@ package iam
import (
"context"
"errors"
"fmt"
"time"
@@ -50,6 +51,11 @@ type (
FullName string
}
LoadOrCreateIdentityRequest struct {
Email mail.Addr
FullName string
}
CreateIdentityWithPasswordRequest struct {
Email mail.Addr
Password string
@@ -59,11 +65,16 @@ type (
PasswordResetData struct {
Email mail.Addr `json:"email"`
}
MagicLinkData struct {
Email mail.Addr `json:"email"`
}
)
const (
TokenTypeOrganizationInvitation = "organization_invitation"
TokenTypePasswordReset = "password_reset"
TokenTypeMagicLink = "magic_link"
)
func NewAuthService(svc *Service) *AuthService {
@@ -98,6 +109,14 @@ func (req ChangePasswordRequest) Validate() error {
return v.Error()
}
func (req LoadOrCreateIdentityRequest) Validate() error {
v := validator.New()
v.Check(req.FullName, "fullName", validator.NotEmpty(), validator.MinLen(1), validator.MaxLen(255))
return v.Error()
}
func (req CreateIdentityWithPasswordRequest) Validate() error {
v := validator.New()
@@ -300,6 +319,51 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
)
}
func (s AuthService) LoadOrCreateIdentity(
ctx context.Context,
req *LoadOrCreateIdentityRequest,
) (*coredata.Identity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var (
identity *coredata.Identity
now = time.Now()
)
if err := s.pg.WithTx(ctx, func(tx pg.Conn) error {
identity = &coredata.Identity{}
if err := identity.LoadByEmail(ctx, tx, req.Email); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load identity: %w", err)
}
identity = &coredata.Identity{
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
EmailAddress: req.Email,
FullName: req.FullName,
HashedPassword: nil,
EmailAddressVerified: false,
CreatedAt: now,
UpdatedAt: now,
}
err = identity.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert identity: %w", err)
}
}
return nil
}); err != nil {
return nil, err
}
return identity, nil
}
func (s AuthService) CreateIdentityWithPassword(
ctx context.Context,
req *CreateIdentityWithPasswordRequest,
@@ -476,3 +540,103 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, email mail.Add
return identity, session, err
}
func (s AuthService) SendMagicLink(ctx context.Context, email mail.Addr) error {
token, err := statelesstoken.NewToken(
s.tokenSecret,
TokenTypeMagicLink,
s.magicLinkTokenValidity,
MagicLinkData{
Email: email,
},
)
if err != nil {
return fmt.Errorf("cannot generate magic link token: %w", err)
}
base, err := baseurl.Parse(s.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
magicLinkURL := base.
WithPath("/auth/magic-link").
WithQuery("token", token).
MustString()
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
fullName := email.Username()
identity := &coredata.Identity{}
err := identity.LoadByEmail(ctx, tx, email)
if err == nil {
fullName = identity.FullName
} else {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load identity: %w", err)
}
}
subject, textBody, htmlBody, err := emails.RenderMagicLink(
s.baseURL,
fullName,
magicLinkURL,
s.invitationTokenValidity,
)
if err != nil {
return fmt.Errorf("cannot render magic link email: %w", err)
}
magicLinkEmail := coredata.NewEmail(
fullName,
email,
subject,
textBody,
htmlBody,
)
err = magicLinkEmail.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert email: %w", err)
}
return nil
},
)
}
func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, token string) (*coredata.Identity, *coredata.Session, error) {
var (
identity = &coredata.Identity{}
session = &coredata.Session{}
)
payload, err := statelesstoken.ValidateToken[MagicLinkData](s.tokenSecret, TokenTypeMagicLink, token)
if err != nil {
return nil, nil, NewInvalidTokenError()
}
if err := s.pg.WithTx(
ctx,
func(conn pg.Conn) error {
err := identity.LoadByEmail(ctx, conn, payload.Data.Email)
if err != nil {
return fmt.Errorf("cannot load identity by email: %w", err)
}
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration)
err = session.Insert(ctx, conn)
if err != nil {
return fmt.Errorf("cannot insert session: %w", err)
}
return nil
},
); err != nil {
return nil, nil, err
}
return identity, session, err
}

View File

@@ -31,6 +31,7 @@ type (
disableSignup bool
invitationTokenValidity time.Duration
passwordResetTokenValidity time.Duration
magicLinkTokenValidity time.Duration
sessionDuration time.Duration
bucket string
certificate *x509.Certificate
@@ -53,6 +54,7 @@ type (
DisableSignup bool
InvitationTokenValidity time.Duration
PasswordResetTokenValidity time.Duration
MagicLinkTokenValidity time.Duration
SessionDuration time.Duration
Bucket string
TokenSecret string
@@ -99,6 +101,7 @@ func NewService(
disableSignup: cfg.DisableSignup,
invitationTokenValidity: cfg.InvitationTokenValidity,
passwordResetTokenValidity: cfg.PasswordResetTokenValidity,
magicLinkTokenValidity: cfg.MagicLinkTokenValidity,
sessionDuration: cfg.SessionDuration,
bucket: cfg.Bucket,
certificate: cfg.Certificate,