diff --git a/.gitignore b/.gitignore index 7c8dc08..e31fe65 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # + +*.env + # Binaries for programs and plugins *.exe *.exe~ diff --git a/Justfile b/Justfile index c80431f..9a06eb0 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,8 @@ # Runs the 'templ generate` every time a file is updated and reloads the debugger debug: - templ generate --watch --proxy="http://localhost:3000" --cmd="go run ." \ No newline at end of file + templ generate --watch --proxy="http://localhost:3000" --cmd="go run cmd/main.go" + +# Generates templ files +gen: + templ generate diff --git a/client/apiclient.go b/client/apiclient.go index 79b2910..7fbfccd 100644 --- a/client/apiclient.go +++ b/client/apiclient.go @@ -7,6 +7,7 @@ const ( type ApiClient struct { Auth Auth + Demo DemoApiClient ServerAddress string } @@ -14,5 +15,6 @@ type ApiClient struct { func New(serverAddress string) ApiClient { return ApiClient{ Auth: newAuthClient(serverAddress), + Demo: newDemoClient(serverAddress), } } diff --git a/client/auth.go b/client/auth.go index 492e94d..91ce39b 100644 --- a/client/auth.go +++ b/client/auth.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" + "net/url" "git.jamestombleson.com/jtom38/go-cook/api/domain" ) type Auth interface { Register(username, password string) error - Login(username, password string) error + Login(username, password string) (LoginResponse, error) } type AuthClient struct { @@ -42,7 +43,7 @@ func (a AuthClient) Register(username, password string) error { return err } - defer resp.Body.Close() + //defer resp.Body.Close() content, err := io.ReadAll(resp.Body) if err != nil { @@ -58,31 +59,25 @@ func (a AuthClient) Register(username, password string) error { return nil } -func (a AuthClient) Login(username, password string) error { - endpoint := fmt.Sprintf("%s/api/v1/auth/register", a.serverAddress) - req, err := http.NewRequest(http.MethodPost, endpoint, nil) +type LoginResponse struct { + Success bool `json:"success"` + Token string `json:"token"` + Type string `json:"type"` + RefreshToken string `json:"refreshToken"` +} + +func (a AuthClient) Login(username, password string) (LoginResponse, error) { + endpoint := fmt.Sprintf("%s/api/v1/auth/login", a.serverAddress) + + param := url.Values{} + param.Set("username", username) + param.Set("password", password) + + var bind = LoginResponse{} + err := PostUrlForm(a.client, endpoint, param, &bind) if err != nil { - return err + return LoginResponse{}, err } - req.Header.Set(HeaderContentType, MIMEApplicationForm) - req.Form.Add("username", username) - req.Form.Add("password", password) - resp, err := a.client.Do(req) - if err != nil { - return err - } - - defer resp.Body.Close() - content, err := io.ReadAll(req.Body) - if err != nil { - return err - } - - var bind = domain.ErrorResponse{} - err = json.Unmarshal(content, &bind) - if err != nil { - return err - } - return nil -} \ No newline at end of file + return bind, nil +} diff --git a/client/demo.go b/client/demo.go new file mode 100644 index 0000000..625070c --- /dev/null +++ b/client/demo.go @@ -0,0 +1,47 @@ +package client + +import ( + "fmt" + "io" + "log" + "net/http" +) + +type DemoApiClient struct { + serverAddress string + client http.Client +} + +func newDemoClient(serverAddress string) DemoApiClient { + return DemoApiClient{ + serverAddress: serverAddress, + client: http.Client{}, + } +} + +type HelloBodyParam struct { + Name string `json:"name"` +} + +// This is an example route to demo passing a body in +func (d DemoApiClient) Hello() error { + endpoint := fmt.Sprintf("%s/api/v1/demo/hello", d.serverAddress) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return err + } + + resp, err := d.client.Do(req) + if err != nil { + return err + } + //defer resp.Body.Close() + + content, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + log.Println(string(content)) + + return nil +} diff --git a/client/util.go b/client/util.go new file mode 100644 index 0000000..d9d3ca5 --- /dev/null +++ b/client/util.go @@ -0,0 +1,31 @@ +package client + +import ( + "bytes" + "encoding/json" + "net/http" + "net/url" +) + +func PostUrlForm(client http.Client, endpoint string, param url.Values, t any) error { + payload := bytes.NewBufferString(param.Encode()) + req, err := http.NewRequest(http.MethodPost, endpoint, payload) + if err != nil { + return err + } + + req.Header.Set(HeaderContentType, MIMEApplicationForm) + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&t) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..4bbbb35 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "templ-test/client" + "templ-test/handlers" + "templ-test/services" + + "github.com/gorilla/sessions" + "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + cfg := services.NewEnvConfig() + + // connect to api server + apiClient := client.New(cfg.ApiServerUri) + + e := echo.New() + e.Use(session.Middleware(sessions.NewCookieStore([]byte(cfg.CookieSecret)))) + e.Pre(middleware.Logger()) + + handler := e.Group("") + portalClient := handlers.NewHandlerClient(apiClient, cfg) + portalClient.Register(*handler) + + fmt.Println("Listening on :1324") + e.Logger.Fatal(e.Start(":1324")) +} diff --git a/go.mod b/go.mod index b3614a9..529da3d 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( require ( github.com/a-h/templ v0.2.648 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/gorilla/sessions v1.2.2 github.com/labstack/echo-contrib v0.16.0 github.com/labstack/echo/v4 v4.11.4 diff --git a/go.sum b/go.sum index 0e993ac..1fb95c0 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= diff --git a/handlers/auth.go b/handlers/auth.go index 879889b..5b152a2 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -1,24 +1,56 @@ package handlers import ( + "log" "net/http" - "templ-test/views" + "templ-test/views/auth" + "templ-test/views/home" "github.com/labstack/echo/v4" ) func (h *Handlers) AuthLogin(c echo.Context) error { - return Render(c, http.StatusOK, views.AuthLogin()) + return Render(c, http.StatusOK, auth.AuthLogin()) } func (h *Handlers) AuthLoginPost(c echo.Context) error { // check the form data - //user := c.FormValue("email") - //password := c.FormValue("password") + user := c.FormValue("username") + password := c.FormValue("password") // send request to the API - //h.api. + resp, err := h.api.Auth.Login(user, password) + if err != nil { + return err + } + + cookie := new(http.Cookie) + cookie.Name = CookieToken + cookie.Value = resp.Token + c.SetCookie(cookie) + + cookie = new(http.Cookie) + cookie.Name = CookieRefreshToken + cookie.Value = resp.RefreshToken + c.SetCookie(cookie) + + cookie = new(http.Cookie) + cookie.Name = CookieUser + cookie.Value = user + c.SetCookie(cookie) // render - return nil + return Render(c, http.StatusOK, home.Home()) +} + +func (h *Handlers) AuthShowCookies(c echo.Context) error { + claims, err := ValidateJwt(c, h.cfg.SharedApiSecret, h.cfg.ApiServerUri) + if err != nil { + return Render(c, http.StatusInternalServerError, home.Error(err)) + } + log.Println(claims) + + cookies := GetCookieValues(c) + + return Render(c, http.StatusOK, auth.ShowCookie(cookies)) } diff --git a/handlers/handler.go b/handlers/handler.go index 3a390d4..4cbfa70 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -1,36 +1,48 @@ package handlers import ( + "errors" "templ-test/client" + "templ-test/models" "templ-test/services" + "time" "github.com/a-h/templ" - "github.com/gorilla/sessions" - "github.com/labstack/echo-contrib/session" + + "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" ) +const ( + CookieToken = "token" + CookieRefreshToken = "refresh" + CookieUser = "user" +) + type Handlers struct { - Server *echo.Echo - api client.ApiClient + api client.ApiClient + cfg services.EnvConfig } func NewHandlerClient(api client.ApiClient, cfg services.EnvConfig) *Handlers { h := Handlers{ - api: api, + api: api, + cfg: cfg, } - e := echo.New() - e.Use(session.Middleware(sessions.NewCookieStore([]byte(cfg.CookieSecret)))) - e.GET("/", h.HomeHandler) - e.GET("/list", h.ListHandler) - - auth := e.Group("/auth") + return &h +} + +func (h *Handlers) Register(group echo.Group) { + group.GET("/", h.HomeHandler) + group.GET("/settings", h.Settings) + group.POST("/settings", h.SettingsPost) + //group.GET("/list", h.ListHandler) + + auth := group.Group("/auth") auth.GET("/login", h.AuthLogin) auth.POST("/login", h.AuthLoginPost) - - h.Server = e - return &h + auth.GET("/cookie", h.AuthShowCookies) } func Render(ctx echo.Context, statusCode int, t templ.Component) error { @@ -38,3 +50,61 @@ func Render(ctx echo.Context, statusCode int, t templ.Component) error { ctx.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTML) return t.Render(ctx.Request().Context(), ctx.Response().Writer) } + +type jwtToken struct { + Exp time.Time `json:"exp"` + Iss string `json:"iss"` + Authorized bool `json:"authorized"` + UserName string `json:"username"` + Scopes []string `json:"scopes"` + jwt.RegisteredClaims +} + +func ValidateJwt(ctx echo.Context, sharedSecret, issuer string) (jwtToken, error) { + cookies := GetCookieValues(ctx) + if cookies.Token == "" { + return jwtToken{}, errors.New("JWT Bearer Token is missing") + } + + token, err := jwt.ParseWithClaims(cookies.Token, &jwtToken{}, func(token *jwt.Token) (interface{}, error) { + return []byte(sharedSecret), nil + }) + if err != nil { + return jwtToken{}, err + } + + if !token.Valid { + return jwtToken{}, errors.New("invalid jwt token") + } + + claims := token.Claims.(*jwtToken) + if !claims.Exp.After(time.Now()) { + return jwtToken{}, errors.New("the jwt token has expired") + } + if claims.Iss != issuer { + return jwtToken{}, errors.New("the issuer was invalid") + } + + return *claims, nil +} + +func GetCookieValues(ctx echo.Context) models.AllCookies { + m := models.AllCookies{} + + token, err := ctx.Cookie(CookieToken) + if err == nil { + m.Token = token.Value + } + + user, err := ctx.Cookie(CookieUser) + if err == nil { + m.Username = user.Value + } + + refresh, err := ctx.Cookie(CookieRefreshToken) + if err == nil { + m.RefreshToken = refresh.Value + } + + return m +} diff --git a/handlers/home.go b/handlers/home.go index aac02ef..d35c8df 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -2,15 +2,25 @@ package handlers import ( "net/http" - "templ-test/views" + "templ-test/views/home" "github.com/labstack/echo/v4" ) func (h *Handlers) HomeHandler(c echo.Context) error { - return Render(c, http.StatusOK, views.Home()) + return Render(c, http.StatusOK, home.Home()) } -func (h *Handlers) ListHandler(c echo.Context) error { - return Render(c, http.StatusOK, views.List()) -} \ No newline at end of file +func (h *Handlers) Settings(c echo.Context) error { + return Render(c, http.StatusOK, home.UserSettings()) +} + +func (h *Handlers) SettingsPost(c echo.Context) error { + // take in the updated values from he user and write the cookies... tbd + + return Render(c, http.StatusOK, home.UserSettings()) +} + +//func (h *Handlers) ListHandler(c echo.Context) error { +// return Render(c, http.StatusOK, views.List()) +//} diff --git a/main.go b/main.go deleted file mode 100644 index ef45cb1..0000000 --- a/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - "templ-test/client" - "templ-test/handlers" - "templ-test/services" -) - -func main() { - cfg := services.NewEnvConfig() - - // connect to api server - apiClient := client.New(cfg.ApiServerUri) - handler := handlers.NewHandlerClient(apiClient, cfg) - - fmt.Println("Listening on :3000") - handler.Server.Start(":3000") -} diff --git a/models/auth.go b/models/auth.go new file mode 100644 index 0000000..af18023 --- /dev/null +++ b/models/auth.go @@ -0,0 +1,11 @@ +package models + +type AllCookies struct { + Username string + Token string + RefreshToken string +} + +type ShowCookie struct { + AllCookies +} diff --git a/services/config.go b/services/config.go index ebfec2a..84cc4cf 100644 --- a/services/config.go +++ b/services/config.go @@ -8,18 +8,23 @@ import ( ) type EnvConfig struct { - ApiServerUri string - CookieSecret string + ApiServerUri string + SharedApiSecret string + CookieSecret string } func NewEnvConfig() EnvConfig { - err := godotenv.Load() - if err != nil { - log.Println(err) + _, err := os.Stat(".env") + if err == nil { + err = godotenv.Load() + if err != nil { + log.Println(err) + } } return EnvConfig{ - ApiServerUri: os.Getenv("ApiServerUri"), - CookieSecret: os.Getenv("CookieSecret"), + ApiServerUri: os.Getenv("ApiServerUri"), + SharedApiSecret: os.Getenv("SharedApiSecret"), + CookieSecret: os.Getenv("CookieSecret"), } } diff --git a/views/auth.templ b/views/auth.templ deleted file mode 100644 index b930484..0000000 --- a/views/auth.templ +++ /dev/null @@ -1,22 +0,0 @@ -package views - -templ AuthLogin() { - @WithLayout("Login", true) { -
-
- - -
We'll never share your email with anyone else.
-
-
- - -
-
- - -
- -
- } -} diff --git a/views/auth/cookie.templ b/views/auth/cookie.templ new file mode 100644 index 0000000..a9e8ada --- /dev/null +++ b/views/auth/cookie.templ @@ -0,0 +1,13 @@ +package auth + +import "templ-test/models" +import "templ-test/views/layout" + +templ ShowCookie(m models.AllCookies) { + @layout.Testing("Cookie Explorer") { +

These are stored as cookies

+

Username: { m.Username }

+

JWT Token: { m.Token }

+

RefreshToken: { m.RefreshToken }

+ } +} diff --git a/views/auth/login.templ b/views/auth/login.templ new file mode 100644 index 0000000..6dc0886 --- /dev/null +++ b/views/auth/login.templ @@ -0,0 +1,23 @@ +package auth + +import "templ-test/views/layout" + +templ AuthLogin() { + @layout.WithLayout("Login", true) { +
+
+ + +
+
+ + +
+
+ + +
+ +
+ } +} diff --git a/views/home/error.templ b/views/home/error.templ new file mode 100644 index 0000000..7ca9230 --- /dev/null +++ b/views/home/error.templ @@ -0,0 +1,10 @@ +package home + +import "templ-test/views/layout" + +templ Error(message error) { + @layout.Testing("Error") { +

Oops... :(

+

{ message.Error() }

+ } +} diff --git a/views/home.templ b/views/home/home.templ similarity index 80% rename from views/home.templ rename to views/home/home.templ index f02c7eb..0a1add7 100644 --- a/views/home.templ +++ b/views/home/home.templ @@ -1,9 +1,10 @@ -package views +package home import "templ-test/views/components/bootstrap" +import "templ-test/views/layout" templ Home() { - @Testing("Home", true) { + @layout.WithLayout("Home", true) {

this should be above the alert

@@ -14,9 +15,3 @@ templ Home() { } } - -templ List() { - @Testing("Lists", true) { - - } -} diff --git a/views/home/settings.templ b/views/home/settings.templ new file mode 100644 index 0000000..30da693 --- /dev/null +++ b/views/home/settings.templ @@ -0,0 +1,9 @@ +package home + +import "templ-test/views/layout" + +templ UserSettings() { + @layout.Testing("Settings") { +

This is not ready yet

+ } +} diff --git a/views/layout/body.templ b/views/layout/body.templ new file mode 100644 index 0000000..a626b65 --- /dev/null +++ b/views/layout/body.templ @@ -0,0 +1,27 @@ +package layout + +templ WithLayout(pageName string, useDarkMode bool) { + + @getHtmlHead() + + @bootstrapNavBar() + @getBodyHeader(pageName) +
+ { children... } +
+ + +} + +templ Testing(pageName string) { + + @getHtmlHead() + + @bootstrapNavBar() + @getBodyHeader(pageName) +
+ { children... } +
+ + +} diff --git a/views/layout/header.templ b/views/layout/header.templ new file mode 100644 index 0000000..7a338a0 --- /dev/null +++ b/views/layout/header.templ @@ -0,0 +1,19 @@ +package layout + +templ getHtmlHead() { + + + + + + + + + +} + +templ getBodyHeader(pageName string) { +
+

{ pageName }

+
+} \ No newline at end of file diff --git a/views/core.templ b/views/layout/navbar.templ similarity index 54% rename from views/core.templ rename to views/layout/navbar.templ index 1557aae..9101e8b 100644 --- a/views/core.templ +++ b/views/layout/navbar.templ @@ -1,51 +1,7 @@ -package views - -templ WithLayout(pageName string, useDarkMode bool) { - - @getHtmlHead() - - @bootstrapNavBar() - @getBodyHeader(pageName) -
- { children... } -
- - -} - -templ Testing(pageName string, useDarkMode bool) { - - @getHtmlHead() - - @bootstrapNavBar() - @getBodyHeader(pageName) -
- { children... } -
- - -} - -templ getHtmlHead() { - - - - - - - - - -} - -templ getBodyHeader(pageName string) { -
-

{ pageName }

-
-} +package layout templ bootstrapNavBar() { -