diff --git a/apiclient/client.go b/apiclient/client.go index 153bc38..86ae79f 100644 --- a/apiclient/client.go +++ b/apiclient/client.go @@ -9,11 +9,13 @@ const ( type ApiClient struct { Articles Articles Users Users + Sources Sources } func New(serverAddress string) ApiClient { return ApiClient{ Articles: newArticleService(serverAddress), Users: newUserService(serverAddress), + Sources: newSourceService(serverAddress), } } diff --git a/apiclient/sources.go b/apiclient/sources.go new file mode 100644 index 0000000..5af6d9f --- /dev/null +++ b/apiclient/sources.go @@ -0,0 +1,77 @@ +package apiclient + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "git.jamestombleson.com/jtom38/newsbot-api/domain" +) + +const ( + SourcesBaseRoute = "api/v1/sources" +) + +type Sources interface { + ListAll(jwt string, page int) (domain.SourcesResponse, error) + GetById(jwt string, id int64) (domain.SourcesResponse, error) +} + +type sourceClient struct { + serverAddress string + client http.Client +} + +func newSourceService(serverAddress string) sourceClient { + return sourceClient{ + serverAddress: serverAddress, + client: http.Client{}, + } +} + +func (c sourceClient) ListAll(jwt string, page int) (domain.SourcesResponse, error) { + var bind domain.SourcesResponse + endpoint := fmt.Sprintf("%s/%s?page=%U", c.serverAddress, SourcesBaseRoute, page) + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return bind, err + } + + req.Header.Set(HeaderContentType, ApplicationJson) + req.Header.Set(HeaderAuthorization, fmt.Sprintf("Bearer %s", jwt)) + resp, err := c.client.Do(req) + if err != nil { + return bind, err + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&bind) + if err != nil { + return bind, err + } + + if (resp.StatusCode != 200) { + return bind, errors.New(bind.Message) + } + + return bind, nil +} + +func (c sourceClient) GetById(jwt string, id int64) (domain.SourcesResponse, error) { + bind := domain.SourcesResponse{} + endpoint := fmt.Sprintf("%s/%s/%d", c.serverAddress, SourcesBaseRoute, id) + + statusCode, err := Get(c.client, endpoint, jwt, &bind) + if err != nil { + return bind, err + } + + if (statusCode != 200) { + return bind, errors.New(bind.Message) + } + + return bind, nil +} diff --git a/apiclient/util.go b/apiclient/util.go index 4c7d212..8be09a0 100644 --- a/apiclient/util.go +++ b/apiclient/util.go @@ -54,7 +54,7 @@ func PostUrlAuthorized(client http.Client, endpoint, jwtToken string, t any) err if err != nil { return err } - + return nil } @@ -75,6 +75,29 @@ func PostBodyUrl(client http.Client, endpoint string, body any, t any) error { if err != nil { return err } - + return nil } + +func Get(client http.Client, endpoint, jwt string, t any) (int, error) { + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return -1, err + } + + req.Header.Set(HeaderContentType, ApplicationJson) + req.Header.Set(HeaderAuthorization, fmt.Sprintf("Bearer %s", jwt)) + resp, err := client.Do(req) + if err != nil { + return -1, err + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&t) + if err != nil { + return -1, err + } + + return resp.StatusCode, nil +} diff --git a/go.mod b/go.mod index 0dac382..e595ac3 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,10 @@ module git.jamestombleson.com/jtom38/newsbot-portal go 1.22.1 require ( - git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240510021003-4e9a17209f02 + git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240603002809-9237369e5a76 github.com/a-h/templ v0.2.680 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo-jwt/v4 v4.2.0 github.com/labstack/echo/v4 v4.12.0 ) diff --git a/go.sum b/go.sum index dc0ffa7..94ff39f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240510021003-4e9a17209f02 h1:yvu0Fnpw19YUH+AXvMQxJV8mUyEXkvpZqd8HctpwMrI= -git.jamestombleson.com/jtom38/newsbot-api v0.0.0-20240510021003-4e9a17209f02/go.mod h1:A3UdJyQ/IEy3utEwJiC4nbi0ohfgrUNRLTei2iZhLLA= +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= github.com/a-h/templ v0.2.680 h1:TflYFucxp5rmOxAXB9Xy3+QHTk8s8xG9+nCT/cLzjeE= github.com/a-h/templ v0.2.680/go.mod h1:NQGQOycaPKBxRB14DmAaeIpcGC1AOBPJEMO4ozS7m90= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -12,8 +12,6 @@ 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c= -github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= diff --git a/internal/handlers/articles.go b/internal/handlers/articles.go index 89e3cde..91d7479 100644 --- a/internal/handlers/articles.go +++ b/internal/handlers/articles.go @@ -25,9 +25,21 @@ func (h *Handler) ArticlesList(c echo.Context) error { if err != nil { return Render(c, http.StatusBadRequest, layout.Error(err)) } + + vm := models.ListArticlesViewModel{} - vm := models.ListArticlesViewModel{ - Items: resp.Payload, + for _, article := range resp.Payload { + source, err := h.api.Sources.GetById(userToken.Value, article.SourceID) + if err != nil { + return Render(c, http.StatusBadRequest, layout.Error(err)) + } + + item := models.ListArticleSourceModel { + Article: article, + Source: source.Payload[0], + } + vm.Items = append(vm.Items, item) + } return Render(c, http.StatusOK, articles.List(vm)) diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index 26f6c8c..9ef3e35 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -59,13 +59,15 @@ func NewServer(ctx context.Context, configs config.Configs, apiClient apiclient. articles := router.Group("/articles") articles.GET("", s.ArticlesList) + sources := router.Group("/sources") + sources.GET("", s.ListAllSources) + users := router.Group("/users") users.GET("/login", s.UserLogin) users.POST("/login", s.UserAfterLogin) users.GET("/signup", s.UserSignUp) users.POST("/signup", s.UserAfterSignUp) - - //users.Use(echojwt.WithConfig(jwtConfig)) + users.GET("/logout", s.UsersLogout) users.GET("/profile", s.UserProfile) s.Router = router diff --git a/internal/handlers/sources.go b/internal/handlers/sources.go new file mode 100644 index 0000000..f912e61 --- /dev/null +++ b/internal/handlers/sources.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "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/views/layout" + "git.jamestombleson.com/jtom38/newsbot-portal/internal/views/sources" + "github.com/labstack/echo/v4" +) + +func (h *Handler) ListAllSources(c echo.Context) error { + _, err := ValidateJwt(c, h.config.JwtSecret, h.config.ServerAddress) + if err != nil { + return Render(c, http.StatusOK, layout.Error(err)) + } + + userToken, err := c.Cookie(domain.CookieToken) + if err != nil { + return Render(c, http.StatusBadRequest, layout.Error(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{ + Items: resp.Payload, + IsError: resp.IsError, + Message: resp.Message, + })) +} diff --git a/internal/handlers/users.go b/internal/handlers/users.go index b12bebb..b76311c 100644 --- a/internal/handlers/users.go +++ b/internal/handlers/users.go @@ -23,6 +23,10 @@ func (h *Handler) UserAfterLogin(c echo.Context) error { return Render(c, http.StatusBadRequest, users.AfterLogin(err.Error(), false)) } + if user == "" { + user = "admin" + } + SetCookie(c, domain.CookieToken, resp.Token, "/") SetCookie(c, domain.CookieRefreshToken, resp.RefreshToken, "/") SetCookie(c, domain.CookieUser, user, "/") @@ -62,3 +66,11 @@ func (h *Handler) ForceLogout(c echo.Context) error { h.api.Users.RefreshSessionToken(GetJwtToken(c)) return nil } + +func (h *Handler) UsersLogout(c echo.Context) error { + SetCookie(c, domain.CookieUser, "", "/") + SetCookie(c, domain.CookieRefreshToken, "", "/") + SetCookie(c, domain.CookieToken, "", "/") + + return Render(c, http.StatusOK, users.Logout()) +} diff --git a/internal/models/articles.go b/internal/models/articles.go index a94a5e3..d934049 100644 --- a/internal/models/articles.go +++ b/internal/models/articles.go @@ -3,5 +3,10 @@ package models import "git.jamestombleson.com/jtom38/newsbot-api/domain" type ListArticlesViewModel struct { - Items []domain.ArticleDto -} \ No newline at end of file + Items []ListArticleSourceModel +} + +type ListArticleSourceModel struct { + Article domain.ArticleDto + Source domain.SourceDto +} diff --git a/internal/models/sources.go b/internal/models/sources.go new file mode 100644 index 0000000..ac44124 --- /dev/null +++ b/internal/models/sources.go @@ -0,0 +1,9 @@ +package models + +import "git.jamestombleson.com/jtom38/newsbot-api/domain" + +type ListAllSourcesViewModel struct { + IsError bool + Message string + Items []domain.SourceDto +} diff --git a/internal/static/Newsbot.svg b/internal/static/Newsbot.svg new file mode 100644 index 0000000..8a2f76e --- /dev/null +++ b/internal/static/Newsbot.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/internal/static/css/main.css b/internal/static/css/main.css new file mode 100644 index 0000000..d3a17ff --- /dev/null +++ b/internal/static/css/main.css @@ -0,0 +1,4 @@ +.pin-bottom { + position: absolute; + bottom: 0; +} \ No newline at end of file diff --git a/internal/views/articles/list.templ b/internal/views/articles/list.templ index f727453..10cff5d 100644 --- a/internal/views/articles/list.templ +++ b/internal/views/articles/list.templ @@ -3,26 +3,15 @@ package articles import ( "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/bulma" ) templ List(model models.ListArticlesViewModel) { @layout.WithTemplate() { @filterBar() for _, item := range model.Items { -
-
-
- -
-
-
-
- -
-
-
+ @bulma.ArticleCardWithThumbnail(item.Article.Title, item.Article.Thumbnail, item.Article.Url, item.Article.PubDate.String(), item.Source.DisplayName ) + } } } diff --git a/internal/views/bulma/card.templ b/internal/views/bulma/card.templ new file mode 100644 index 0000000..9b226a2 --- /dev/null +++ b/internal/views/bulma/card.templ @@ -0,0 +1,22 @@ +package bulma + +templ ArticleCardWithThumbnail(title, thumbnailUrl, url, datePosted, sourceName string) { +
+
+
+ +
+
+
+
+
+ { title } +
+
+
+ { datePosted }
+ { sourceName } +
+
+
+} diff --git a/internal/views/layout/footer.templ b/internal/views/layout/footer.templ index d8d8e8c..88627e8 100644 --- a/internal/views/layout/footer.templ +++ b/internal/views/layout/footer.templ @@ -1,7 +1,9 @@ package layout templ footer() { + } diff --git a/internal/views/layout/navbar.templ b/internal/views/layout/navbar.templ index b77d84f..b5162d9 100644 --- a/internal/views/layout/navbar.templ +++ b/internal/views/layout/navbar.templ @@ -3,12 +3,7 @@ package layout templ navBar() {