Adding personal access tokens
3 files changed, 103 insertions(+), 16 deletions(-)

M input.go
M models.go
M routes.go
M input.go +18 -7
@@ 1,12 1,27 @@ 
 package oauth2
 
 import (
-	"fmt"
-
 	"github.com/labstack/echo/v4"
 	"hg.code.netlandish.com/~netlandish/gobwebs/validate"
 )
 
+type AddPersonalTokenForm struct {
+	Comment string `form:"comment" validate:"-"`
+}
+
+func (a *AddPersonalTokenForm) Validate(c echo.Context) error {
+	// Binding each field specifically to use BindErrors
+	errs := validate.FormFieldBinder(c, a).
+		FailFast(false).
+		String("comment", &a.Comment).
+		BindErrors()
+	if errs != nil {
+		return validate.GetInputErrors(errs)
+	}
+
+	return c.Validate(a)
+}
+
 type AddClientForm struct {
 	Name        string `form:"name" validate:"required"`
 	Description string `form:"description" validate:"-"`

          
@@ 27,9 42,5 @@ func (a *AddClientForm) Validate(c echo.
 		return validate.GetInputErrors(errs)
 	}
 
-	if err := c.Validate(a); err != nil {
-		return err
-	}
-	fmt.Println("foo")
-	return nil
+	return c.Validate(a)
 }

          
M models.go +11 -8
@@ 1,6 1,9 @@ 
 package oauth2
 
-import "time"
+import (
+	"database/sql"
+	"time"
+)
 
 // Client ...
 type Client struct {

          
@@ 20,11 23,11 @@ type Client struct {
 
 // Grant ...
 type Grant struct {
-	ID        int       `db:"id"`
-	Issued    time.Time `db:"issued"`
-	Expires   time.Time `db:"expires"`
-	Comment   time.Time `db:"comment"`
-	TokenHash string    `db:"token_hash"`
-	UserID    int       `db:"user_id"`
-	ClientID  int       `db:"client_id"`
+	ID        int           `db:"id"`
+	Issued    time.Time     `db:"issued"`
+	Expires   time.Time     `db:"expires"`
+	Comment   string        `db:"comment"`
+	TokenHash string        `db:"token_hash"`
+	UserID    int           `db:"user_id"`
+	ClientID  sql.NullInt64 `db:"client_id"`
 }

          
M routes.go +74 -1
@@ 1,8 1,11 @@ 
 package oauth2
 
 import (
+	"crypto/sha512"
+	"encoding/hex"
 	"fmt"
 	"net/http"
+	"time"
 
 	sq "github.com/Masterminds/squirrel"
 	"github.com/labstack/echo/v4"

          
@@ 24,11 27,81 @@ func (s *Service) RegisterRoutes() {
 	s.eg.POST("/introspect", s.Introspect).Name = s.RouteName("introspect_post")
 
 	s.eg.Use(auth.AuthRequired())
+	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("/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")
 }
 
+// ListPersonal ...
+func (s *Service) ListPersonal(c echo.Context) error {
+	gctx := c.(*server.Context)
+	opts := &database.FilterOptions{
+		Filter: sq.And{
+			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
+	}
+	return gctx.Render(http.StatusOK, "oauth2_personal_list.html", gobwebs.Map{
+		"tokens": tokens,
+	})
+}
+
+// AddPersonal ...
+func (s *Service) AddPersonal(c echo.Context) error {
+	gctx := c.(*server.Context)
+	form := &AddPersonalTokenForm{}
+	gmap := gobwebs.Map{
+		"form": form,
+	}
+
+	req := c.Request()
+	if req.Method == "POST" {
+		if err := form.Validate(c); err != nil {
+			gmap["errors"] = err
+			return gctx.Render(http.StatusOK, "oauth2_add_personal.html", gmap)
+		}
+
+		issued := time.Now().UTC()
+		expires := issued.Add(366 * 24 * time.Hour)
+
+		grant := BearerToken{
+			Version:  TokenVersion,
+			Issued:   ToTimestamp(issued),
+			Expires:  ToTimestamp(expires),
+			Grants:   "",
+			UserID:   int(gctx.User.GetID()),
+			ClientID: "",
+		}
+		token := grant.Encode(c.Request().Context())
+		hash := sha512.Sum512([]byte(token))
+		tokenHash := hex.EncodeToString(hash[:])
+
+		dbgrant := &Grant{
+			Issued:    issued,
+			Expires:   expires,
+			Comment:   form.Comment,
+			TokenHash: tokenHash,
+			UserID:    int(gctx.User.GetID()),
+		}
+
+		if err := dbgrant.Store(c.Request().Context()); err != nil {
+			return err
+		}
+		gmap["token"] = token
+		return gctx.Render(http.StatusOK, "oauth2_add_personal_done.html", gmap)
+	}
+
+	return gctx.Render(http.StatusOK, "oauth2_add_personal.html", gmap)
+}
+
 // ListClients ...
 func (s *Service) ListClients(c echo.Context) error {
 	gctx := c.(*server.Context)

          
@@ 44,7 117,7 @@ func (s *Service) ListClients(c echo.Con
 	})
 }
 
-// AddClient
+// AddClient ...
 func (s *Service) AddClient(c echo.Context) error {
 	gctx := c.(*server.Context)
 	form := &AddClientForm{}