veyron/services/identity: Add revocation timestamps and buttons to identity server UI.

This change is dependent on https://veyron-review.googlesource.com/#/c/4183

This is what the UI looks like for a failed revocation: https://screenshot.googleplex.com/bMKno7uO1s.png.
Revoked identities display the timestamp at which they were revoked.

Change-Id: I7f0274849468e5c26110473fbfb11561186276e8
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 2ac5641..77292c0 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -138,6 +138,7 @@
 		Start, End         time.Time
 		Blessed            security.PublicID
 		RevocationCaveatID string
+		RevocationTime     time.Time
 	}
 	tmplargs := struct {
 		Log              chan tmplentry
@@ -171,13 +172,14 @@
 				}
 				if blessEntry.RevocationCaveat != nil {
 					tmplentry.RevocationCaveatID = base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
+					if revocationTime := h.revocationManager.GetRevocationTime(blessEntry.RevocationCaveat.ID()); revocationTime != nil {
+						tmplentry.RevocationTime = *revocationTime
+					}
 					// TODO(suharshs): Add a timeout that removes old entries to reduce storage space.
 					// TODO(suharshs): Make this map from CSRFToken to Email address, have
 					// revocation manager have map from caveatID to Email address in DirectoryStore.
 					h.tokenRevocationCaveatMap[tokenCaveatIDKey{csrf, tmplentry.RevocationCaveatID}] = true
 				}
-				// TODO(suharshs): Make the UI depend on where the caveatID exists and if it hasn't been revoked.
-				// Use the revocation manager IsRevoked function.
 				ch <- tmplentry
 			}
 		}(tmplargs.Log)
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index be8b525..5c2da76 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -7,7 +7,6 @@
 	"html/template"
 )
 
-// TODO(suharshs): Add an if statement to only show the revoke buttons for non-revoked ids.
 var tmpl = template.Must(template.New("auditor").Funcs(tmplFuncMap()).Parse(`<!doctype html>
 <html>
 <head>
@@ -15,9 +14,11 @@
 <title>Blessings for {{.Email}}</title>
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.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="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
 <script>
 function setTimeText(elem) {
   var timestamp = elem.data("unixtime");
@@ -50,16 +51,26 @@
         "CSRFToken": "{{.CSRFToken}}"
       })
     }).done(function(data) {
-      // TODO(suharshs): Have a fail message, add a strikethrough on the revoked caveats.
-      console.log(data)
-      revokeButton.remove()
-    }).fail(function(jqXHR, textStatus){
-      console.log(jqXHR)
-      console.log("The request failed :( :", textStatus)
+      if (!data.success) {
+        failMessage(revokeButton);
+        return;
+      }
+      revokeButton.replaceWith("<div>Just Revoked!</div>");
+    }).fail(function(xhr, textStatus){
+      failMessage(revokeButton);
+      console.error('Bad request: %s', status, xhr)
     });
   });
 });
 
+function failMessage(revokeButton) {
+  revokeButton.parent().parent().fadeIn(function(){
+    $(this).addClass("bg-danger");
+  });
+  toastr.options.closeButton = true;
+  toastr.error('Unable to revoke identity!', 'Error!')
+}
+
 </script>
 </head>
 <body>
@@ -84,7 +95,15 @@
 <td><div class="unixtime" data-unixtime={{.Start.Unix}}>{{.Start.String}}</div></td>
 <td><div class="unixtime" data-unixtime={{.End.Unix}}>{{.End.String}}</div></td>
 <td>{{publicKeyHash .Blessee.PublicKey}}</td>
-<td><button class="revoke" value="{{.RevocationCaveatID}}" type="button">Revoke</button></td>
+<td>
+{{if .RevocationCaveatID}}
+  {{ if .RevocationTime.IsZero }}
+  <button class="revoke" value="{{.RevocationCaveatID}}">Revoke</button>
+  {{ else }}
+    <div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
+  {{ end }}
+{{ end }}
+</td>
 </tr>
 {{else}}
 <tr>
diff --git a/services/identity/revocation/revocation_manager.go b/services/identity/revocation/revocation_manager.go
index b988696..139012a 100644
--- a/services/identity/revocation/revocation_manager.go
+++ b/services/identity/revocation/revocation_manager.go
@@ -6,6 +6,7 @@
 	"encoding/hex"
 	"fmt"
 	"path/filepath"
+	"strconv"
 	"sync"
 	"time"
 
@@ -47,16 +48,26 @@
 	if err != nil {
 		return err
 	}
-	return revocationMap.Put(token, string(time.Now().Unix()))
+	return revocationMap.Put(token, strconv.FormatInt(time.Now().Unix(), 10))
 }
 
-// Returns true if the provided caveat has been revoked.
-func (r *RevocationManager) IsRevoked(caveatID security.ThirdPartyCaveatID) bool {
+// GetRevocationTimestamp returns the timestamp at which a caveat was revoked.
+// If the caveat wasn't revoked returns nil
+func (r *RevocationManager) GetRevocationTime(caveatID security.ThirdPartyCaveatID) *time.Time {
 	token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
-	if err == nil {
-		return revocationMap.Exists(token)
+	if err != nil {
+		return nil
 	}
-	return false
+	timestamp, err := revocationMap.Get(token)
+	if err != nil {
+		return nil
+	}
+	unix_int, err := strconv.ParseInt(timestamp, 10, 64)
+	if err != nil {
+		return nil
+	}
+	revocationTime := time.Unix(unix_int, 0)
+	return &revocationTime
 }
 
 type revocationCaveat [16]byte