Adding client token management handlers
2 files changed, 149 insertions(+), 1 deletions(-)

M clients.go
M routes.go
M clients.go +29 -0
@@ 145,6 145,35 @@ func (c *Client) VerifyClientSecret(clie
 
 }
 
+// Revoke will revoke all grants for a given client
+func (c *Client) Revoke(ctx context.Context) error {
+	if c.ID == 0 {
+		return fmt.Errorf("Client object is not populated")
+	}
+	err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
+		c.Revoked = true
+		err := sq.
+			Update("oauth2_clients").
+			Set("revoked", c.Revoked).
+			Where("id = ?", c.ID).
+			Suffix(`RETURNING (updated_on)`).
+			PlaceholderFormat(sq.Dollar).
+			RunWith(tx).
+			ScanContext(ctx, &c.UpdatedOn)
+		if err != nil {
+			return err
+		}
+
+		_, err = tx.ExecContext(ctx, `
+			UPDATE oauth2_grants
+			SET expires = NOW() AT TIME ZONE 'UTC'
+			WHERE client_id = $1;
+		`, c.ID)
+		return err
+	})
+	return err
+}
+
 // Delete will delete this client
 func (c *Client) Delete(ctx context.Context) error {
 	if c.ID == 0 {

          
M routes.go +120 -1
@@ 10,6 10,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 	"time"
 

          
@@ 44,9 45,14 @@ func (s *Service) RegisterRoutes() {
 	s.eg.GET("/personal", s.ListPersonal).Name = s.RouteName("list_personal")
 	s.eg.GET("/personal/add", s.AddPersonal).Name = s.RouteName("add_personal")
 	s.eg.POST("/personal/add", s.AddPersonal).Name = s.RouteName("add_personal_post")
+	s.eg.GET("/personal/:id/revoke", s.RevokePersonal).Name = s.RouteName("revoke_personal")
+	s.eg.POST("/personal/:id/revoke", s.RevokePersonal).Name = s.RouteName("revoke_personal_post")
 	s.eg.GET("/clients", s.ListClients).Name = s.RouteName("list_clients")
 	s.eg.GET("/clients/add", s.AddClient).Name = s.RouteName("add_client")
 	s.eg.POST("/clients/add", s.AddClient).Name = s.RouteName("add_client_post")
+	s.eg.GET("/clients/:id", s.DetailClient).Name = s.RouteName("detail_client")
+	s.eg.POST("/clients/:id/unregister", s.UnregisterClient).Name = s.RouteName("unregister_client_post")
+	s.eg.POST("/clients/:id/reissue", s.ReissueClient).Name = s.RouteName("reissue_client_post")
 	s.eg.GET("/authorize", s.Authorize).Name = s.RouteName("authorize")
 	s.eg.POST("/authorize", s.AuthorizePOST).Name = s.RouteName("authorize_post")
 }

          
@@ 118,11 124,53 @@ func (s *Service) AddPersonal(c echo.Con
 	return gctx.Render(http.StatusOK, "oauth2_add_personal.html", gmap)
 }
 
+// RevokePersonal ...
+func (s *Service) RevokePersonal(c echo.Context) error {
+	id, err := strconv.Atoi(c.Param("id"))
+	if err != nil {
+		return echo.NotFoundHandler(c)
+	}
+	gctx := c.(*server.Context)
+	opts := &database.FilterOptions{
+		Filter: sq.And{
+			sq.Eq{"id": id},
+			sq.Eq{"user_id": gctx.User.GetID()},
+			sq.Expr("client_id IS NULL"),
+			sq.Expr("expires > NOW() at time zone 'UTC'"),
+		},
+	}
+	tokens, err := GetGrants(c.Request().Context(), opts)
+	if err != nil {
+		return err
+	}
+	if len(tokens) == 0 {
+		return echo.NotFoundHandler(c)
+	}
+	token := tokens[0]
+
+	req := c.Request()
+	if req.Method == "POST" {
+		if err := token.Revoke(req.Context()); err != nil {
+			return err
+		}
+		return c.Redirect(http.StatusMovedPermanently,
+			c.Echo().Reverse(s.RouteName("list_personal")))
+	}
+
+	return gctx.Render(http.StatusOK, "oauth2_revoke_confirm.html", gobwebs.Map{
+		"token": token,
+	})
+
+}
+
 // ListClients ...
 func (s *Service) ListClients(c echo.Context) error {
 	gctx := c.(*server.Context)
 	opts := &database.FilterOptions{
-		Filter: sq.Eq{"owner_id": gctx.User.GetID()},
+		Filter: sq.And{
+			sq.Eq{"owner_id": gctx.User.GetID()},
+			sq.Eq{"revoked": false},
+		},
 	}
 	clients, err := GetClients(c.Request().Context(), opts)
 	if err != nil {

          
@@ 171,6 219,77 @@ func (s *Service) AddClient(c echo.Conte
 	return gctx.Render(http.StatusOK, "oauth2_add_client.html", gmap)
 }
 
+// DetailClient ...
+func (s *Service) DetailClient(c echo.Context) error {
+	id := c.Param("id")
+	gctx := c.(*server.Context)
+	client, err := GetClientByID(c.Request().Context(), id)
+	if err != nil {
+		return err
+	}
+	if client == nil || client.OwnerID != int(gctx.User.GetID()) {
+		return echo.NotFoundHandler(c)
+	}
+	return gctx.Render(http.StatusOK, "oauth2_client_detail.html", gobwebs.Map{
+		"client": client,
+	})
+}
+
+// UnregisterClient ...
+func (s *Service) UnregisterClient(c echo.Context) error {
+	id := c.Param("id")
+	gctx := c.(*server.Context)
+	client, err := GetClientByID(c.Request().Context(), id)
+	if err != nil {
+		return err
+	}
+	if client == nil || client.OwnerID != int(gctx.User.GetID()) {
+		return echo.NotFoundHandler(c)
+	}
+
+	err = client.Revoke(c.Request().Context())
+	if err != nil {
+		return err
+	}
+
+	return c.Redirect(http.StatusMovedPermanently,
+		c.Echo().Reverse(s.RouteName("list_clients")))
+}
+
+// ReissueClient ...
+func (s *Service) ReissueClient(c echo.Context) error {
+	id := c.Param("id")
+	gctx := c.(*server.Context)
+	client, err := GetClientByID(c.Request().Context(), id)
+	if err != nil {
+		return err
+	}
+	if client == nil || client.OwnerID != int(gctx.User.GetID()) {
+		return echo.NotFoundHandler(c)
+	}
+
+	err = client.Revoke(c.Request().Context())
+	if err != nil {
+		return err
+	}
+
+	newClient := &Client{
+		OwnerID:     int(gctx.User.GetID()),
+		Name:        client.Name,
+		Description: client.Description,
+		RedirectURL: client.RedirectURL,
+		ClientURL:   client.ClientURL,
+	}
+	token := newClient.GenerateKeys()
+	if err := newClient.Store(c.Request().Context()); err != nil {
+		return err
+	}
+	return gctx.Render(http.StatusOK, "oauth2_add_client_done.html", gobwebs.Map{
+		"client": newClient,
+		"token":  token,
+	})
+}
+
 func oauth2Redirect(c echo.Context, redirectURI string, params gobwebs.Map) error {
 	parts, err := url.Parse(redirectURI)
 	if err != nil {