veyron/services/identity: Client side expiry caveat selection improvement.

* Datetime picker is now used on form page.
* cleaned up handling of caveat construction.

Change-Id: If92c2c45fd2362e1bac4a8dfc9600bac15975481
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 2ac127b..fb93bc1 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -327,6 +327,8 @@
 	ToolRedirectURL, ToolState, Email string
 }
 
+var caveatList = []string{"ExpiryCaveat", "MethodCaveat"}
+
 func (h *handler) addCaveats(w http.ResponseWriter, r *http.Request) {
 	var inputMacaroon seekBlessingsMacaroon
 	if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, &inputMacaroon); err != nil {
@@ -354,9 +356,9 @@
 	}
 	tmplargs := struct {
 		Extension               string
-		CaveatMap               map[string]caveatInfo
+		CaveatList              []string
 		Macaroon, MacaroonRoute string
-	}{email, caveatMap, outputMacaroon, sendMacaroonRoute}
+	}{email, caveatList, outputMacaroon, sendMacaroonRoute}
 	w.Header().Set("Context-Type", "text/html")
 	if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
 		vlog.Errorf("Unable to execute bless page template: %v", err)
@@ -416,12 +418,7 @@
 	// Fill in the required caveat.
 	switch required := r.FormValue("requiredCaveat"); required {
 	case "Expiry":
-		str := r.FormValue("expiry")
-		d, err := time.ParseDuration(str)
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse expiration duration(%q): %v", str, err)
-		}
-		expiry, err := security.ExpiryCaveat(time.Now().Add(d))
+		expiry, err := newExpiryCaveat(r.FormValue("expiry"), r.FormValue("timezoneOffset"))
 		if err != nil {
 			return nil, fmt.Errorf("failed to create ExpiryCaveat: %v", err)
 		}
@@ -444,56 +441,45 @@
 
 	// And find any additional ones
 	for i, cavName := range r.Form["caveat"] {
-		if cavName == "none" {
+		var err error
+		var caveat security.Caveat
+		switch cavName {
+		case "ExpiryCaveat":
+			caveat, err = newExpiryCaveat(r.Form[cavName][i], r.FormValue("timezoneOffset"))
+		case "MethodCaveat":
+			caveat, err = newMethodCaveat(strings.Split(r.Form[cavName][i], ","))
+		case "none":
 			continue
-		}
-		args := strings.Split(r.Form[cavName][i], ",")
-		cavInfo, ok := caveatMap[cavName]
-		if !ok {
+		default:
 			return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
 		}
-		caveat, err := cavInfo.New(args...)
 		if err != nil {
-			return nil, fmt.Errorf("unable to create caveat %s(%v): cavInfo.New failed: %v", cavName, args, err)
+			return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
 		}
 		caveats = append(caveats, caveat)
 	}
 	return caveats, nil
 }
 
-type caveatInfo struct {
-	New         func(args ...string) (security.Caveat, error)
-	Placeholder string
+func newExpiryCaveat(timestamp, utcOffset string) (security.Caveat, error) {
+	var empty security.Caveat
+	t, err := time.Parse("2006-01-02T15:04", timestamp)
+	if err != nil {
+		return empty, fmt.Errorf("parseTime failed: %v", err)
+	}
+	// utcOffset is returned as minutes from JS, so we need to parse it to a duration.
+	offset, err := time.ParseDuration(utcOffset + "m")
+	if err != nil {
+		return empty, fmt.Errorf("failed to parse duration: %v", err)
+	}
+	return security.ExpiryCaveat(t.Add(time.Minute * offset))
 }
 
-// caveatMap is a map from Caveat name to caveat information.
-// To add to this map append
-// key = "CaveatName"
-// New = func that returns instantiation of specific caveat wrapped in security.Caveat.
-// Placeholder = the placeholder text for the html input element.
-var caveatMap = map[string]caveatInfo{
-	"ExpiryCaveat": {
-		New: func(args ...string) (security.Caveat, error) {
-			if len(args) != 1 {
-				return security.Caveat{}, fmt.Errorf("must pass exactly one duration string.")
-			}
-			dur, err := time.ParseDuration(args[0])
-			if err != nil {
-				return security.Caveat{}, fmt.Errorf("parse duration failed: %v", err)
-			}
-			return security.ExpiryCaveat(time.Now().Add(dur))
-		},
-		Placeholder: "i.e. 2h45m. Valid time units are ns, us (or µs), ms, s, m, h.",
-	},
-	"MethodCaveat": {
-		New: func(args ...string) (security.Caveat, error) {
-			if len(args) < 1 {
-				return security.Caveat{}, fmt.Errorf("must pass at least one method")
-			}
-			return security.MethodCaveat(args[0], args[1:]...)
-		},
-		Placeholder: "Comma-separated method names.",
-	},
+func newMethodCaveat(methods []string) (security.Caveat, error) {
+	if len(methods) < 1 {
+		return security.Caveat{}, fmt.Errorf("must pass at least one method")
+	}
+	return security.MethodCaveat(methods[0], methods[1:]...)
 }
 
 // exchangeAuthCodeForEmail exchanges the authorization code (which must
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 958c70e..a3dc4ee 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -124,7 +124,10 @@
 <title>Blessings: Select caveats</title>
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
+<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
 <script>
   // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
   $(document).ready(function() {
@@ -152,19 +155,30 @@
 
     // Upon clicking the '-' button caveats should be removed.
     $('body').on('click', '.removeCaveat', function() {
-      $(this).parents(".caveatRow").remove();
+      $(this).parents('.caveatRow').remove();
     });
+
+    // Get the timezoneOffset for the server to create a correct expiry caveat.
+    // The offset is the minutes between UTC and local time.
+    var d = new Date();
+    $('#timezoneOffset').val(d.getTimezoneOffset());
+
+    // Set the datetime picker to have a default value of one day from now.
+    var m = moment().add(1, 'd').format("YYYY-MM-DDTHH:MM")
+    $('#expiry').val(m);
+    $('#ExpiryCaveat').val(m);
   });
 </script>
 </head>
 <body class="container">
-<form class="form-horizontal" method="POST" name="input" action="/google/{{.MacaroonRoute}}" role="form">
+<form class="form-horizontal" method="POST" id="caveats-form" name="input" action="/google/{{.MacaroonRoute}}" role="form">
 <h2 class="form-signin-heading">{{.Extension}}</h2>
 <input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
 <div class="form-group form-group-lg">
   <label class="col-sm-2" for="blessing-extension">Extension</label>
   <div class="col-sm-10">
   <input name="blessingExtension" type="text" class="form-control" id="blessing-extension" placeholder="(optional) name of the device/application for which the blessing is being sought, e.g. homelaptop">
+  <input type="text" class="hidden" id="timezoneOffset" name="timezoneOffset">
   </div>
 </div>
 <div class="form-group form-group-lg">
@@ -179,7 +193,7 @@
     <div class="radio">
       <div class="input-group">
         <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
-        <input type="text" name="expiry" id="expiry" value="1h" placeholder="time after which the blessing will expire">
+        <input type="datetime-local" id="expiry" name="expiry">
       </div>
     </div>
   </div>
@@ -190,15 +204,19 @@
   <div class="col-md-4">
     <select name="caveat" class="form-control caveats">
       <option value="none" selected="selected">Select a caveat.</option>
-      {{ $caveatMap := .CaveatMap }}
-      {{range $key, $value := $caveatMap}}
-      <option name="{{$key}}" value="{{$key}}">{{$key}}</option>
+      {{ $caveatList := .CaveatList }}
+      {{range $index, $name := $caveatList}}
+      <option name="{{$name}}" value="{{$name}}">{{$name}}</option>
       {{end}}
     </select>
   </div>
   <div class="col-md-7">
-    {{range $key, $entry := $caveatMap}}
-      <input type="text" id="{{$key}}" class="form-control caveatInput" name="{{$key}}" placeholder="{{$entry.Placeholder}}">
+    {{range $index, $name := $caveatList}}
+      {{if eq $name "ExpiryCaveat"}}
+      <input type="datetime-local" class="form-control caveatInput" id="{{$name}}" name="{{$name}}">
+      {{else if eq $name "MethodCaveat"}}
+      <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated method list">
+      {{end}}
     {{end}}
   </div>
   <div class="col-md-1">