@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user