Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Create an account settings page #599

Merged
merged 8 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
1 change: 1 addition & 0 deletions cmd/server/assets/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ <h6 class="dropdown-header">Actions</h6>
{{if gt (len .currentUser.Realms) 1}}
<a class="dropdown-item" href="/login/select-realm">Change realm</a>
{{end}}
<a class="dropdown-item" href="/account">My account</a>
<a class="dropdown-item" href="/signout">Sign out</a>
</div>
</li>
Expand Down
112 changes: 112 additions & 0 deletions cmd/server/assets/login/account.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{{define "account"}}

{{$user := .currentUser}}

<!doctype html>
<html lang="en">

<head>
{{template "head" .}}
{{template "firebase" .}}
</head>

<body class="bg-light">
{{template "navbar" .}}

<main role="main" class="container">
{{template "flash" .}}

<h1>My Account</h1>
<p>Information and settings for your account</p>

<div class="card mb-3 shadow-sm">
<div class="card-header">Details</div>
<div class="card-body">
<h6 class="card-title">Name</h6>
<div class="card-text mb-3">
{{$user.Name}}
</div>

<h6 class="card-title">Email</h6>
<div class="card-text">
{{$user.Email}}
</div>

{{if $user.Admin}}
<h6 class="card-title mt-3">System admin</h6>
<div class="card-text text-success">Enabled</div>
{{end}}
</div>
</div>

<div class="card mb-3 shadow-sm">
<div class="card-header">Authentication</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<div class="card-text" id="email-verified">loading</div>
</li>
<li class="list-group-item">
<div class="card-text" id="phone-registered">loading</div>
</li>
<li class="list-group-item">
<div class="card-text">Password was last changed <span class="text-info">{{$user.PasswordAgeString}}</span>
ago</div>
<a href="/login/change-password" class="card-link">Reset password</a>
</li>
</ul>
</div>

<div class="card mb-3 shadow-sm">
<div class="card">
<div class="card-header">Member of realms</div>
<ul class="list-group list-group-flush">
{{range $realm := $user.Realms}}
<li class="list-group-item">
{{$realm.Name}}
{{range $admin := $user.AdminRealms}}
{{if eq $admin.ID $realm.ID}}
<span class="badge badge-primary">Admin</span>
{{end}}
{{end}}
</li>
{{end}}
</ul>
</div>
</div>
</main>

{{template "scripts" .}}
<script type="text/javascript">
$(function() {
let $emailVer = $('#email-verified');
let $emailVerLink = $('#email-verified-link');
let $phoneReg = $('#phone-registered');
let $phoneRegLink = $('#register-phone-link');

firebase.auth().onAuthStateChanged(function(user) {
if (!user) {
return
}

if (user.multiFactor.enrolledFactors.length > 0) {
$phoneReg.html('Two-factor auth is <span class="text-success">enabled</span>');
} else {
$phoneReg.addClass("text-danger");
$phoneReg.html('No second auth factor registered');
$phoneReg.after('<a href="/login/register-phone" class="card-link">Register phone</a>');
}

if (user.emailVerified) {
$emailVer.html('Email address is <span class="text-success">verified</span>');
} else {
$emailVer.addClass("text-danger");
$emailVer.html('Email address is <strong>not</strong> verified');
$emailVer.after('<a href="/login/verify-email" class="card-link">Verify email</a>');
}
});
});
</script>
</body>

</html>
{{end}}
8 changes: 4 additions & 4 deletions cmd/server/assets/login/verify-email.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

<head>
{{template "head" .}}

{{template "firebase" .}}
</head>

Expand All @@ -28,7 +27,7 @@
</div>
{{end}}

<p>Email address ownership for <em>{{.currentUser.Email}}</em> is <strong>not</strong> confirmed.
<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>
Expand All @@ -51,6 +50,7 @@
<script>
let $verify = $('#verify');
let $skip = $('#skip');
let $not = $('#not');

$(function() {
$verify.on('click', function(event) {
Expand All @@ -72,8 +72,8 @@
}

if (user.emailVerified) {
window.location.assign('/home');
return;
$not.hide();
$skip.text("Go home");
} else {
$verify.prop('disabled', false);
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,14 @@ func realMain(ctx context.Context) error {
sub.Handle("/session", loginController.HandleCreateSession()).Methods("POST")
sub.Handle("/signout", loginController.HandleSignOut()).Methods("GET")

// Realm selection
// Realm selection & account settings
sub = r.PathPrefix("").Subrouter()
sub.Use(requireAuth)
sub.Use(rateLimit)
sub.Use(loadCurrentRealm)
sub.Handle("/login/select-realm", loginController.HandleSelectRealm()).Methods("GET", "POST")
sub.Handle("/login/change-password", loginController.HandleResetPassword()).Methods("GET")
sub.Handle("/account", loginController.HandleAccountSettings()).Methods("GET")

// Verifying email requires the user is logged in
sub = r.PathPrefix("").Subrouter()
Expand Down
32 changes: 32 additions & 0 deletions pkg/controller/login/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package login defines the controller for the login page.
package login

import (
"net/http"

"github.com/google/exposure-notifications-verification-server/pkg/controller"
)

func (c *Controller) HandleAccountSettings() http.Handler {
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

m := controller.TemplateMapFromContext(ctx)
m["firebase"] = c.config.Firebase
c.h.RenderHTML(w, "account", m)
})
}
16 changes: 16 additions & 0 deletions pkg/database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ type User struct {
LastPasswordChange time.Time
}

// PasswordAgeString displays the age of the password in friendly text.
func (u *User) PasswordAgeString() string {
ago := time.Now().Sub(u.LastPasswordChange)
whaught marked this conversation as resolved.
Show resolved Hide resolved
h := ago.Hours()
if h > 48 {
return fmt.Sprintf("%v days", int(h/24))
}
if h > 2 {
return fmt.Sprintf("%d hours", int(h))
}
if ago.Minutes() > 2 {
return fmt.Sprintf("%d minutes", int(ago.Minutes()))
}
return fmt.Sprintf("%d minutes", int(ago.Seconds()))
}

// BeforeSave runs validations. If there are errors, the save fails.
func (u *User) BeforeSave(tx *gorm.DB) error {
u.Email = strings.TrimSpace(u.Email)
Expand Down