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 all 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 @@ -256,6 +256,7 @@ <h6 class="dropdown-header">Actions</h6>
{{if gt (len .currentUser.Realms) 1}}
<a class="dropdown-item" href="/login/select-realm">{{if .currentRealm}}Change realm{{else}}Select realm{{end}}</a>
{{end}}
<a class="dropdown-item" href="/account">My account</a>
<a class="dropdown-item" href="/signout">Sign out</a>
</div>
</li>
Expand Down
122 changes: 122 additions & 0 deletions cmd/server/assets/login/account.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{{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}}

{{- /* system admins can remove themselves from realms */ -}}
{{if $user.Admin}}
<a href="/users/{{.ID}}" class="d-block text-danger float-right" data-method="DELETE"
data-confirm="Are you sure you want to leave {{.Name}}?">
<span class="oi oi-account-logout" aria-hidden="true"></span>
Leave realm
</a>
{{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.Since(u.LastPasswordChange)
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