Extract email presentation configuration layer to streamline email branding
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -21,14 +21,73 @@ import (
|
|||||||
htmltemplate "html/template"
|
htmltemplate "html/template"
|
||||||
texttemplate "text/template"
|
texttemplate "text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist
|
//go:embed dist
|
||||||
var Templates embed.FS
|
var Templates embed.FS
|
||||||
|
|
||||||
const (
|
type (
|
||||||
logoURLPath = "/logos/probo.png"
|
PresenterConfig struct {
|
||||||
|
BaseURL string
|
||||||
|
SenderCompanyName string
|
||||||
|
SenderCompanyWebsiteURL string
|
||||||
|
SenderCompanyLogoURL string
|
||||||
|
SenderCompanyHeadquarterAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
PresenterVariables struct {
|
||||||
|
// Static variables
|
||||||
|
BaseURL string
|
||||||
|
SenderCompanyName string
|
||||||
|
SenderCompanyWebsiteURL string
|
||||||
|
SenderCompanyLogoURL string
|
||||||
|
SenderCompanyHeadquarterAddress string
|
||||||
|
|
||||||
|
// Common variables
|
||||||
|
RecipientFullName string
|
||||||
|
// Not to confuse with the SenderCompanyName, which is the brand of the product being used
|
||||||
|
OrganizationName string
|
||||||
|
}
|
||||||
|
|
||||||
|
Presenter struct {
|
||||||
|
variables PresenterVariables
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultPresenterConfig(baseURL string) PresenterConfig {
|
||||||
|
return PresenterConfig{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
SenderCompanyName: "Probo",
|
||||||
|
SenderCompanyWebsiteURL: "https://www.getprobo.com",
|
||||||
|
SenderCompanyLogoURL: baseurl.MustParse(baseURL).WithPath("/logos/probo.png").MustString(),
|
||||||
|
SenderCompanyHeadquarterAddress: "Probo Inc, 490 Post St, STE 640, San Francisco, CA, 94102, US",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPresenterFromConfig(cfg PresenterConfig, fullName string) *Presenter {
|
||||||
|
return &Presenter{
|
||||||
|
variables: PresenterVariables{
|
||||||
|
BaseURL: cfg.BaseURL,
|
||||||
|
SenderCompanyName: cfg.SenderCompanyName,
|
||||||
|
SenderCompanyWebsiteURL: cfg.SenderCompanyWebsiteURL,
|
||||||
|
SenderCompanyLogoURL: cfg.SenderCompanyLogoURL,
|
||||||
|
SenderCompanyHeadquarterAddress: cfg.SenderCompanyHeadquarterAddress,
|
||||||
|
|
||||||
|
RecipientFullName: fullName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPresenter(baseURL string, fullName string) *Presenter {
|
||||||
|
return NewPresenterFromConfig(
|
||||||
|
DefaultPresenterConfig(baseURL),
|
||||||
|
fullName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
subjectConfirmEmail = "Confirm your email address"
|
subjectConfirmEmail = "Confirm your email address"
|
||||||
subjectPasswordReset = "Reset your password"
|
subjectPasswordReset = "Reset your password"
|
||||||
subjectInvitation = "Invitation to join %s on Probo"
|
subjectInvitation = "Invitation to join %s on Probo"
|
||||||
@@ -37,7 +96,7 @@ const (
|
|||||||
subjectFrameworkExport = "Your framework export is ready"
|
subjectFrameworkExport = "Your framework export is ready"
|
||||||
subjectTrustCenterAccess = "Compliance Page Access Invitation - %s"
|
subjectTrustCenterAccess = "Compliance Page Access Invitation - %s"
|
||||||
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
||||||
subjectMagicLink = "Connect to Probo"
|
subjectMagicLink = "Connect to %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -61,154 +120,158 @@ var (
|
|||||||
magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl"))
|
magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl"))
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderConfirmEmail(baseURL, fullName, confirmationUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderConfirmEmail(confirmationURLPath string, confirmationTokenParam string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
confirmationUrl := baseurl.
|
||||||
|
MustParse(p.variables.BaseURL).
|
||||||
|
WithPath(confirmationURLPath).
|
||||||
|
WithQuery("token", confirmationTokenParam).
|
||||||
|
MustString()
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
ConfirmationUrl string
|
ConfirmationUrl string
|
||||||
LogoURL string
|
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
ConfirmationUrl: confirmationUrl,
|
ConfirmationUrl: confirmationUrl,
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
|
||||||
return subjectConfirmEmail, textBody, htmlBody, err
|
return subjectConfirmEmail, textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPasswordReset(baseURL, fullName, resetUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderPasswordReset(resetPasswordURLPath string, resetPasswordToken string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
resetUrl := baseurl.
|
||||||
|
MustParse(p.variables.BaseURL).
|
||||||
|
WithPath(resetPasswordURLPath).
|
||||||
|
WithQuery("token", resetPasswordToken).
|
||||||
|
MustString()
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
ResetUrl string
|
ResetUrl string
|
||||||
LogoURL string
|
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
ResetUrl: resetUrl,
|
ResetUrl: resetUrl,
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
|
||||||
return subjectPasswordReset, textBody, htmlBody, err
|
return subjectPasswordReset, textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderInvitation(baseURL, fullName, organizationName, invitationUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderInvitation(invitationURLPath string, invitationToken string, organizationName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
invitationURL := baseurl.
|
||||||
|
MustParse(p.variables.BaseURL).
|
||||||
|
WithPath(invitationURLPath).
|
||||||
|
WithQuery("token", invitationToken).
|
||||||
|
WithQuery("fullName", p.variables.RecipientFullName).
|
||||||
|
MustString()
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
OrganizationName string
|
|
||||||
InvitationUrl string
|
InvitationUrl string
|
||||||
LogoURL string
|
OrganizationName string
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
|
InvitationUrl: invitationURL,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
InvitationUrl: invitationUrl,
|
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
|
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderDocumentSigning(baseURL, fullName, organizationName, signingUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderDocumentSigning(signinURLPath string, token string, organizationName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
signingURL := baseurl.MustParse(p.variables.BaseURL).
|
||||||
|
WithPath(signinURLPath).
|
||||||
|
WithQuery("token", token).
|
||||||
|
MustString()
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
OrganizationName string
|
|
||||||
SigningUrl string
|
SigningUrl string
|
||||||
LogoURL string
|
OrganizationName string
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
|
SigningUrl: signingURL,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
SigningUrl: signingUrl,
|
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err
|
return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderDocumentExport(baseURL, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderDocumentExport(downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
DownloadUrl string
|
DownloadUrl string
|
||||||
LogoURL string
|
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
DownloadUrl: downloadUrl,
|
DownloadUrl: downloadUrl,
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
|
||||||
return subjectDocumentExport, textBody, htmlBody, err
|
return subjectDocumentExport, textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderFrameworkExport(baseURL, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderFrameworkExport(downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
DownloadUrl string
|
DownloadUrl string
|
||||||
LogoURL string
|
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
DownloadUrl: downloadUrl,
|
DownloadUrl: downloadUrl,
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
|
||||||
return subjectFrameworkExport, textBody, htmlBody, err
|
return subjectFrameworkExport, textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderTrustCenterAccess(baseURL, fullName, organizationName, accessUrl string) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderTrustCenterAccess(organizationName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
OrganizationName string
|
OrganizationName string
|
||||||
AccessUrl string
|
|
||||||
LogoURL string
|
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
AccessUrl: accessUrl,
|
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectTrustCenterAccess, organizationName), textBody, htmlBody, err
|
return fmt.Sprintf(subjectTrustCenterAccess, organizationName), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderTrustCenterDocumentAccessRejected(
|
func (p *Presenter) RenderTrustCenterDocumentAccessRejected(
|
||||||
baseURL string,
|
|
||||||
fullName string,
|
|
||||||
organizationName string,
|
|
||||||
fileNames []string,
|
fileNames []string,
|
||||||
|
organizationName string,
|
||||||
) (subject string, textBody string, htmlBody *string, err error) {
|
) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
OrganizationName string
|
|
||||||
LogoURL string
|
|
||||||
FileNames []string
|
FileNames []string
|
||||||
|
OrganizationName string
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
OrganizationName: organizationName,
|
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
FileNames: fileNames,
|
FileNames: fileNames,
|
||||||
|
OrganizationName: organizationName,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(trustCenterDocumentAccessRejectedTextTemplate, trustCenterDocumentAccessRejectedHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(trustCenterDocumentAccessRejectedTextTemplate, trustCenterDocumentAccessRejectedHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err
|
return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderMagicLink(baseURL, fullName, magicLinkUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) {
|
func (p *Presenter) RenderMagicLink(magicLinkUrlPath string, tokenDuration time.Duration, organizationName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
data := struct {
|
data := struct {
|
||||||
FullName string
|
PresenterVariables
|
||||||
MagicLinkURL string
|
MagicLinkURL string
|
||||||
LogoURL string
|
|
||||||
DurationInMinutes int
|
DurationInMinutes int
|
||||||
|
OrganizationName string
|
||||||
}{
|
}{
|
||||||
FullName: fullName,
|
PresenterVariables: p.variables,
|
||||||
MagicLinkURL: magicLinkUrl,
|
MagicLinkURL: baseurl.MustParse(p.variables.BaseURL).WithPath(magicLinkUrlPath).MustString(),
|
||||||
LogoURL: baseURL + logoURLPath,
|
|
||||||
DurationInMinutes: int(tokenDuration.Minutes()),
|
DurationInMinutes: int(tokenDuration.Minutes()),
|
||||||
|
OrganizationName: organizationName,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(magicLinkTextTemplate, magicLinkHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(magicLinkTextTemplate, magicLinkHTMLTemplate, data)
|
||||||
return subjectMagicLink, textBody, htmlBody, err
|
return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) {
|
func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import EmailLayout, {
|
|||||||
export const MagicLink = () => {
|
export const MagicLink = () => {
|
||||||
return (
|
return (
|
||||||
<EmailLayout subject="Probo Magic Link">
|
<EmailLayout subject="Probo Magic Link">
|
||||||
<Text style={bodyText}>Please use this link to connect to Probo:</Text>
|
<Text style={bodyText}>{"Please use this link to connect to {{.OrganizationName}}'s Compliance Page:"}</Text>
|
||||||
|
|
||||||
<Section style={buttonContainer}>
|
<Section style={buttonContainer}>
|
||||||
<Button style={button} href={"{{.MagicLinkURL}}"}>
|
<Button style={button} href={"{{.MagicLinkURL}}"}>
|
||||||
Connect to Probo
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ProboLogo } from './ProboLogo';
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
interface EmailLayoutProps {
|
interface EmailLayoutProps {
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -33,19 +33,19 @@ export const EmailLayout = ({
|
|||||||
<Container style={container}>
|
<Container style={container}>
|
||||||
<Section style={content}>
|
<Section style={content}>
|
||||||
<Section style={logoSection}>
|
<Section style={logoSection}>
|
||||||
<Link href="https://www.getprobo.com">
|
<Link href="{{.SenderCompanyWebsiteURL}}">
|
||||||
<ProboLogo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Text style={text}>Hi {'{{.FullName}}'},</Text>
|
<Text style={text}>Hi {'{{.RecipientFullName}}'},</Text>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section style={footerSection}>
|
<Section style={footerSection}>
|
||||||
<Text style={footerAddress}>
|
<Text style={footerAddress}>
|
||||||
Probo Inc, 490 Post St, STE 640, San Francisco, CA, 94102, US
|
{"{{.SenderCompanyHeadquarterAddress}}"}
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
13
packages/emails/src/components/Logo.tsx
Normal file
13
packages/emails/src/components/Logo.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Img } from '@react-email/components';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<Img
|
||||||
|
className="max-width-[220px]"
|
||||||
|
src="{{.SenderCompanyLogoURL}}"
|
||||||
|
alt="{{.SenderCompanyName}}"
|
||||||
|
height="60"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Img } from '@react-email/components';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
export function ProboLogo() {
|
|
||||||
return (
|
|
||||||
<Img
|
|
||||||
src={'{{.LogoURL}}'}
|
|
||||||
alt="Probo"
|
|
||||||
width="220"
|
|
||||||
height="60"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/packages/emails"
|
"go.probo.inc/probo/packages/emails"
|
||||||
"go.probo.inc/probo/pkg/baseurl"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/mail"
|
"go.probo.inc/probo/pkg/mail"
|
||||||
@@ -86,16 +85,6 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
|||||||
return fmt.Errorf("cannot generate confirmation token: %w", err)
|
return fmt.Errorf("cannot generate confirmation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := baseurl.Parse(s.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmationUrl := base.
|
|
||||||
WithPath("/auth/verify-email").
|
|
||||||
WithQuery("token", confirmationToken).
|
|
||||||
MustString()
|
|
||||||
|
|
||||||
return s.pg.WithTx(
|
return s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
@@ -127,11 +116,9 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
|||||||
return fmt.Errorf("cannot update identity: %w", err)
|
return fmt.Errorf("cannot update identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
emailPresenter := emails.NewPresenter(s.baseURL, identity.FullName)
|
||||||
s.baseURL,
|
|
||||||
identity.FullName,
|
subject, textBody, htmlBody, err := emailPresenter.RenderConfirmEmail("/auth/verify-email", confirmationToken)
|
||||||
confirmationUrl,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render confirmation email: %w", err)
|
return fmt.Errorf("cannot render confirmation email: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/packages/emails"
|
"go.probo.inc/probo/packages/emails"
|
||||||
"go.probo.inc/probo/pkg/baseurl"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/mail"
|
"go.probo.inc/probo/pkg/mail"
|
||||||
@@ -65,7 +64,10 @@ type (
|
|||||||
|
|
||||||
SendMagicLinkRequest struct {
|
SendMagicLinkRequest struct {
|
||||||
Email mail.Addr
|
Email mail.Addr
|
||||||
BaseURL *baseurl.URLBuilder
|
URLPath string
|
||||||
|
OrganizationID gid.GID
|
||||||
|
// If users tries to connect to compliance page, we must brand the emails accordingly
|
||||||
|
CompliancePageID *gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordResetData struct {
|
PasswordResetData struct {
|
||||||
@@ -276,16 +278,6 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
|
|||||||
return fmt.Errorf("cannot generate password reset token: %w", err)
|
return fmt.Errorf("cannot generate password reset token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := baseurl.Parse(s.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPasswordUrl := base.
|
|
||||||
WithPath("/auth/reset-password").
|
|
||||||
WithQuery("token", token).
|
|
||||||
MustString()
|
|
||||||
|
|
||||||
return s.pg.WithTx(
|
return s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
@@ -298,10 +290,11 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
|
|||||||
return fmt.Errorf("cannot load identity: %w", err)
|
return fmt.Errorf("cannot load identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
|
emailPresenter := emails.NewPresenter(s.baseURL, identity.FullName)
|
||||||
s.baseURL,
|
|
||||||
identity.FullName,
|
subject, textBody, htmlBody, err := emailPresenter.RenderPasswordReset(
|
||||||
resetPasswordUrl,
|
"/auth/reset-password",
|
||||||
|
token,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render password reset email: %w", err)
|
return fmt.Errorf("cannot render password reset email: %w", err)
|
||||||
@@ -413,24 +406,9 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
return nil, nil, fmt.Errorf("cannot generate confirmation token: %w", err)
|
return nil, nil, fmt.Errorf("cannot generate confirmation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := baseurl.Parse(s.baseURL)
|
emailPresenter := emails.NewPresenter(s.baseURL, req.FullName)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmationUrl, err := base.
|
subject, textBody, htmlBody, err := emailPresenter.RenderConfirmEmail("/auth/verify-email", confirmationToken)
|
||||||
WithPath("/auth/verify-email").
|
|
||||||
WithQuery("token", confirmationToken).
|
|
||||||
String()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot build confirmation URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
|
||||||
s.baseURL,
|
|
||||||
req.FullName,
|
|
||||||
confirmationUrl,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot render confirmation email: %w", err)
|
return nil, nil, fmt.Errorf("cannot render confirmation email: %w", err)
|
||||||
}
|
}
|
||||||
@@ -560,10 +538,6 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
|||||||
return fmt.Errorf("cannot generate magic link token: %w", err)
|
return fmt.Errorf("cannot generate magic link token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
magicLinkURL := req.BaseURL.
|
|
||||||
WithQuery("token", tokenString).
|
|
||||||
MustString()
|
|
||||||
|
|
||||||
return s.pg.WithTx(
|
return s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
@@ -579,9 +553,9 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
|||||||
|
|
||||||
fullName := req.Email.Username()
|
fullName := req.Email.Username()
|
||||||
identity := &coredata.Identity{}
|
identity := &coredata.Identity{}
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
|
||||||
err := identity.LoadByEmail(ctx, tx, req.Email)
|
if err := identity.LoadByEmail(ctx, tx, req.Email); err == nil {
|
||||||
if err == nil {
|
|
||||||
if identity.FullName != "" {
|
if identity.FullName != "" {
|
||||||
fullName = identity.FullName
|
fullName = identity.FullName
|
||||||
}
|
}
|
||||||
@@ -591,11 +565,25 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderMagicLink(
|
if err := organization.LoadByID(ctx, tx, coredata.NewNoScope(), req.OrganizationID); err != nil {
|
||||||
s.baseURL,
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
fullName,
|
}
|
||||||
magicLinkURL,
|
|
||||||
|
emailPresenterCfg := emails.DefaultPresenterConfig(s.baseURL)
|
||||||
|
if req.CompliancePageID != nil {
|
||||||
|
var err error
|
||||||
|
emailPresenterCfg, err = s.CompliancePageService.EmailPresenterConfig(ctx, *req.CompliancePageID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get compliance page email presenter config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenter := emails.NewPresenterFromConfig(emailPresenterCfg, fullName)
|
||||||
|
|
||||||
|
subject, textBody, htmlBody, err := emailPresenter.RenderMagicLink(
|
||||||
|
req.URLPath,
|
||||||
s.magicLinkTokenValidity,
|
s.magicLinkTokenValidity,
|
||||||
|
organization.Name,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render magic link email: %w", err)
|
return fmt.Errorf("cannot render magic link email: %w", err)
|
||||||
|
|||||||
175
pkg/iam/compliance_page_service.go
Normal file
175
pkg/iam/compliance_page_service.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/packages/emails"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CompliancePageService struct {
|
||||||
|
*Service
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCompliancePageService(svc *Service) *CompliancePageService {
|
||||||
|
return &CompliancePageService{Service: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompliancePageService) GenerateLogoURL(
|
||||||
|
ctx context.Context,
|
||||||
|
compliancePageID gid.GID,
|
||||||
|
expiresIn time.Duration,
|
||||||
|
) (*string, error) {
|
||||||
|
file := &coredata.File{}
|
||||||
|
compliancePage := &coredata.TrustCenter{}
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(compliancePageID)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.FileKey == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
presignedURL, err := s.fm.GenerateFileUrl(ctx, file, expiresIn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &presignedURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompliancePageService) EmailPresenterConfig(ctx context.Context, compliancePageID gid.GID) (emails.PresenterConfig, error) {
|
||||||
|
var (
|
||||||
|
compliancePage = &coredata.TrustCenter{}
|
||||||
|
organization = &coredata.Organization{}
|
||||||
|
customDomain *coredata.CustomDomain
|
||||||
|
logoFile = &coredata.File{}
|
||||||
|
emailPresenterCfg = emails.DefaultPresenterConfig(s.baseURL)
|
||||||
|
)
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(compliancePageID)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if err := logoFile.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load logoFile: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := organization.LoadByID(ctx, conn, scope, compliancePage.OrganizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
customDomain = &coredata.CustomDomain{}
|
||||||
|
if err := customDomain.LoadByOrganizationID(ctx, conn, scope, s.encryptionKey, organization.ID); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load custom domain: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedBaseURL, err := url.Parse(s.baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot parse base URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := url.URL{
|
||||||
|
Scheme: parsedBaseURL.Scheme,
|
||||||
|
Host: parsedBaseURL.Host,
|
||||||
|
Path: "/trust/" + compliancePage.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive {
|
||||||
|
baseURL.Host = customDomain.Domain
|
||||||
|
baseURL.Scheme = "https"
|
||||||
|
baseURL.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.BaseURL = baseURL.String()
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if logoFile.FileKey == "" {
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If logo exists, then we will brand the emails with the org as a sender
|
||||||
|
|
||||||
|
presignedURL, err := s.fm.GenerateFileUrl(ctx, logoFile, 1*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyLogoURL = presignedURL
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyName = organization.Name
|
||||||
|
|
||||||
|
if organization.WebsiteURL != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyWebsiteURL = *organization.WebsiteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization.HeadquarterAddress != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
"go.gearno.de/crypto/uuid"
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/packages/emails"
|
"go.probo.inc/probo/packages/emails"
|
||||||
"go.probo.inc/probo/pkg/baseurl"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/filevalidation"
|
"go.probo.inc/probo/pkg/filevalidation"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
@@ -465,21 +464,12 @@ func (s *OrganizationService) InviteMember(
|
|||||||
return fmt.Errorf("cannot generate invitation token: %w", err)
|
return fmt.Errorf("cannot generate invitation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseurl, err := baseurl.Parse(s.baseURL)
|
emailPresenter := emails.NewPresenter(s.baseURL, identity.FullName)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invitationURL := baseurl.WithPath("/auth/signup-from-invitation").
|
subject, textBody, htmlBody, err := emailPresenter.RenderInvitation(
|
||||||
WithQuery("token", invitationToken).
|
"/auth/signup-from-invitation",
|
||||||
WithQuery("fullName", invitation.FullName).
|
invitationToken,
|
||||||
MustString()
|
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderInvitation(
|
|
||||||
s.baseURL,
|
|
||||||
invitation.FullName,
|
|
||||||
organization.Name,
|
organization.Name,
|
||||||
invitationURL,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render invitation email: %w", err)
|
return fmt.Errorf("cannot render invitation email: %w", err)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type (
|
|||||||
|
|
||||||
AccountService *AccountService
|
AccountService *AccountService
|
||||||
OrganizationService *OrganizationService
|
OrganizationService *OrganizationService
|
||||||
|
CompliancePageService *CompliancePageService
|
||||||
SessionService *SessionService
|
SessionService *SessionService
|
||||||
AuthService *AuthService
|
AuthService *AuthService
|
||||||
SAMLService *saml.Service
|
SAMLService *saml.Service
|
||||||
@@ -118,6 +119,7 @@ func NewService(
|
|||||||
|
|
||||||
svc.AccountService = NewAccountService(svc)
|
svc.AccountService = NewAccountService(svc)
|
||||||
svc.OrganizationService = NewOrganizationService(svc)
|
svc.OrganizationService = NewOrganizationService(svc)
|
||||||
|
svc.CompliancePageService = NewCompliancePageService(svc)
|
||||||
svc.SessionService = NewSessionService(svc)
|
svc.SessionService = NewSessionService(svc)
|
||||||
svc.AuthService = NewAuthService(svc)
|
svc.AuthService = NewAuthService(svc)
|
||||||
svc.APIKeyService = NewAPIKeyService(svc)
|
svc.APIKeyService = NewAPIKeyService(svc)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -544,25 +543,12 @@ func (s *DocumentService) SendSigningNotifications(
|
|||||||
return fmt.Errorf("cannot create signing request token: %w", err)
|
return fmt.Errorf("cannot create signing request token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURLParsed, err := url.Parse(s.svc.baseURL)
|
emailPresenter := emails.NewPresenter(s.svc.baseURL, people.FullName)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signRequestURL := url.URL{
|
subject, textBody, htmlBody, err := emailPresenter.RenderDocumentSigning(
|
||||||
Scheme: baseURLParsed.Scheme,
|
"/documents/signing-requests",
|
||||||
Host: baseURLParsed.Host,
|
token,
|
||||||
Path: "/documents/signing-requests",
|
|
||||||
RawQuery: url.Values{
|
|
||||||
"token": []string{token},
|
|
||||||
}.Encode(),
|
|
||||||
}
|
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderDocumentSigning(
|
|
||||||
s.svc.baseURL,
|
|
||||||
people.FullName,
|
|
||||||
organization.Name,
|
organization.Name,
|
||||||
signRequestURL.String(),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render signing request email: %w", err)
|
return fmt.Errorf("cannot render signing request email: %w", err)
|
||||||
@@ -1837,9 +1823,9 @@ func (s *DocumentService) SendExportEmail(
|
|||||||
return fmt.Errorf("cannot generate download URL: %w", err)
|
return fmt.Errorf("cannot generate download URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderDocumentExport(
|
emailPresenter := emails.NewPresenter(s.svc.baseURL, recipientName)
|
||||||
s.svc.baseURL,
|
|
||||||
recipientName,
|
subject, textBody, htmlBody, err := emailPresenter.RenderDocumentExport(
|
||||||
downloadURL,
|
downloadURL,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -800,9 +800,9 @@ func (s FrameworkService) SendExportEmail(
|
|||||||
return fmt.Errorf("cannot generate download URL: %w", err)
|
return fmt.Errorf("cannot generate download URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderFrameworkExport(
|
emailPresenter := emails.NewPresenter(s.svc.baseURL, recipientName)
|
||||||
s.svc.baseURL,
|
|
||||||
recipientName,
|
subject, textBody, htmlBody, err := emailPresenter.RenderFrameworkExport(
|
||||||
downloadURL,
|
downloadURL,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
@@ -412,36 +411,6 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
|
|||||||
return fmt.Errorf("cannot load organization: %w", err)
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURLParsed, err := url.Parse(s.svc.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := baseURLParsed.Host
|
|
||||||
scheme := baseURLParsed.Scheme
|
|
||||||
path := "/trust/" + trustCenter.Slug
|
|
||||||
|
|
||||||
if organization.CustomDomainID != nil {
|
|
||||||
customDomain, err := s.svc.CustomDomains.GetOrganizationCustomDomain(ctx, organization.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load custom domain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomain == nil || customDomain.SSLStatus != coredata.CustomDomainSSLStatusActive {
|
|
||||||
return fmt.Errorf("custom domain is not active")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname = customDomain.Domain
|
|
||||||
scheme = "https"
|
|
||||||
path = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
accessURL := url.URL{
|
|
||||||
Scheme: scheme,
|
|
||||||
Host: hostname,
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
access.UpdatedAt = now
|
access.UpdatedAt = now
|
||||||
|
|
||||||
@@ -449,30 +418,21 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
|
|||||||
return fmt.Errorf("cannot update trust center access with expiration: %w", err)
|
return fmt.Errorf("cannot update trust center access with expiration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.sendTrustCenterAccessEmail(ctx, tx, access.Name, access.Email, organization.Name, accessURL.String())
|
emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID)
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get compliance page email presenter config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (s TrustCenterAccessService) sendTrustCenterAccessEmail(
|
emailPresenter := emails.NewPresenterFromConfig(emailPresenterCfg, access.Name)
|
||||||
ctx context.Context,
|
|
||||||
tx pg.Conn,
|
subject, textBody, htmlBody, err := emailPresenter.RenderTrustCenterAccess(organization.Name)
|
||||||
name string,
|
|
||||||
email mail.Addr,
|
|
||||||
companyName string,
|
|
||||||
accessURL string,
|
|
||||||
) error {
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
|
||||||
s.svc.baseURL,
|
|
||||||
name,
|
|
||||||
companyName,
|
|
||||||
accessURL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render trust center access email: %w", err)
|
return fmt.Errorf("cannot render trust center access email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessEmail := coredata.NewEmail(
|
accessEmail := coredata.NewEmail(
|
||||||
name,
|
access.Name,
|
||||||
email,
|
access.Email,
|
||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ package probo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"go.gearno.de/crypto/uuid"
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/packages/emails"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/validator"
|
"go.probo.inc/probo/pkg/validator"
|
||||||
@@ -568,3 +571,90 @@ func (s TrustCenterService) GenerateDarkLogoURL(
|
|||||||
|
|
||||||
return &presignedURL, nil
|
return &presignedURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, compliancePageID gid.GID) (emails.PresenterConfig, error) {
|
||||||
|
var (
|
||||||
|
compliancePage = &coredata.TrustCenter{}
|
||||||
|
organization = &coredata.Organization{}
|
||||||
|
customDomain *coredata.CustomDomain
|
||||||
|
logoFile = &coredata.File{}
|
||||||
|
emailPresenterCfg = emails.DefaultPresenterConfig(s.svc.baseURL)
|
||||||
|
)
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(compliancePageID)
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if err := logoFile.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load logoFile: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := organization.LoadByID(ctx, conn, scope, compliancePage.OrganizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
customDomain = &coredata.CustomDomain{}
|
||||||
|
if err := customDomain.LoadByOrganizationID(ctx, conn, scope, s.svc.encryptionKey, organization.ID); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load custom domain: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedBaseURL, err := url.Parse(s.svc.baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot parse base URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := url.URL{
|
||||||
|
Scheme: parsedBaseURL.Scheme,
|
||||||
|
Host: parsedBaseURL.Host,
|
||||||
|
Path: "/trust/" + compliancePage.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive {
|
||||||
|
baseURL.Host = customDomain.Domain
|
||||||
|
baseURL.Scheme = "https"
|
||||||
|
baseURL.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.BaseURL = baseURL.String()
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if logoFile.FileKey == "" {
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, logoFile, 1*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyLogoURL = presignedURL
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyName = organization.Name
|
||||||
|
|
||||||
|
if organization.WebsiteURL != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyWebsiteURL = *organization.WebsiteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization.HeadquarterAddress != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.probo.inc/probo/pkg/baseurl"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/iam"
|
"go.probo.inc/probo/pkg/iam"
|
||||||
@@ -161,39 +160,11 @@ func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMa
|
|||||||
|
|
||||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||||
|
|
||||||
organization, err := r.iam.OrganizationService.GetOrganization(ctx, trustCenter.OrganizationID)
|
|
||||||
if err != nil {
|
|
||||||
var errNotFound *iam.ErrOrganizationNotFound
|
|
||||||
if errors.As(err, &errNotFound) {
|
|
||||||
return nil, gqlutils.NotFoundf(ctx, "organization not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
customDomain, err := r.trust.GetCustomDomainByOrganizationID(ctx, organization.ID)
|
|
||||||
if err != nil && !errors.Is(err, trust.ErrCustomDomainNotFound) {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get custom domain", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &iam.SendMagicLinkRequest{
|
req := &iam.SendMagicLinkRequest{
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
}
|
CompliancePageID: &trustCenter.ID,
|
||||||
|
OrganizationID: trustCenter.OrganizationID,
|
||||||
if customDomain != nil {
|
URLPath: "verify-magic-link",
|
||||||
baseURL, err := baseurl.Parse(fmt.Sprintf("https://%s", customDomain.Domain))
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot parse custom domain url", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.BaseURL = baseURL.WithPath("/verify-magic-link")
|
|
||||||
} else {
|
|
||||||
req.BaseURL = r.baseURL.WithPath(
|
|
||||||
fmt.Sprintf("/trust/%s/verify-magic-link", trustCenter.ID.String()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.iam.AuthService.SendMagicLink(ctx, req); err != nil {
|
if err := r.iam.AuthService.SendMagicLink(ctx, req); err != nil {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
@@ -478,36 +477,6 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
|
|||||||
return fmt.Errorf("cannot load organization: %w", err)
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURLParsed, err := url.Parse(s.svc.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname := baseURLParsed.Host
|
|
||||||
scheme := baseURLParsed.Scheme
|
|
||||||
path := "/trust/" + trustCenter.Slug
|
|
||||||
|
|
||||||
if organization.CustomDomainID != nil {
|
|
||||||
customDomain, err := s.svc.Organizations.GetOrganizationCustomDomain(ctx, organization.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load custom domain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomain == nil || customDomain.SSLStatus != coredata.CustomDomainSSLStatusActive {
|
|
||||||
return fmt.Errorf("custom domain is not active")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname = customDomain.Domain
|
|
||||||
scheme = "https"
|
|
||||||
path = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
accessURL := url.URL{
|
|
||||||
Scheme: scheme,
|
|
||||||
Host: hostname,
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
access.UpdatedAt = now
|
access.UpdatedAt = now
|
||||||
|
|
||||||
@@ -515,30 +484,21 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
|
|||||||
return fmt.Errorf("cannot update trust center access with expiration: %w", err)
|
return fmt.Errorf("cannot update trust center access with expiration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.sendTrustCenterAccessEmail(ctx, tx, access.Name, access.Email, organization.Name, accessURL.String())
|
emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID)
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get compliance page email presenter config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TrustCenterAccessService) sendTrustCenterAccessEmail(
|
emailPresenter := emails.NewPresenterFromConfig(emailPresenterCfg, access.Name)
|
||||||
ctx context.Context,
|
|
||||||
tx pg.Conn,
|
subject, textBody, htmlBody, err := emailPresenter.RenderTrustCenterAccess(organization.Name)
|
||||||
name string,
|
|
||||||
email mail.Addr,
|
|
||||||
companyName string,
|
|
||||||
accessURL string,
|
|
||||||
) error {
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
|
||||||
s.svc.baseURL,
|
|
||||||
name,
|
|
||||||
companyName,
|
|
||||||
accessURL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render trust center access email: %w", err)
|
return fmt.Errorf("cannot render trust center access email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessEmail := coredata.NewEmail(
|
accessEmail := coredata.NewEmail(
|
||||||
name,
|
access.Name,
|
||||||
email,
|
access.Email,
|
||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
@@ -648,11 +608,16 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderTrustCenterDocumentAccessRejected(
|
emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID)
|
||||||
s.svc.baseURL,
|
if err != nil {
|
||||||
access.Name,
|
return fmt.Errorf("cannot get compliance page email presenter config: %w", err)
|
||||||
organization.Name,
|
}
|
||||||
|
|
||||||
|
emailPresenter := emails.NewPresenterFromConfig(emailPresenterCfg, access.Name)
|
||||||
|
|
||||||
|
subject, textBody, htmlBody, err := emailPresenter.RenderTrustCenterDocumentAccessRejected(
|
||||||
fileNames,
|
fileNames,
|
||||||
|
organization.Name,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render trust center documents access rejected email: %w", err)
|
return fmt.Errorf("cannot render trust center documents access rejected email: %w", err)
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ package trust
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/packages/emails"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
@@ -216,3 +219,92 @@ func (s TrustCenterService) GenerateDarkLogoURL(
|
|||||||
|
|
||||||
return &presignedURL, nil
|
return &presignedURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, compliancePageID gid.GID) (emails.PresenterConfig, error) {
|
||||||
|
var (
|
||||||
|
compliancePage = &coredata.TrustCenter{}
|
||||||
|
organization = &coredata.Organization{}
|
||||||
|
customDomain *coredata.CustomDomain
|
||||||
|
logoFile = &coredata.File{}
|
||||||
|
emailPresenterCfg = emails.DefaultPresenterConfig(s.svc.baseURL)
|
||||||
|
)
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(compliancePageID)
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if err := logoFile.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load logoFile: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := organization.LoadByID(ctx, conn, scope, compliancePage.OrganizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
customDomain = &coredata.CustomDomain{}
|
||||||
|
if err := customDomain.LoadByOrganizationID(ctx, conn, scope, s.svc.encryptionKey, organization.ID); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load custom domain: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedBaseURL, err := url.Parse(s.svc.baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot parse base URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := url.URL{
|
||||||
|
Scheme: parsedBaseURL.Scheme,
|
||||||
|
Host: parsedBaseURL.Host,
|
||||||
|
Path: "/trust/" + compliancePage.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive {
|
||||||
|
baseURL.Host = customDomain.Domain
|
||||||
|
baseURL.Scheme = "https"
|
||||||
|
baseURL.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.BaseURL = baseURL.String()
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if logoFile.FileKey == "" {
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If logo exists, then we will brand the emails with the org as a sender
|
||||||
|
|
||||||
|
presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, logoFile, 1*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return emailPresenterCfg, fmt.Errorf("cannot generate file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyLogoURL = presignedURL
|
||||||
|
|
||||||
|
emailPresenterCfg.SenderCompanyName = organization.Name
|
||||||
|
|
||||||
|
if organization.WebsiteURL != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyWebsiteURL = *organization.WebsiteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization.HeadquarterAddress != nil {
|
||||||
|
emailPresenterCfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailPresenterCfg, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user