Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
78b1cfc
starting email template
whaught Oct 13, 2020
6cf444d
first thing
whaught Oct 14, 2020
3613515
email settings
whaught Oct 14, 2020
d2ec5ab
Merge remote-tracking branch 'upstream/main' into realm-email-setting
whaught Oct 15, 2020
3a1dd10
fixes form integrate
whaught Oct 15, 2020
eca29c8
update migrations
whaught Oct 15, 2020
1563e26
some feedback
whaught Oct 15, 2020
b953abf
words
whaught Oct 15, 2020
5bb6a21
remaining migration
whaught Oct 15, 2020
1476644
fix form fields
whaught Oct 15, 2020
48df05a
default port
whaught Oct 15, 2020
257a98f
provider type
whaught Oct 15, 2020
996fe11
actually save settings
whaught Oct 15, 2020
0d353be
tested
whaught Oct 15, 2020
32101e5
Merge remote-tracking branch 'upstream/main' into use-realm-email
whaught Oct 16, 2020
7457237
use realm emailer to send mail
whaught Oct 16, 2020
57011e8
fix error
whaught Oct 16, 2020
80a1276
sned invite
whaught Oct 16, 2020
48300a7
handle verification email
whaught Oct 16, 2020
a5a0274
reset password from system
whaught Oct 16, 2020
4ca5a9a
lint
whaught Oct 16, 2020
3c1576d
review stuff
whaught Oct 16, 2020
b80e341
nils
whaught Oct 16, 2020
8f5e42a
review stuff
whaught Oct 16, 2020
dc80624
merge send realm email code
whaught Oct 16, 2020
78f7f95
expect notfound
whaught Oct 16, 2020
510812a
unit test
whaught Oct 16, 2020
e0cbda5
json ignore
whaught Oct 16, 2020
351b0a1
ignore on sms
whaught Oct 16, 2020
9464021
only fallback on error
whaught Oct 16, 2020
1cab802
fix notfound error message
whaught Oct 19, 2020
0c2a1e3
Merge remote-tracking branch 'upstream/main' into use-realm-email
whaught Oct 19, 2020
532e9e0
correct import
whaught Oct 19, 2020
cc7d8dd
ensure new port 587
whaught Oct 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmd/server/assets/email/default_email_verify.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- define "email/verifyemail" -}}
{{template "email/plainheader" .}}

Hello,

Click the link below to verify your email address for {{.RealmName}} on the COVID-19 exposure notifications verification server:

{{.VerifyLink}}

If you did not request verification of this email, please disregard this message.
{{end}}
11 changes: 11 additions & 0 deletions cmd/server/assets/email/default_reset_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- define "email/passwordresetemail" -}}
{{template "email/plainheader" .}}

Hello,

Click the link below to reset your password for the COVID-19 exposure notifications verification server.

{{.ResetLink}}

If you did not request a password reset, please disregard this message.
{{end}}
31 changes: 17 additions & 14 deletions cmd/server/assets/login/verify-email.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@
<p>Email address ownership for <em>{{.currentUser.Email}}</em> is <strong id="not">not</strong> confirmed.
</p>

<button id="verify" class="btn btn-primary btn-block" disabled>Send verification email</button>
<form method="POST">
<button id="verify" class="btn btn-primary btn-block" disabled>Send verification email</button>

<small class="form-text text-muted">Click to send an email containing a verification
link.</small>
<small class="form-text text-muted">
Click to send an email containing a verification link.
</small>
</form>

{{if ne .currentRealm.EmailVerifiedMode.String "required"}}
<a id="skip" class="card-link float-right mt-3" href="/home">Skip for now</a>
Expand All @@ -52,26 +55,26 @@
let $skip = $('#skip');
let $not = $('#not');

{{if eq .emailVerify "send"}}
$(function() {
$verify.on('click', function(event) {
let user = firebase.auth().currentUser
if (!user.emailVerified) {
user.sendEmailVerification().then(function() {
flash.clear();
flash.alert('Verification email sent.');
$verify.prop('disabled', true);
});
}
});
let user = firebase.auth().currentUser
if (!user.emailVerified) {
user.sendEmailVerification().then(function() {
flash.clear();
flash.alert('Verification email sent.');
$verify.prop('disabled', true);
});
}
});
{{end}}

firebase.auth().onAuthStateChanged(function(user) {
if (!user) {
window.location.assign("/signout");
return;
}

if (user.emailVerified) {
if ({{if eq .emailVerify "sent"}}true{{else}}user.emailVerified{{end}}) {
$not.hide();
$skip.text("Go home");
} else {
Expand Down
89 changes: 89 additions & 0 deletions cmd/server/assets/realmadmin/_form_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{{define "realmadmin/_form_email"}}

{{$realm := .realm}}
{{$emailConfig := .emailConfig}}

<p class="mb-4">
These are the settings for configuring an SMTP email provider. The verification server
will use this email account to send invitations, password resets, and account-verifications
for the realm.
</p>

<form method="POST" action="/realm/settings#email" class="floating-form">
{{ .csrfField }}
<input type="hidden" name="email" value="1" />

{{if $realm.CanUseSystemEmailConfig}}
{{if $realm.UseSystemEmailConfig}}
<div class="alert alert-danger">
<strong>Warning!</strong> You are currently using the system-provided
SMTP email configuration. Specifying values below will override the system
configuration to use your supplied credentials.
</div>
{{end}}

<div class="form-group form-check">
<input type="checkbox" name="use_system_smtp_config" id="use-system-smtp-config" class="form-check-input" value="1" {{if $realm.UseSystemEmailConfig}} checked{{end}}
data-toggle="collapse" data-target="#smtp-form">
<label class="form-check-label" for="use-system-smtp-config">
Use system SMTP configuration
</label>
</div>
{{end}}

<div id="smtp-form" class="collapse{{if not $realm.UseSystemEmailConfig}} show{{end}}">
<div class="form-label-group">
<input type="text" name="smtp_account" id="smtp-account" class="form-control text-monospace{{if $emailConfig.ErrorsFor "smtpAccount"}} is-invalid{{end}}"
placeholder="SMTP account" value="{{if $emailConfig}}{{$emailConfig.SMTPAccount}}{{end}}" />
<label for="smtp-account">SMTP account</label>
{{template "errorable" $emailConfig.ErrorsFor "smtpAccount"}}
<small class="form-text text-muted">
This is the SMTP email account eg. noreply@example.com
</small>
</div>

<div class="form-label-group">
<div class="input-group">
<input type="password" name="smtp_password" id="smtp-password" class="form-control text-monospace{{if $emailConfig.ErrorsFor "smtpPassword"}} is-invalid{{end}}" autocomplete="new-password"
placeholder="SMTP password" value="{{if $emailConfig}}{{$emailConfig.SMTPPassword}}{{end}}">
<label for="smtp-password">SMTP password</label>
<div class="input-group-append">
<a class="input-group-text" data-toggle-password="smtp-password">
<span class="oi oi-lock-locked" aria-hidden="true"></span>
</a>
</div>
</div>
{{template "errorable" $emailConfig.ErrorsFor "smtpPassword"}}
<small class="form-text text-muted">
This is the password for your SMTP email.
</small>
</div>

<div class="form-label-group">
<input name="smtp_host" id="smtp-host" class="form-control text-monospace{{if $emailConfig.ErrorsFor "smtpPort"}} is-invalid{{end}}"
placeholder="SMTP host" value="{{if $emailConfig}}{{$emailConfig.SMTPHost}}{{end}}" />
<label for="smtp-port">SMTP host</label>
{{template "errorable" $emailConfig.ErrorsFor "smtpHost"}}
<small class="form-text text-muted">
SMTP host is the hostname for the SMTP server.
</small>
</div>

<div class="form-label-group">
<input name="smtp_port" id="smtp-port" class="form-control text-monospace{{if $emailConfig.ErrorsFor "smtpPort"}} is-invalid{{end}}"
placeholder="SMTP port" value="{{if $emailConfig}}{{$emailConfig.SMTPPort}}{{else}}587{{end}}" />
<label for="smtp-port">SMTP port</label>
{{template "errorable" $emailConfig.ErrorsFor "smtpPort"}}
<small class="form-text text-muted">
SMTP port is the port number to connect to.
587 is the default port for SMTP, and legacy port 25 is blocked.
</small>
</div>
</div>

<div class="mt-4">
<input type="submit" class="btn btn-primary btn-block" value="Update SMTP settings" />
</div>
</form>

{{end}}
6 changes: 6 additions & 0 deletions cmd/server/assets/realmadmin/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ <h1>Realm settings</h1>
<li class="nav-item" role="presentation">
<a class="nav-link" id="sms-tab" data-toggle="tab" href="#sms" role="tab" aria-controls="sms" aria-selected="false">SMS</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="email-tab" data-toggle="tab" href="#email" role="tab" aria-controls="email" aria-selected="false">Email</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="security-tab" data-toggle="tab" href="#security" role="tab" aria-controls="security" aria-selected="false">Security</a>
</li>
Expand All @@ -64,6 +67,9 @@ <h1>Realm settings</h1>
<div class="tab-pane" id="sms" role="tabpanel" aria-labelledby="sms-tab">
{{template "realmadmin/_form_sms" .}}
</div>
<div class="tab-pane" id="email" role="tabpanel" aria-labelledby="email-tab">
{{template "realmadmin/_form_email" .}}
</div>
<div class="tab-pane" id="security" role="tabpanel" aria-labelledby="security-tab">
{{template "realmadmin/_form_security" .}}
</div>
Expand Down
17 changes: 4 additions & 13 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/controller/realmadmin"
"github.com/google/exposure-notifications-verification-server/pkg/controller/realmkeys"
"github.com/google/exposure-notifications-verification-server/pkg/controller/user"
"github.com/google/exposure-notifications-verification-server/pkg/email"
"github.com/google/exposure-notifications-verification-server/pkg/ratelimit"
"github.com/google/exposure-notifications-verification-server/pkg/ratelimit/limitware"
"github.com/google/exposure-notifications-verification-server/pkg/render"
Expand Down Expand Up @@ -156,16 +155,6 @@ func realMain(ctx context.Context) error {
return fmt.Errorf("failed to create renderer: %w", err)
}

// Setup server emailer
var emailer email.Provider
if cfg.Email.HasSMTPCreds() {
cfg.Email.ProviderType = email.ProviderTypeSMTP
emailer, err = email.ProviderFor(ctx, &cfg.Email)
if err != nil {
return fmt.Errorf("failed to configure internal firebase client: %w", err)
}
}

// Rate limiting
limiterStore, err := ratelimit.RateLimiterFor(ctx, &cfg.RateLimit)
if err != nil {
Expand Down Expand Up @@ -240,7 +229,7 @@ func realMain(ctx context.Context) error {
Queries("oobCode", "", "mode", "resetPassword").Methods("GET")
sub.Handle("/login/manage-account", loginController.HandleSubmitNewPassword()).
Queries("oobCode", "", "mode", "resetPassword").Methods("POST")
sub.Handle("/login/manage-account", loginController.HandleSubmitVerifyEmail()).
sub.Handle("/login/manage-account", loginController.HandleReceiveVerifyEmail()).
Queries("oobCode", "{oobCode:.+}", "mode", "{mode:(?:verifyEmail|recoverEmail)}").Methods("GET")
sub.Handle("/session", loginController.HandleCreateSession()).Methods("POST")
sub.Handle("/signout", loginController.HandleSignOut()).Methods("GET")
Expand All @@ -266,6 +255,8 @@ func realMain(ctx context.Context) error {
sub.Use(processFirewall)
sub.Handle("/login/manage-account", loginController.HandleShowVerifyEmail()).
Queries("mode", "verifyEmail").Methods("GET")
sub.Handle("/login/manage-account", loginController.HandleSubmitVerifyEmail()).
Queries("mode", "verifyEmail").Methods("POST")

// SMS auth registration is realm-specific, so it needs to load the current realm.
sub = r.PathPrefix("").Subrouter()
Expand Down Expand Up @@ -374,7 +365,7 @@ func realMain(ctx context.Context) error {
userSub.Use(requireMFA)
userSub.Use(rateLimit)

userController := user.New(ctx, firebaseInternal, auth, emailer, cacher, cfg, db, h)
userController := user.New(ctx, firebaseInternal, auth, cacher, cfg, db, h)
userSub.Handle("", userController.HandleIndex()).Methods("GET")
userSub.Handle("", userController.HandleIndex()).
Queries("offset", "{[0-9]*}", "email", "").Methods("GET")
Expand Down
2 changes: 0 additions & 2 deletions pkg/config/server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/email"
"github.com/google/exposure-notifications-verification-server/pkg/ratelimit"

"github.com/google/exposure-notifications-server/pkg/observability"
Expand Down Expand Up @@ -52,7 +51,6 @@ type ServerConfig struct {
Database database.Config
Observability observability.Config
Cache cache.Config
Email email.Config

Port string `env:"PORT,default=8080"`

Expand Down
89 changes: 89 additions & 0 deletions pkg/controller/email_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ import (
"fmt"

"firebase.google.com/go/auth"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/email"
"github.com/google/exposure-notifications-verification-server/pkg/render"
)

type ComposeFn func(provider email.Provider, realm *database.Realm, toEmail string) ([]byte, error)

// ComposeInviteEmail uses the renderer and auth client to generate a password reset link
// and emit an invite email
func ComposeInviteEmail(
Expand Down Expand Up @@ -51,3 +55,88 @@ func ComposeInviteEmail(
}
return message, nil
}

// ComposeEmailVerifyEmail uses the renderer and auth client to generate an email to verify
// the user's email address.
func ComposeEmailVerifyEmail(
ctx context.Context,
h *render.Renderer,
auth *auth.Client, toEmail, fromEmail, realmName string) ([]byte, error) {
verifyLink, err := auth.EmailVerificationLink(ctx, toEmail)
if err != nil {
return nil, fmt.Errorf("failed generating verification link: %w", err)
}

// Compose message
message, err := h.RenderEmail("email/verifyemail",
struct {
ToEmail string
FromEmail string
VerifyLink string
RealmName string
}{
ToEmail: toEmail,
FromEmail: fromEmail,
VerifyLink: verifyLink,
RealmName: realmName,
})
if err != nil {
return nil, fmt.Errorf("failed rendering email verification template: %w", err)
}
return message, nil
}

// ComposePasswordResetEmail uses the renderer and auth client to generate an email for resetting
// a user password
func ComposePasswordResetEmail(
ctx context.Context,
h *render.Renderer,
auth *auth.Client, toEmail, fromEmail string) ([]byte, error) {
resetLink, err := auth.PasswordResetLink(ctx, toEmail)
if err != nil {
return nil, fmt.Errorf("failed generating password reset link: %w", err)
}

// Compose message
message, err := h.RenderEmail("email/passwordresetemail",
struct {
ToEmail string
FromEmail string
ResetLink string
RealmName string
}{
ToEmail: toEmail,
FromEmail: fromEmail,
ResetLink: resetLink,
})
if err != nil {
return nil, fmt.Errorf("failed rendering password reset template: %w", err)
}
return message, nil
}

func SendRealmEmail(ctx context.Context, db *database.Database, compose ComposeFn, toEmail string) (bool, error) {
realm := RealmFromContext(ctx)
if realm == nil {
return false, nil
}

emailer, err := realm.EmailProvider(db)
if err != nil {
if database.IsNotFound(err) {
return false, nil
}
return false, fmt.Errorf("failed creating email provider: %w", err)
}

message, err := compose(emailer, realm, toEmail)
if err != nil {
return false, fmt.Errorf("failed composing email verification: %w", err)
}

if err := emailer.SendEmail(ctx, toEmail, message); err != nil {
return false, fmt.Errorf("failed sending email verification: %w", err)
}

return true, nil
}
Loading