diff --git a/.env.template b/.env.template index aa62c62c..32655a8f 100644 --- a/.env.template +++ b/.env.template @@ -25,4 +25,23 @@ TRISA_TRP_ENABLED=true TRISA_TRP_BIND_ADDR=:8200 TRISA_TRP_USE_MTLS=false TRISA_TRP_POOL=.secret/localhost.pem.gz -TRISA_TRP_CERTS=.secret/server.pem.gz \ No newline at end of file +TRISA_TRP_CERTS=.secret/server.pem.gz + +TRISA_TRP_IDENTITY_VASP_NAME="Alice VASP" +TRISA_TRP_IDENTITY_LEI="254900OPPU84GM83MG36" + +# If Sunrise is enabled an email configuration must be specified +TRISA_SUNRISE_ENABLED=true + +# Email configuration +TRISA_EMAIL_SENDER="AliceVASP Compliance " + +# Specify either SendGrid or SMTP +# SendGrid configuration +TRISA_EMAIL_SENDGRID_API_KEY="" + +# SMTP Configuration +TRISA_EMAIL_SMTP_HOST="smtp.alice.us" +TRISA_EMAIL_SMTP_PORT=587 +TRISA_EMAIL_SMTP_USERNAME="admin" +TRISA_EMAIL_SMTP_PASSWORD="password" diff --git a/pkg/emails/emails.go b/pkg/emails/emails.go index 14d80793..0ae93872 100644 --- a/pkg/emails/emails.go +++ b/pkg/emails/emails.go @@ -32,8 +32,14 @@ const ( initialInterval = 2500 * time.Millisecond ) -// Configure the package to start sending emails. +// Configure the package to start sending emails. If there is no valid email +// configuration available then configuration is gracefully ignored without error. func Configure(conf Config) (err error) { + // Do not configure email if it is not available but also do not return an error. + if !conf.Available() { + return nil + } + if err = conf.Validate(); err != nil { return err } diff --git a/pkg/node/node.go b/pkg/node/node.go index b06ed071..cf7ff5ec 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -9,6 +9,7 @@ import ( "github.com/trisacrypto/envoy/pkg/config" "github.com/trisacrypto/envoy/pkg/directory" + "github.com/trisacrypto/envoy/pkg/emails" "github.com/trisacrypto/envoy/pkg/logger" "github.com/trisacrypto/envoy/pkg/metrics" "github.com/trisacrypto/envoy/pkg/store" @@ -81,6 +82,11 @@ func New(conf config.Config) (node *Node, err error) { node.webhook = webhook.New(conf.Webhook()) } + // Configure email if it's available + if err = emails.Configure(conf.Email); err != nil { + return nil, err + } + // Create the TRISA management system if node.network, err = network.New(conf.Node); err != nil { return nil, err diff --git a/pkg/store/models/transaction.go b/pkg/store/models/transaction.go index 276e2d02..ca8c7e6b 100644 --- a/pkg/store/models/transaction.go +++ b/pkg/store/models/transaction.go @@ -90,6 +90,8 @@ type PreparedTransaction interface { Update(*Transaction) error // Update the transaction with new information; e.g. data from decryption AddCounterparty(*Counterparty) error // Add counterparty by database ULID, counterparty name, or registered directory ID; if the counterparty doesn't exist, it is created AddEnvelope(*SecureEnvelope) error // Associate a secure envelope with the prepared transaction + CreateSunrise(*Sunrise) error // Create a sunrise message sent to the counterparty for the transaction + UpdateSunrise(*Sunrise) error // Update the sunrise message Rollback() error // Rollback the prepared transaction and conclude it Commit() error // Commit the prepared transaction and conclude it } diff --git a/pkg/store/sqlite/migrations/0005_sunrise_tokens.sql b/pkg/store/sqlite/migrations/0005_sunrise_tokens.sql index 63f49316..2a770f87 100644 --- a/pkg/store/sqlite/migrations/0005_sunrise_tokens.sql +++ b/pkg/store/sqlite/migrations/0005_sunrise_tokens.sql @@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS sunrise ( envelope_id TEXT NOT NULL, email TEXT NOT NULL, expiration DATETIME NOT NULL, - signature BLOB NOT NULL, + signature BLOB DEFAULT NULL, status TEXT NOT NULL, sent_on DATETIME DEFAULT NULL, verified_on DATETIME DEFAULT NULL, diff --git a/pkg/store/sqlite/sunrise.go b/pkg/store/sqlite/sunrise.go index bd77343e..a069be02 100644 --- a/pkg/store/sqlite/sunrise.go +++ b/pkg/store/sqlite/sunrise.go @@ -59,6 +59,8 @@ const createSunriseSQL = "INSERT INTO sunrise (id, envelope_id, email, expiratio // Create a sunrise message in the database. func (s *Store) CreateSunrise(ctx context.Context, msg *models.Sunrise) (err error) { // Basic validation + // Note: this is duplicated in updateSunrise but better to check before starting a + // transaction that will take up system resources. if !ulids.IsZero(msg.ID) { return dberr.ErrNoIDOnCreate } @@ -69,6 +71,22 @@ func (s *Store) CreateSunrise(ctx context.Context, msg *models.Sunrise) (err err } defer tx.Rollback() + if err = createSunrise(tx, msg); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + return nil +} + +func createSunrise(tx *sql.Tx, msg *models.Sunrise) (err error) { + // Basic validation + if !ulids.IsZero(msg.ID) { + return dberr.ErrNoIDOnCreate + } + // Create IDs and model metadata, updating the sunrise message in place. msg.ID = ulids.New() msg.Created = time.Now() @@ -79,10 +97,6 @@ func (s *Store) CreateSunrise(ctx context.Context, msg *models.Sunrise) (err err // TODO: handle constraint violations return err } - - if err = tx.Commit(); err != nil { - return err - } return nil } @@ -120,6 +134,8 @@ const updateSunriseSQL = "UPDATE sunrise SET envelope_id=:envelopeID, email=:ema // Update sunrise message information. func (s *Store) UpdateSunrise(ctx context.Context, msg *models.Sunrise) (err error) { // Basic validation + // Note: this is duplicated in updateSunrise but better to check before starting a + // transaction that will take up system resources. if ulids.IsZero(msg.ID) { return dberr.ErrMissingID } @@ -130,6 +146,22 @@ func (s *Store) UpdateSunrise(ctx context.Context, msg *models.Sunrise) (err err } defer tx.Rollback() + if err = updateSunrise(tx, msg); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + return nil +} + +func updateSunrise(tx *sql.Tx, msg *models.Sunrise) (err error) { + // Basic validation + if ulids.IsZero(msg.ID) { + return dberr.ErrMissingID + } + // Update modified timestamp (in place). msg.Modified = time.Now() @@ -142,9 +174,6 @@ func (s *Store) UpdateSunrise(ctx context.Context, msg *models.Sunrise) (err err return dberr.ErrNotFound } - if err = tx.Commit(); err != nil { - return err - } return nil } diff --git a/pkg/store/sqlite/transactions.go b/pkg/store/sqlite/transactions.go index 5dbddaad..c19a3bc2 100644 --- a/pkg/store/sqlite/transactions.go +++ b/pkg/store/sqlite/transactions.go @@ -608,6 +608,14 @@ func (p *PreparedTransaction) AddEnvelope(in *models.SecureEnvelope) (err error) return nil } +func (p *PreparedTransaction) CreateSunrise(in *models.Sunrise) error { + return createSunrise(p.tx, in) +} + +func (p *PreparedTransaction) UpdateSunrise(in *models.Sunrise) error { + return updateSunrise(p.tx, in) +} + func (p *PreparedTransaction) Rollback() error { return p.tx.Rollback() } diff --git a/pkg/web/api/v1/transaction.go b/pkg/web/api/v1/transaction.go index 6328c1c2..2919900a 100644 --- a/pkg/web/api/v1/transaction.go +++ b/pkg/web/api/v1/transaction.go @@ -245,6 +245,9 @@ const ( colorUnspecified = "text-gray-500" tooltipUnspecified = "The transfer state is unknown or purposefully not specified." + colorDraft = "text-gray-500" + tooltipDraft = "The TRISA exchange is in a draft state and has not been sent." + colorPending = "text-yellow-700" tooltipPending = "Action is required by the sending party, await a following RPC." @@ -272,6 +275,8 @@ func (c *Transaction) ColorStatus() string { switch c.Status { case models.StatusUnspecified, "": return colorUnspecified + case models.StatusDraft: + return colorDraft case models.StatusPending: return colorPending case models.StatusReview: @@ -293,6 +298,8 @@ func (c *Transaction) TooltipStatus() string { switch c.Status { case models.StatusUnspecified, "": return tooltipUnspecified + case models.StatusDraft: + return tooltipDraft case models.StatusPending: return tooltipPending case models.StatusReview: diff --git a/pkg/web/server.go b/pkg/web/server.go index f85016c6..009dac05 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -67,7 +67,10 @@ func (s *Server) Serve(errc chan<- error) (err error) { } }() - log.Info().Str("url", s.URL()).Msg("compliance and admin web user interface started") + log.Info(). + Str("url", s.URL()). + Bool("sunrise", s.conf.Sunrise.Enabled && s.conf.Email.Available()). + Msg("compliance and admin web user interface started") return nil } diff --git a/pkg/web/sunrise.go b/pkg/web/sunrise.go index 650a470d..afb6b813 100644 --- a/pkg/web/sunrise.go +++ b/pkg/web/sunrise.go @@ -138,7 +138,7 @@ func (s *Server) SendSunrise(c *gin.Context) { } // This method will create the ID on the sunrise record - if err = s.store.CreateSunrise(ctx, record); err != nil { + if err = packet.DB.CreateSunrise(record); err != nil { c.Error(err) c.JSON(http.StatusInternalServerError, api.Error("could not complete sunrise request")) return @@ -170,7 +170,7 @@ func (s *Server) SendSunrise(c *gin.Context) { // Update the sunrise record in the database with the token and sent on timestamp record.SentOn = sql.NullTime{Valid: true, Time: time.Now()} - if err = s.store.UpdateSunrise(ctx, record); err != nil { + if err = packet.DB.UpdateSunrise(record); err != nil { c.Error(err) c.JSON(http.StatusInternalServerError, api.Error("could not complete sunrise request")) return