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

Commit

Permalink
Use local time zone when issuing a code. (#340)
Browse files Browse the repository at this point in the history
* Use local time zone when issuing a code.

Rather than requiring the user to use UTC when requesting a code, use
the client's local timezone.

Fixes #63

* Use local time zone when issuing a code.

Rather than requiring the user to use UTC when requesting a code, use
the client's local timezone.

Fixes #63
  • Loading branch information
jeremyfaller authored Aug 26, 2020
1 parent 86a05f0 commit 85a4fe8
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 30 deletions.
13 changes: 7 additions & 6 deletions cmd/get-code/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import (
)

var (
testFlag = flag.String("type", "", "diagnosis test type: confirmed, likely, negative")
onsetFlag = flag.String("onset", "", "Symptom onset date, YYYY-MM-DD format")
apikeyFlag = flag.String("apikey", "", "API Key to use")
addrFlag = flag.String("addr", "http://localhost:8080", "protocol, address and port on which to make the API call")
timeoutFlag = flag.Duration("timeout", 5*time.Second, "request time out duration in the format: 0h0m0s")
testFlag = flag.String("type", "", "diagnosis test type: confirmed, likely, negative")
onsetFlag = flag.String("onset", "", "Symptom onset date, YYYY-MM-DD format")
tzOffsetFlag = flag.Int("tzOffset", 0, "timezone adjustment (minutes) from UTC for request")
apikeyFlag = flag.String("apikey", "", "API Key to use")
addrFlag = flag.String("addr", "http://localhost:8080", "protocol, address and port on which to make the API call")
timeoutFlag = flag.Duration("timeout", 5*time.Second, "request time out duration in the format: 0h0m0s")
)

func main() {
Expand All @@ -58,7 +59,7 @@ func main() {
func realMain(ctx context.Context) error {
logger := logging.FromContext(ctx)

request, response, err := clients.IssueCode(ctx, *addrFlag, *apikeyFlag, *testFlag, *onsetFlag, *timeoutFlag)
request, response, err := clients.IssueCode(ctx, *addrFlag, *apikeyFlag, *testFlag, *onsetFlag, *tzOffsetFlag, *timeoutFlag)
logger.Infow("sent request", "request", request)
if err != nil {
return fmt.Errorf("failed to get token: %w", err)
Expand Down
15 changes: 3 additions & 12 deletions cmd/server/assets/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,25 +102,15 @@ <h1>Create verification code</h1>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-6">
<label for="testDate">Testing date</label>
<label for="testDate">Testing date (local time)</label>
<input type="date" id="test-date" name="testDate" min="{{.minDate}}" max="{{.maxDate}}" class="form-control" />
<small class="form-text text-muted">
<strong>Recommended.</strong>
This is the <a href="https://www.timeanddate.com/worldclock/timezone/utc" target="_blank">UTC date</a> when the test was performed.
Click here to <a href="#" data-fill-target="test-date" data-fill-value="{{.maxDate}}">use today's UTC date</a>.
</small>
</div>

<div class="form-group col-md-6">
<label for="symptomDate">Symptoms onset</label>
<label for="symptomDate">Symptoms onset (local time)</label>
<div class="input-group">
<input type="date" id="symptom-date" name="symptomDate" min="{{.minDate}}" max="{{.maxDate}}" class="form-control" />
</div>
<small class="form-text text-muted">
<strong>Recommended.</strong>
This is the <a href="https://www.timeanddate.com/worldclock/timezone/utc" target="_blank">UTC date</a> when symptoms began. It cannot be more than {{.maxSymptomDays}} days ago.
Click here to <a href="#" data-fill-target="symptom-date" data-fill-value="{{.maxDate}}">use today's UTC date</a>.
</small>
</div>
</div>
</div>
Expand Down Expand Up @@ -274,6 +264,7 @@ <h1>Create verification code</h1>
$($form.serializeArray()).each(function(i, obj) {
data[obj.name] = obj.value
});
data.tzOffset = new Date().getTimezoneOffset();

getCode(data);
});
Expand Down
Binary file added e2e-test
Binary file not shown.
5 changes: 4 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ type IssueCodeRequest struct {
SymptomDate string `json:"symptomDate"` // ISO 8601 formatted date, YYYY-MM-DD
TestDate string `json:"testDate"`
TestType string `json:"testType"`
Phone string `json:"phone"`
// Offset in minutes of the user's timezone. Positive, negative, 0, or omitted
// (using the default of 0) are all valid. 0 is considered to be UTC.
TZOffset int `json:"tzOffset"`
Phone string `json:"phone"`
}

// IssueCodeResponse defines the response type for IssueCodeRequest.
Expand Down
3 changes: 2 additions & 1 deletion pkg/clients/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import (

// IssueCode uses the ADMIN API to issue a verification code.
// Currently does not accept the SMS param.
func IssueCode(ctx context.Context, hostname string, apiKey, testType, symptomDate string, timeout time.Duration) (*api.IssueCodeRequest, *api.IssueCodeResponse, error) {
func IssueCode(ctx context.Context, hostname string, apiKey, testType, symptomDate string, tzMinOffset int, timeout time.Duration) (*api.IssueCodeRequest, *api.IssueCodeResponse, error) {
url := hostname + "/api/issue"
request := api.IssueCodeRequest{
TestType: testType,
SymptomDate: symptomDate,
TZOffset: tzMinOffset,
}
client := &http.Client{
Timeout: timeout,
Expand Down
63 changes: 54 additions & 9 deletions pkg/controller/issueapi/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package issueapi

import (
"errors"
"fmt"
"net/http"
"strings"
Expand All @@ -27,6 +28,49 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/sms"
)

// Cache the UTC time.Location, to speed runtime.
var utc *time.Location

func init() {
var err error
utc, err = time.LoadLocation("UTC")
if err != nil {
panic("should have found UTC")
}
}

// validateDate validates the date given -- returning the time or an error.
func validateDate(date, minDate, maxDate time.Time, tzOffset int) (*time.Time, error) {
// Check that all our dates are utc.
if date.Location() != utc || minDate.Location() != utc || maxDate.Location() != utc {
return nil, errors.New("dates weren't in UTC")
}

// If we're dealing with a timezone where the offset is earlier than this one,
// we loosen up the lower bound. We might have the following circumstance:
//
// Server time: UTC Aug 1, 12:01 AM
// Client time: UTC July 30, 11:01 PM (ie, tzOffset = -30)
//
// In this circumstance, we'll have the following:
//
// minTime: UTC July 31, maxTime: Aug 1, clientTime: July 30.
//
// which would be an error. Loosening up the lower bound, by a day, keeps us
// all ok.
if tzOffset < 0 {
if m := minDate.Add(-24 * time.Hour); m.After(date) {
return nil, fmt.Errorf("date %v before min %v", date, m)
}
} else if minDate.After(date) {
return nil, fmt.Errorf("date %v before min %v", date, minDate)
}
if date.After(maxDate) {
return nil, fmt.Errorf("date %v after max %v", date, maxDate)
}
return &date, nil
}

func (c *Controller) HandleIssue() http.Handler {
logger := c.logger.Named("issueapi.HandleIssue")

Expand Down Expand Up @@ -96,25 +140,26 @@ func (c *Controller) HandleIssue() http.Handler {
return
}

// Max date is today (local time) and min date is AllowedTestAge ago, truncated.
maxDate := time.Now().Local()
minDate := maxDate.Add(-1 * c.config.GetAllowedSymptomAge()).Truncate(24 * time.Hour)

var symptomDate *time.Time
if request.SymptomDate != "" {
if parsed, err := time.Parse("2006-01-02", request.SymptomDate); err != nil {
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("failed to process symptom onset date: %v", err))
return
} else {
parsed = parsed.Local()
if minDate.After(parsed) || parsed.After(maxDate) {
err := fmt.Errorf("symptom onset date must be on/after %v and on/before %v",
// Max date is today (UTC time) and min date is AllowedTestAge ago, truncated.
maxDate := time.Now().UTC().Truncate(24 * time.Hour)
minDate := maxDate.Add(-1 * c.config.GetAllowedSymptomAge()).Truncate(24 * time.Hour)

symptomDate, err = validateDate(parsed, minDate, maxDate, request.TZOffset)
if err != nil {
err := fmt.Errorf("symptom onset date must be on/after %v and on/before %v %v",
minDate.Format("2006-01-02"),
maxDate.Format("2006-01-02"))
maxDate.Format("2006-01-02"),
parsed.Format("2006-01-02"),
)
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
return
}
symptomDate = &parsed
}
}

Expand Down
68 changes: 68 additions & 0 deletions pkg/controller/issueapi/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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 issueapi

import (
"testing"
"time"
)

func TestDateValidationn(t *testing.T) {
utc, err := time.LoadLocation("UTC")
if err != nil {
t.Fatalf("error loading utc")
}
var aug1 time.Time
aug1, err = time.ParseInLocation("2006-01-02", "2020-08-01", utc)
if err != nil {
t.Fatalf("error parsing date")
}

tests := []struct {
v string
max time.Time
tzOffset int
shouldErr bool
expected string
}{
{"2020-08-01", aug1, 0, false, "2020-08-01"},
{"2020-08-01", aug1, 60, false, "2020-08-01"},
{"2020-08-01", aug1, 60 * 12, false, "2020-08-01"},
{"2020-07-31", aug1, 60, false, "2020-07-31"},
{"2020-08-01", aug1, -60, false, "2020-08-01"},
{"2020-07-31", aug1, -60, false, "2020-07-31"},
{"2020-07-30", aug1, -60, false, "2020-07-30"},
{"2020-07-29", aug1, -60, true, "2020-07-30"},
}
for i, test := range tests {
date, err := time.ParseInLocation("2006-01-02", test.v, utc)
if err != nil {
t.Fatalf("[%d] error parsing date %q", i, test.v)
}
min := test.max.Add(-24 * time.Hour)
var newDate *time.Time
if newDate, err = validateDate(date, min, test.max, test.tzOffset); newDate == nil {
if err != nil {
if !test.shouldErr {
t.Fatalf("[%d] validateDate returned an unexpected error: %q", i, err)
}
} else {
t.Fatalf("[%d] expected error", i)
}
} else if s := newDate.Format("2006-01-02"); s != test.expected {
t.Fatalf("[%d] validateDate returned a different date %q != %q", i, s, test.expected)
}
}
}
2 changes: 1 addition & 1 deletion tools/e2e-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func realMain(ctx context.Context) error {
for i := 0; i < iterations; i++ {
// Issue the verification code.
logger.Infof("Issuing verification code, iteration %d", i)
codeRequest, code, err := clients.IssueCode(ctx, config.VerificationAdminAPIServer, config.VerificationAdminAPIKey, testType, symptomDate, timeout)
codeRequest, code, err := clients.IssueCode(ctx, config.VerificationAdminAPIServer, config.VerificationAdminAPIKey, testType, symptomDate, 0, timeout)
if err != nil {
return fmt.Errorf("error issuing verification code: %w", err)
} else if code.Error != "" {
Expand Down

0 comments on commit 85a4fe8

Please sign in to comment.