features/token-things #2
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,7 +10,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
tmp/
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-api/domain"
|
"git.jamestombleson.com/jtom38/newsbot-api/domain"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -16,6 +17,7 @@ type Users interface {
|
|||||||
Login(username, password string) (domain.LoginResponse, error)
|
Login(username, password string) (domain.LoginResponse, error)
|
||||||
SignUp(username, password string) (domain.BaseResponse, error)
|
SignUp(username, password string) (domain.BaseResponse, error)
|
||||||
RefreshJwtToken(username, refreshToken string) (domain.LoginResponse, error)
|
RefreshJwtToken(username, refreshToken string) (domain.LoginResponse, error)
|
||||||
|
RefreshJwtTokenFromContext(ctx echo.Context) (domain.LoginResponse, error)
|
||||||
RefreshSessionToken(jwtToken string) (domain.BaseResponse, error)
|
RefreshSessionToken(jwtToken string) (domain.BaseResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +83,23 @@ func (a userClient) RefreshJwtToken(username, refreshToken string) (domain.Login
|
|||||||
return bind, nil
|
return bind, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (a userClient) RefreshJwtTokenFromContext(ctx echo.Context) (domain.LoginResponse, error) {
|
||||||
|
resp := domain.LoginResponse{}
|
||||||
|
|
||||||
|
username, err := ctx.Cookie("newsbot.user")
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := ctx.Cookie("newsbot.refreshToken")
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.RefreshJwtToken(username.Value, refreshToken.Value)
|
||||||
|
}
|
||||||
|
|
||||||
func (a userClient) RefreshSessionToken(jwtToken string) (domain.BaseResponse, error) {
|
func (a userClient) RefreshSessionToken(jwtToken string) (domain.BaseResponse, error) {
|
||||||
endpoint := fmt.Sprintf("%s/%s/refresh/sessionToken", a.serverAddress, UserBaseRoute)
|
endpoint := fmt.Sprintf("%s/%s/refresh/sessionToken", a.serverAddress, UserBaseRoute)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ func main() {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
cfg := config.New()
|
cfg := config.New()
|
||||||
apiClient := apiclient.New(cfg.ServerAddress)
|
apiClient := apiclient.New(cfg.ApiServerAddress)
|
||||||
server := handlers.NewServer(ctx, cfg, apiClient)
|
server := handlers.NewServer(ctx, cfg, apiClient)
|
||||||
|
|
||||||
fmt.Println("The server is online and waiting for requests.")
|
fmt.Println("The server is online and waiting for requests.")
|
||||||
|
4
go.mod
4
go.mod
@ -4,7 +4,7 @@ go 1.22.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76
|
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76
|
||||||
github.com/a-h/templ v0.2.680
|
github.com/a-h/templ v0.2.747
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
@ -19,7 +19,7 @@ require (
|
|||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.22.0 // indirect
|
golang.org/x/crypto v0.22.0 // indirect
|
||||||
golang.org/x/net v0.24.0 // indirect
|
golang.org/x/net v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
)
|
)
|
||||||
|
8
go.sum
8
go.sum
@ -1,7 +1,7 @@
|
|||||||
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 h1:B9t5fcfVerMjqnXXPUmYwdmUk76EoEL8x9IRehqg2c4=
|
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 h1:B9t5fcfVerMjqnXXPUmYwdmUk76EoEL8x9IRehqg2c4=
|
||||||
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA=
|
git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA=
|
||||||
github.com/a-h/templ v0.2.680 h1:TflYFucxp5rmOxAXB9Xy3+QHTk8s8xG9+nCT/cLzjeE=
|
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
|
||||||
github.com/a-h/templ v0.2.680/go.mod h1:NQGQOycaPKBxRB14DmAaeIpcGC1AOBPJEMO4ozS7m90=
|
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/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 h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
@ -35,8 +35,8 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
|||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
@ -8,8 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Configs struct {
|
type Configs struct {
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
JwtSecret string
|
JwtSecret string
|
||||||
|
ApiServerAddress string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() Configs {
|
func New() Configs {
|
||||||
@ -20,8 +21,9 @@ func New() Configs {
|
|||||||
|
|
||||||
func getEnvConfig() Configs {
|
func getEnvConfig() Configs {
|
||||||
return Configs{
|
return Configs{
|
||||||
ServerAddress: os.Getenv("ServerAddress"),
|
ServerAddress: os.Getenv("ServerAddress"),
|
||||||
JwtSecret: os.Getenv("JwtSecret"),
|
JwtSecret: os.Getenv("JwtSecret"),
|
||||||
|
ApiServerAddress: os.Getenv("ApiServerAddress"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,43 +3,37 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
apidomain "git.jamestombleson.com/jtom38/newsbot-api/domain"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/articles"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/articles"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) ArticlesList(c echo.Context) error {
|
func (h *Handler) ArticlesList(c echo.Context) error {
|
||||||
_, err := ValidateJwt(c, h.config.JwtSecret, h.config.ServerAddress)
|
err := HasValidScope(c, apidomain.ScopeArticleRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusOK, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userToken, err := c.Cookie(domain.CookieToken)
|
resp, err := h.api.Articles.List(GetJwtToken(c), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusBadRequest, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.api.Articles.List(userToken.Value, 0)
|
|
||||||
if err != nil {
|
|
||||||
return Render(c, http.StatusBadRequest, layout.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
vm := models.ListArticlesViewModel{}
|
vm := models.ListArticlesViewModel{}
|
||||||
|
|
||||||
for _, article := range resp.Payload {
|
for _, article := range resp.Payload {
|
||||||
source, err := h.api.Sources.GetById(userToken.Value, article.SourceID)
|
source, err := h.api.Sources.GetById(GetJwtToken(c), article.SourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusBadRequest, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
item := models.ListArticleSourceModel {
|
item := models.ListArticleSourceModel{
|
||||||
Article: article,
|
Article: article,
|
||||||
Source: source.Payload[0],
|
Source: source.Payload[0],
|
||||||
}
|
}
|
||||||
vm.Items = append(vm.Items, item)
|
vm.Items = append(vm.Items, item)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Render(c, http.StatusOK, articles.List(vm))
|
return Render(c, http.StatusOK, articles.List(vm))
|
||||||
|
@ -49,6 +49,7 @@ func NewServer(ctx context.Context, configs config.Configs, apiClient apiclient.
|
|||||||
router.Pre(middleware.Logger())
|
router.Pre(middleware.Logger())
|
||||||
router.Pre(middleware.Recover())
|
router.Pre(middleware.Recover())
|
||||||
router.Use(middleware.Static("/internal/static"))
|
router.Use(middleware.Static("/internal/static"))
|
||||||
|
//router.Use(RefreshJwtMiddleware(apiClient))
|
||||||
|
|
||||||
router.GET("/", s.HomeIndex)
|
router.GET("/", s.HomeIndex)
|
||||||
router.GET("/about", s.HomeAbout)
|
router.GET("/about", s.HomeAbout)
|
||||||
@ -57,57 +58,23 @@ func NewServer(ctx context.Context, configs config.Configs, apiClient apiclient.
|
|||||||
debug.GET("/cookies", s.DebugCookies)
|
debug.GET("/cookies", s.DebugCookies)
|
||||||
|
|
||||||
articles := router.Group("/articles")
|
articles := router.Group("/articles")
|
||||||
|
//articles.Use(ValidateJwtMiddleware(configs.JwtSecret))
|
||||||
articles.GET("", s.ArticlesList)
|
articles.GET("", s.ArticlesList)
|
||||||
|
|
||||||
sources := router.Group("/sources")
|
sources := router.Group("/sources")
|
||||||
|
//sources.Use(ValidateJwtMiddleware(configs.JwtSecret))
|
||||||
sources.GET("", s.ListAllSources)
|
sources.GET("", s.ListAllSources)
|
||||||
|
sources.GET("/add", s.AddSource)
|
||||||
|
|
||||||
users := router.Group("/users")
|
users := router.Group("/users")
|
||||||
users.GET("/login", s.UserLogin)
|
users.GET("/login", s.UserLogin)
|
||||||
users.POST("/login", s.UserAfterLogin)
|
users.POST("/login", s.UserAfterLogin)
|
||||||
users.GET("/signup", s.UserSignUp)
|
users.GET("/signup", s.UserSignUp)
|
||||||
users.POST("/signup", s.UserAfterSignUp)
|
users.POST("/signup", s.UserAfterSignUp)
|
||||||
|
users.Use(ValidateJwtMiddleware(configs.JwtSecret))
|
||||||
users.GET("/logout", s.UsersLogout)
|
users.GET("/logout", s.UsersLogout)
|
||||||
users.GET("/profile", s.UserProfile)
|
users.GET("/profile", s.UserProfile)
|
||||||
|
|
||||||
s.Router = router
|
s.Router = router
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the token is not valid then an json error will be returned.
|
|
||||||
// If the token has the wrong scope, a json error will be returned.
|
|
||||||
// If the token passes all the checks, it is valid and is returned back to the caller.
|
|
||||||
//func (s *Handler) ValidateJwtToken(c echo.Context, requiredScope string) (JwtToken, error) {
|
|
||||||
// token, err := s.getJwtTokenFromContext(c)
|
|
||||||
// if err != nil {
|
|
||||||
// s.WriteMessage(c, ErrJwtMissing, http.StatusUnauthorized)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// err = token.hasExpired()
|
|
||||||
// if err != nil {
|
|
||||||
// return JwtToken{}, errors.New(ErrJwtExpired)
|
|
||||||
// //s.WriteMessage(c, ErrJwtExpired, http.StatusUnauthorized)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// err = token.hasScope(requiredScope)
|
|
||||||
// if err != nil {
|
|
||||||
// return JwtToken{}, errors.New(ErrJwtScopeMissing)
|
|
||||||
// //s.WriteMessage(c, ErrJwtScopeMissing, http.StatusUnauthorized)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if token.Iss != s.config.ServerAddress {
|
|
||||||
// return JwtToken{}, errors.New(ErrJwtInvalidIssuer)
|
|
||||||
// //s.WriteMessage(c, ErrJwtInvalidIssuer, http.StatusUnauthorized)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return token, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
//func (s *Handler) GetUserIdFromJwtToken(c echo.Context) int64 {
|
|
||||||
// token, err := s.getJwtTokenFromContext(c)
|
|
||||||
// if err != nil {
|
|
||||||
// s.WriteMessage(c, ErrJwtMissing, http.StatusUnauthorized)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return token.GetUserId()
|
|
||||||
//}
|
|
||||||
|
64
internal/handlers/middleware.go
Normal file
64
internal/handlers/middleware.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.jamestombleson.com/jtom38/newsbot-portal/apiclient"
|
||||||
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateJwtMiddleware(jwtSecret string) echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
cookie, err := c.Cookie(domain.CookieToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cookie.Value == "" {
|
||||||
|
return echo.NewHTTPError(401, "Authorization token is missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(cookie.Value, &jwtToken{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(jwtSecret), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return echo.NewHTTPError(401, "Invalid authorization token.")
|
||||||
|
//return errors.New("invalid jwt token")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := token.Claims.(*jwtToken)
|
||||||
|
if !claims.Exp.After(time.Now()) {
|
||||||
|
return echo.NewHTTPError(401, "Your Authorization token has expired.")
|
||||||
|
//return errors.New("the jwt token has expired")
|
||||||
|
}
|
||||||
|
//if claims.Iss != issuer {
|
||||||
|
// return jwtToken{}, errors.New("the issuer was invalid")
|
||||||
|
//}
|
||||||
|
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshJwtMiddleware(api apiclient.ApiClient) echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
resp, err := api.Users.RefreshJwtTokenFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCookie(c, domain.CookieToken, resp.Token, "/")
|
||||||
|
SetCookie(c, domain.CookieRefreshToken, resp.RefreshToken, "/")
|
||||||
|
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,32 +3,36 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/sources"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/sources"
|
||||||
|
|
||||||
|
apidomain "git.jamestombleson.com/jtom38/newsbot-api/domain"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) ListAllSources(c echo.Context) error {
|
func (h *Handler) ListAllSources(c echo.Context) error {
|
||||||
_, err := ValidateJwt(c, h.config.JwtSecret, h.config.ServerAddress)
|
err := HasValidScope(c, apidomain.ScopeSourceRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusOK, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userToken, err := c.Cookie(domain.CookieToken)
|
resp, err := h.api.Sources.ListAll(GetJwtToken(c), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusBadRequest, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.api.Sources.ListAll(userToken.Value, 0)
|
|
||||||
if err != nil {
|
|
||||||
return Render(c, http.StatusOK, layout.Error(err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Render(c, http.StatusOK, sources.ListAll(models.ListAllSourcesViewModel{
|
return Render(c, http.StatusOK, sources.ListAll(models.ListAllSourcesViewModel{
|
||||||
Items: resp.Payload,
|
Items: resp.Payload,
|
||||||
IsError: resp.IsError,
|
IsError: resp.IsError,
|
||||||
Message: resp.Message,
|
Message: resp.Message,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AddSource(c echo.Context) error {
|
||||||
|
err := HasValidScope(c, apidomain.ScopeSourceCreate)
|
||||||
|
if err != nil {
|
||||||
|
return RenderError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Render(c, http.StatusOK, sources.Add(models.AddSourcePayloadModel{}))
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/users"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/users"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -20,7 +20,10 @@ func (h *Handler) UserAfterLogin(c echo.Context) error {
|
|||||||
|
|
||||||
resp, err := h.api.Users.Login(user, password)
|
resp, err := h.api.Users.Login(user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusBadRequest, users.AfterLogin(err.Error(), false))
|
return Render(c, http.StatusBadRequest, users.AfterLogin(models.AfterLoginViewModel{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == "" {
|
if user == "" {
|
||||||
@ -31,7 +34,11 @@ func (h *Handler) UserAfterLogin(c echo.Context) error {
|
|||||||
SetCookie(c, domain.CookieRefreshToken, resp.RefreshToken, "/")
|
SetCookie(c, domain.CookieRefreshToken, resp.RefreshToken, "/")
|
||||||
SetCookie(c, domain.CookieUser, user, "/")
|
SetCookie(c, domain.CookieUser, user, "/")
|
||||||
|
|
||||||
return Render(c, http.StatusOK, users.AfterLogin("Login Successful!", true))
|
vm := models.AfterLoginViewModel{
|
||||||
|
Success: true,
|
||||||
|
Message: "Login Successful!",
|
||||||
|
}
|
||||||
|
return Render(c, http.StatusOK, users.AfterLogin(vm))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) UserSignUp(c echo.Context) error {
|
func (h *Handler) UserSignUp(c echo.Context) error {
|
||||||
@ -44,11 +51,17 @@ func (h *Handler) UserAfterSignUp(c echo.Context) error {
|
|||||||
|
|
||||||
resp, err := h.api.Users.SignUp(user, password)
|
resp, err := h.api.Users.SignUp(user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusBadRequest, users.AfterLogin(err.Error(), false))
|
return Render(c, http.StatusBadRequest, users.AfterLogin(models.AfterLoginViewModel{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
if resp.Message != "OK" {
|
if resp.Message != "OK" {
|
||||||
msg := fmt.Sprintf("Failed to create account. Message: %s", resp.Message)
|
msg := fmt.Sprintf("Failed to create account. Message: %s", resp.Message)
|
||||||
return Render(c, http.StatusBadRequest, users.AfterLogin(msg, false))
|
return Render(c, http.StatusBadRequest, users.AfterLogin(models.AfterLoginViewModel{
|
||||||
|
Message: msg,
|
||||||
|
Success: false,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
return Render(c, http.StatusOK, users.AfterSignUp("Registration Successful!", true))
|
return Render(c, http.StatusOK, users.AfterSignUp("Registration Successful!", true))
|
||||||
}
|
}
|
||||||
@ -58,9 +71,9 @@ func (h *Handler) UserProfile(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ForceLogout(c echo.Context) error {
|
func (h *Handler) ForceLogout(c echo.Context) error {
|
||||||
_, err := ValidateJwt(c, h.config.JwtSecret, h.config.ServerAddress)
|
err := IsLoggedIn(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Render(c, http.StatusOK, layout.Error(err))
|
return RenderError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.api.Users.RefreshSessionToken(GetJwtToken(c))
|
h.api.Users.RefreshSessionToken(GetJwtToken(c))
|
||||||
|
@ -4,9 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/config"
|
||||||
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/domain"
|
||||||
|
"git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -33,7 +36,31 @@ type jwtToken struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateJwt(ctx echo.Context, sharedSecret, issuer string) (jwtToken, error) {
|
func IsLoggedIn(ctx echo.Context) error {
|
||||||
|
_, err := GetUserInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasValidScope(ctx echo.Context, scope string) error {
|
||||||
|
token, err := GetUserInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userScopes := strings.Join(token.Scopes, ",")
|
||||||
|
if strings.Contains(userScopes, scope) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("required permission is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserInfo(ctx echo.Context) (jwtToken, error) {
|
||||||
|
cfg := config.New()
|
||||||
cookie, err := ctx.Cookie(domain.CookieToken)
|
cookie, err := ctx.Cookie(domain.CookieToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jwtToken{}, err
|
return jwtToken{}, err
|
||||||
@ -44,7 +71,7 @@ func ValidateJwt(ctx echo.Context, sharedSecret, issuer string) (jwtToken, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(cookie.Value, &jwtToken{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(cookie.Value, &jwtToken{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return []byte(sharedSecret), nil
|
return []byte(cfg.JwtSecret), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jwtToken{}, err
|
return jwtToken{}, err
|
||||||
@ -58,9 +85,6 @@ func ValidateJwt(ctx echo.Context, sharedSecret, issuer string) (jwtToken, error
|
|||||||
if !claims.Exp.After(time.Now()) {
|
if !claims.Exp.After(time.Now()) {
|
||||||
return jwtToken{}, errors.New("the jwt token has expired")
|
return jwtToken{}, errors.New("the jwt token has expired")
|
||||||
}
|
}
|
||||||
//if claims.Iss != issuer {
|
|
||||||
// return jwtToken{}, errors.New("the issuer was invalid")
|
|
||||||
//}
|
|
||||||
|
|
||||||
return *claims, nil
|
return *claims, nil
|
||||||
}
|
}
|
||||||
@ -79,7 +103,26 @@ func Render(ctx echo.Context, statusCode int, t templ.Component) error {
|
|||||||
|
|
||||||
// take the request context and make it a var
|
// take the request context and make it a var
|
||||||
request := ctx.Request().Context()
|
request := ctx.Request().Context()
|
||||||
|
|
||||||
|
//Check to see if we the echo context has the cookie we are looking for, if so, create a new context based on what we had and add the value
|
||||||
|
username, err := ctx.Cookie(domain.CookieUser)
|
||||||
|
if err == nil {
|
||||||
|
request = context.WithValue(request, domain.UserNameContext, username.Value)
|
||||||
|
} else {
|
||||||
|
request = context.WithValue(request, domain.UserNameContext, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Render(request, ctx.Response().Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderError(ctx echo.Context, err error) error {
|
||||||
|
var t templ.Component = layout.Error(err)
|
||||||
|
ctx.Response().Writer.WriteHeader(200)
|
||||||
|
ctx.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTML)
|
||||||
|
|
||||||
|
// take the request context and make it a var
|
||||||
|
request := ctx.Request().Context()
|
||||||
|
|
||||||
//Check to see if we the echo context has the cookie we are looking for, if so, create a new context based on what we had and add the value
|
//Check to see if we the echo context has the cookie we are looking for, if so, create a new context based on what we had and add the value
|
||||||
username, err := ctx.Cookie(domain.CookieUser)
|
username, err := ctx.Cookie(domain.CookieUser)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -7,3 +7,7 @@ type ListAllSourcesViewModel struct {
|
|||||||
Message string
|
Message string
|
||||||
Items []domain.SourceDto
|
Items []domain.SourceDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddSourcePayloadModel struct {
|
||||||
|
|
||||||
|
}
|
6
internal/models/users.go
Normal file
6
internal/models/users.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type AfterLoginViewModel struct {
|
||||||
|
Message string
|
||||||
|
Success bool
|
||||||
|
}
|
10
internal/views/sources/add.templ
Normal file
10
internal/views/sources/add.templ
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package sources
|
||||||
|
|
||||||
|
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/layout"
|
||||||
|
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
||||||
|
|
||||||
|
templ Add(model models.AddSourcePayloadModel) {
|
||||||
|
@layout.WithTemplate() {
|
||||||
|
<form hx-post="/sources/add"></form>
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
package users
|
package users
|
||||||
|
|
||||||
|
import "git.jamestombleson.com/jtom38/newsbot-portal/internal/models"
|
||||||
|
|
||||||
// This is returned after the user logs into the application.
|
// This is returned after the user logs into the application.
|
||||||
// It just returns a partial view because it will overlap with the existing template.
|
// It just returns a partial view because it will overlap with the existing template.
|
||||||
templ AfterLogin(message string, success bool) {
|
templ AfterLogin(vm models.AfterLoginViewModel) {
|
||||||
if success {
|
if vm.Success {
|
||||||
<div class="notification is-success">
|
<div class="notification is-success">
|
||||||
{ message }
|
{ vm.Message }
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="notification is-error">
|
<div class="notification is-error">
|
||||||
{ message }
|
{ vm.Message }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user