From db9b0bbb1d5326790f30d7d8595ece8144123914 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 4 May 2024 11:47:02 -0700 Subject: [PATCH 01/10] Removed the settings handler because its not needed anymore --- internal/handler/v1/settings.go | 39 --------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 internal/handler/v1/settings.go diff --git a/internal/handler/v1/settings.go b/internal/handler/v1/settings.go deleted file mode 100644 index 26367af..0000000 --- a/internal/handler/v1/settings.go +++ /dev/null @@ -1,39 +0,0 @@ -package v1 - -import ( - "encoding/json" - "net/http" - - "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -// GetSettings -// @Summary Returns a object based on the Key that was given. -// @Param key path string true "Settings Key value" -// @Produce application/json -// @Tags Settings -// @Router /v1/settings/{key} [get] -func (s *Handler) getSettings(c echo.Context) error { - id := c.Param("ID") - - uuid, err := uuid.Parse(id) - if err != nil { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: err.Error(), - }) - } - - res, err := s.Db.GetSourceByID(c.Request().Context(), uuid) - if err != nil { - return c.JSON(http.StatusInternalServerError, err.Error()) - } - - bResult, err := json.Marshal(res) - if err != nil { - return c.JSON(http.StatusInternalServerError, err.Error()) - } - - return c.JSON(http.StatusOK, bResult) -} -- 2.45.2 From 238f617452a39e72a8ec40decd9b9bac631c69cb Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 4 May 2024 11:47:30 -0700 Subject: [PATCH 02/10] jwt was added and started to tie it into handlers --- go.mod | 2 + go.sum | 4 ++ internal/handler/v1/handler.go | 31 ++++++++++- internal/handler/v1/jwt.go | 99 ++++++++++++++++++++++++++++++++++ internal/services/config.go | 2 + makefile | 1 + 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 internal/handler/v1/jwt.go diff --git a/go.mod b/go.mod index f4bf926..8cc9542 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ require ( github.com/PuerkitoBio/goquery v1.8.0 github.com/glebarez/go-sqlite v1.22.0 github.com/go-rod/rod v0.107.1 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/huandu/go-sqlbuilder v1.27.1 github.com/joho/godotenv v1.4.0 + github.com/labstack/echo-jwt/v4 v4.2.0 github.com/labstack/echo/v4 v4.12.0 github.com/mmcdole/gofeed v1.1.3 github.com/nicklaw5/helix/v2 v2.4.0 diff --git a/go.sum b/go.sum index a92e69f..0711bf5 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= @@ -60,6 +62,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/handler/v1/handler.go b/internal/handler/v1/handler.go index 149de1f..f39ad3d 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -3,7 +3,10 @@ package v1 import ( "context" "database/sql" + "errors" + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" swagger "github.com/swaggo/echo-swagger" @@ -17,7 +20,6 @@ import ( type Handler struct { Router *echo.Echo Db *database.Queries - //dto *dto.DtoClient config services.Configs repo services.RepositoryService } @@ -28,6 +30,7 @@ const ( ErrUnableToParseId = "Unable to parse the requested ID" ErrRecordMissing = "The requested record was not found" ErrFailedToCreateRecord = "The record was not created due to a database problem" + ErrUserUnknown = "User is unknown" ResponseMessageSuccess = "Success" ) @@ -47,6 +50,13 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han repo: services.NewRepositoryService(conn), } + jwtConfig := echojwt.Config{ + NewClaimsFunc: func(c echo.Context) jwt.Claims { + return new(JwtToken) + }, + SigningKey: []byte(configs.JwtSecret), + } + router := echo.New() router.Pre(middleware.RemoveTrailingSlash()) router.Pre(middleware.Logger()) @@ -55,6 +65,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han v1 := router.Group("/api/v1") articles := v1.Group("/articles") + articles.Use(echojwt.WithConfig(jwtConfig)) articles.GET("", s.listArticles) articles.GET(":id", s.getArticle) articles.GET(":id/details", s.getArticleDetails) @@ -76,6 +87,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han //settings.GET("/", s.getSettings) sources := v1.Group("/sources") + sources.Use(echojwt.WithConfig(jwtConfig)) sources.GET("", s.listSources) sources.GET("/by/source", s.listSourcesBySource) sources.GET("/by/sourceAndName", s.GetSourceBySourceAndName) @@ -89,6 +101,7 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han sources.POST("/:ID/enable", s.enableSource) subs := v1.Group("/subscriptions") + subs.Use(echojwt.WithConfig(jwtConfig)) subs.GET("/", s.ListSubscriptions) subs.GET("/details", s.ListSubscriptionDetails) subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId) @@ -120,3 +133,19 @@ func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) e Message: msg, }) } + +func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) { + // Make sure that the request came with a jwtToken + token, ok := c.Get("user").(*jwt.Token) + if !ok { + return JwtToken{}, errors.New(ErrJwtMissing) + } + + // Generate the claims from the token + claims, ok := token.Claims.(*JwtToken) + if !ok { + return JwtToken{}, errors.New(ErrJwtClaimsMissing) + } + + return *claims, nil +} diff --git a/internal/handler/v1/jwt.go b/internal/handler/v1/jwt.go new file mode 100644 index 0000000..b7f1222 --- /dev/null +++ b/internal/handler/v1/jwt.go @@ -0,0 +1,99 @@ +package v1 + +import ( + "errors" + "strings" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/golang-jwt/jwt/v5" +) + +const ( + ErrJwtMissing = "auth token is missing" + ErrJwtClaimsMissing = "claims missing on token" + ErrJwtExpired = "auth token has expired" + ErrJwtScopeMissing = "required scope is missing" +) + +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 (j JwtToken) IsValid(scope string) error { + err := j.hasExpired() + if err != nil { + return err + } + + err = j.hasScope(scope) + if err != nil { + return err + } + + return nil +} + +func (j JwtToken) GetUsername() string { + return j.UserName +} + +func (j JwtToken) hasExpired() error { + // Check to see if the token has expired + hasExpired := j.Exp.Compare(time.Now()) + if hasExpired == -1 { + return errors.New(ErrJwtExpired) + } + return nil +} + +func (j JwtToken) hasScope(scope string) error { + // they have the scope to access everything, so let them pass. + if strings.Contains(domain.ScopeAll, scope) { + return nil + } + + for _, s := range j.Scopes { + if strings.Contains(s, scope) { + return nil + } + } + return errors.New(ErrJwtScopeMissing) +} + +func (h *Handler) generateJwt(username, issuer string) (string, error) { + return h.generateJwtWithExp(username, issuer, time.Now().Add(10*time.Minute)) +} + +func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Time) (string, error) { + secret := []byte(h.config.JwtSecret) + + // Anyone who wants to decrypt the key needs to use the same method + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["exp"] = expiresAt + claims["authorized"] = true + claims["username"] = username + claims["iss"] = issuer + + var scopes []string + if username == "admin" { + scopes = append(scopes, domain.ScopeAll) + claims["scopes"] = scopes + } else { + scopes = append(scopes, domain.ScopeRead) + claims["scopes"] = scopes + } + + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/internal/services/config.go b/internal/services/config.go index b1acbf6..8a923a9 100644 --- a/internal/services/config.go +++ b/internal/services/config.go @@ -34,6 +34,7 @@ const ( type Configs struct { ServerAddress string + JwtSecret string RedditEnabled bool RedditPullTop bool @@ -64,6 +65,7 @@ func NewConfig() ConfigClient { func GetEnvConfig() Configs { return Configs{ ServerAddress: os.Getenv(ServerAddress), + JwtSecret: os.Getenv("JwtSecret"), RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)), RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)), diff --git a/makefile b/makefile index b2d1ed4..ad19d77 100644 --- a/makefile +++ b/makefile @@ -6,6 +6,7 @@ build: ## builds the application with the current go runtime ~/go/bin/swag f ~/go/bin/swag init -g cmd/server.go go build cmd/server.go + ls -lh server docker-build: ## Generates the docker image docker build -t "newsbot.collector.api" . -- 2.45.2 From f0d36eb2ab2299a8ebe15375c8931bbefafb170f Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 4 May 2024 11:58:10 -0700 Subject: [PATCH 03/10] Refactored some tables to remove subscriptions and break out the naming --- .../migrations/20240425083756_init.sql | 23 +++- internal/domain/entity.go | 73 +++++++---- internal/domain/scopes.go | 6 + internal/repository/alertDiscord.go | 122 ++++++++++++++++++ internal/repository/alertDiscord_test.go | 63 +++++++++ internal/repository/userSourceSubscription.go | 120 +++++++++++++++++ internal/services/database.go | 24 ++-- 7 files changed, 396 insertions(+), 35 deletions(-) create mode 100644 internal/domain/scopes.go create mode 100644 internal/repository/alertDiscord.go create mode 100644 internal/repository/alertDiscord_test.go create mode 100644 internal/repository/userSourceSubscription.go diff --git a/internal/database/migrations/20240425083756_init.sql b/internal/database/migrations/20240425083756_init.sql index 96e82ae..8ab36dd 100644 --- a/internal/database/migrations/20240425083756_init.sql +++ b/internal/database/migrations/20240425083756_init.sql @@ -72,15 +72,36 @@ CREATE Table Sources ( Tags TEXT NOT NULL ); +/* CREATE TABLE Subscriptions ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, DiscordWebHookID NUMBER NOT NULL, SourceID NUMBER NOT NULL, UserID NUMBER NOT NULL ); +*/ + +CREATE TABLE UserSourceSubscriptions ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + CreatedAt DATETIME NOT NULL, + UpdatedAt DATETIME NOT NULL, + DeletedAt DATETIME NOT NULL, + UserID NUMBER NOT NULL, + SourceID NUMBER NOT NULL +); + +CREATE TABLE AlertDiscord ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + CreatedAt DATETIME NOT NULL, + UpdatedAt DATETIME NOT NULL, + DeletedAt DATETIME NOT NULL, + UserID NUMBER NOT NULL, + SourceID NUMBER NOT NULL, + DiscordWebHookID NUMBER NOT NULL +); CREATE TABLE Users ( ID INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/internal/domain/entity.go b/internal/domain/entity.go index bc56a9b..6983fb5 100644 --- a/internal/domain/entity.go +++ b/internal/domain/entity.go @@ -4,6 +4,18 @@ import ( "time" ) +// This links a source to a discord webhook. +// It is owned by a user so they can remove the link +type AlertDiscordEntity struct { + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + UserID int64 + SourceID int64 + DiscordWebHookId int64 +} + type ArticleEntity struct { ID int64 CreatedAt time.Time @@ -35,10 +47,11 @@ type DiscordWebHookEntity struct { CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time - Url string - Server string - Channel string - Enabled bool + UserID int64 + Url string + Server string + Channel string + Enabled bool } type IconEntity struct { @@ -61,38 +74,50 @@ type SettingEntity struct { } type SourceEntity struct { - ID int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time - // Who will collect from it. Used + // Who will collect from it. Used // domain.SourceCollector... - Source string + Source string // Human Readable value to state what is getting collected DisplayName string // Tells the parser where to look for data - Url string - + Url string + // Static tags for this defined record - Tags string + Tags string // If the record is disabled, then it will be skipped on processing - Enabled bool + Enabled bool } -type SubscriptionEntity struct { - ID int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - SourceID int64 - SourceType string - SourceName string - DiscordID int64 - DiscordName string +//type SubscriptionEntity struct { +// ID int64 +// CreatedAt time.Time +// UpdatedAt time.Time +// DeletedAt time.Time +// UserID int64 +// SourceID int64 +// //SourceType string +// //SourceName string +// DiscordID int64 +// //DiscordName string +//} + +// This defines what sources a user wants to follow. +// These will show up for the user as a front page +type UserSourceSubscriptionEntity struct { + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + UserID int64 + SourceID int64 } type UserEntity struct { diff --git a/internal/domain/scopes.go b/internal/domain/scopes.go new file mode 100644 index 0000000..c3ef7df --- /dev/null +++ b/internal/domain/scopes.go @@ -0,0 +1,6 @@ +package domain + +const ( + ScopeAll = "newsbot:all" + ScopeRead = "newsbot:read" +) diff --git a/internal/repository/alertDiscord.go b/internal/repository/alertDiscord.go new file mode 100644 index 0000000..e6ef6de --- /dev/null +++ b/internal/repository/alertDiscord.go @@ -0,0 +1,122 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type AlertDiscordRepo interface { + Create(ctx context.Context, userId, sourceId, webhookId int64) (int64, error) + SoftDelete(ctx context.Context, id int64) (int64, error) + Restore(ctx context.Context, id int64) (int64, error) + Delete(ctx context.Context, id int64) (int64, error) + ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.AlertDiscordEntity, error) +} + +type alertDiscordRepository struct { + conn *sql.DB + defaultLimit int + defaultOffset int +} + +func NewAlertDiscordRepository(conn *sql.DB) alertDiscordRepository { + return alertDiscordRepository{ + conn: conn, + defaultLimit: 50, + defaultOffset: 50, + } +} + +func (r alertDiscordRepository) Create(ctx context.Context, userId, sourceId, webhookId int64) (int64, error) { + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("AlertDiscord") + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID", "DiscordWebHookID") + queryBuilder.Values(dt, dt, timeZero, userId, sourceId, webhookId) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r alertDiscordRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { + return softDeleteRow(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) Restore(ctx context.Context, id int64) (int64, error) { + return restoreRow(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) Delete(ctx context.Context, id int64) (int64, error) { + return deleteFromTable(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.AlertDiscordEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("AlertDiscord") + builder.Where( + builder.Equal("UserID", userId), + ) + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.AlertDiscordEntity{}, err + } + + data := r.processRows(rows) + if len(data) == 0 { + return []domain.AlertDiscordEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ur alertDiscordRepository) processRows(rows *sql.Rows) []domain.AlertDiscordEntity { + items := []domain.AlertDiscordEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt time.Time + var userId int64 + var sourceId int64 + var webhookId int64 + + err := rows.Scan( + &id, &createdAt, &updatedAt, &deletedAt, + &userId, &sourceId, &webhookId, + ) + if err != nil { + fmt.Println(err) + } + + item := domain.AlertDiscordEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + UserID: userId, + SourceID: sourceId, + DiscordWebHookId: webhookId, + } + + items = append(items, item) + } + + return items +} diff --git a/internal/repository/alertDiscord_test.go b/internal/repository/alertDiscord_test.go new file mode 100644 index 0000000..e9cadcb --- /dev/null +++ b/internal/repository/alertDiscord_test.go @@ -0,0 +1,63 @@ +package repository_test + +import ( + "context" + "testing" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +func TestAlertDiscordCreate(t *testing.T) { + t.Log(time.Time{}) + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + r := repository.NewAlertDiscordRepository(db) + created, err := r.Create(context.Background(), 1, 1, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + if created != 1 { + t.Log("failed to create the record") + t.FailNow() + } +} + +func TestAlertDiscordCreateAndValidate(t *testing.T) { + t.Log(time.Time{}) + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + source := repository.NewSourceRepository(db) + source.Create(context.Background(), domain.SourceCollectorRss, "Unit Testing", "www.fake.com", "testing,units", true) + sourceRecord, _ := source.GetBySourceAndName(context.Background(), domain.SourceCollectorRss, "Unit Testing") + + webhookRepo := repository.NewDiscordWebHookRepository(db) + webhookRepo.Create(context.Background(), 999, "discord.com", "Unit Testing", "memes", true) + webhook, _ := webhookRepo.GetByUrl(context.Background(), "discord.com") + + r := repository.NewAlertDiscordRepository(db) + r.Create(context.Background(), 999, sourceRecord.ID, webhook.ID) + alert, err := r.ListByUser(context.Background(), 0, 10, 999) + if err != nil { + t.Error(err) + t.FailNow() + } + + if len(alert) != 1 { + t.Error("got the incorrect number of rows back") + t.FailNow() + } +} diff --git a/internal/repository/userSourceSubscription.go b/internal/repository/userSourceSubscription.go new file mode 100644 index 0000000..7a9a5c2 --- /dev/null +++ b/internal/repository/userSourceSubscription.go @@ -0,0 +1,120 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type UserSourceRepo interface { + Create(ctx context.Context, userId, sourceId int64) (int64, error) + SoftDelete(ctx context.Context, id int64) (int64, error) + Restore(ctx context.Context, id int64) (int64, error) + Delete(ctx context.Context, id int64) (int64, error) + ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.UserSourceSubscriptionEntity, error) +} + +type userSourceRepository struct { + conn *sql.DB + defaultLimit int + defaultOffset int +} + +func NewUserSourceRepository(conn *sql.DB) userSourceRepository { + return userSourceRepository{ + conn: conn, + defaultLimit: 50, + defaultOffset: 50, + } +} + +func (r userSourceRepository) Create(ctx context.Context, userId, sourceId int64) (int64, error) { + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("UserSourceSubscriptions") + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID") + queryBuilder.Values(dt, dt, timeZero, userId, sourceId) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r userSourceRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { + return softDeleteRow(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) Restore(ctx context.Context, id int64) (int64, error) { + return restoreRow(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) Delete(ctx context.Context, id int64) (int64, error) { + return deleteFromTable(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.UserSourceSubscriptionEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("UserSourceSubscriptions") + builder.Where( + builder.Equal("UserID", userId), + ) + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.UserSourceSubscriptionEntity{}, err + } + + data := r.processRows(rows) + if len(data) == 0 { + return []domain.UserSourceSubscriptionEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ur userSourceRepository) processRows(rows *sql.Rows) []domain.UserSourceSubscriptionEntity { + items := []domain.UserSourceSubscriptionEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt time.Time + var userId int64 + var sourceId int64 + + err := rows.Scan( + &id, &createdAt, &updatedAt, &deletedAt, + &userId, &sourceId, + ) + if err != nil { + fmt.Println(err) + } + + item := domain.UserSourceSubscriptionEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + UserID: userId, + SourceID: sourceId, + } + + items = append(items, item) + } + + return items +} diff --git a/internal/services/database.go b/internal/services/database.go index 9d11878..d5db9cd 100644 --- a/internal/services/database.go +++ b/internal/services/database.go @@ -7,19 +7,23 @@ import ( ) type RepositoryService struct { - Articles repository.ArticlesRepo - DiscordWebHooks repository.DiscordWebHookRepo - Sources repository.Sources - Users repository.Users - RefreshTokens repository.RefreshToken + AlertDiscord repository.AlertDiscordRepo + Articles repository.ArticlesRepo + DiscordWebHooks repository.DiscordWebHookRepo + RefreshTokens repository.RefreshToken + Sources repository.Sources + Users repository.Users + UserSourceSubscriptions repository.UserSourceRepo } func NewRepositoryService(conn *sql.DB) RepositoryService { return RepositoryService{ - Articles: repository.NewArticleRepository(conn), - DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), - Sources: repository.NewSourceRepository(conn), - Users: repository.NewUserRepository(conn), - RefreshTokens: repository.NewRefreshTokenRepository(conn), + AlertDiscord: repository.NewAlertDiscordRepository(conn), + Articles: repository.NewArticleRepository(conn), + DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), + RefreshTokens: repository.NewRefreshTokenRepository(conn), + Sources: repository.NewSourceRepository(conn), + Users: repository.NewUserRepository(conn), + UserSourceSubscriptions: repository.NewUserSourceRepository(conn), } } -- 2.45.2 From 53b04696471a425a2f3b94eea90fc21187b51487 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 4 May 2024 11:58:35 -0700 Subject: [PATCH 04/10] discordwebhooks now bind to a user id. More work to do though --- internal/handler/v1/discordwebhooks.go | 14 ++++++- internal/handler/v1/sources.go | 45 ++++++++------------- internal/repository/discordWebHooks.go | 14 ++++--- internal/repository/discordWebHooks_test.go | 18 ++++----- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/internal/handler/v1/discordwebhooks.go b/internal/handler/v1/discordwebhooks.go index 328d179..275a256 100644 --- a/internal/handler/v1/discordwebhooks.go +++ b/internal/handler/v1/discordwebhooks.go @@ -111,6 +111,13 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse func (s *Handler) NewDiscordWebHook(c echo.Context) error { + token, err := s.getJwtToken(c) + if err != nil { + return c.JSON(http.StatusUnauthorized, domain.BaseResponse{ + Message: ErrJwtMissing, + }) + } + _url := c.QueryParam("url") _server := c.QueryParam("server") _channel := c.QueryParam("channel") @@ -136,7 +143,12 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { }) } - rows, err := s.repo.DiscordWebHooks.Create(c.Request().Context(), _url, _server, _channel, true) + user, err := s.repo.Users.GetByName(token.UserName) + if err != nil { + s.WriteMessage(c, ErrUserUnknown, http.StatusBadRequest) + } + + rows, err := s.repo.DiscordWebHooks.Create(c.Request().Context(), user.ID, _url, _server, _channel, true) if err != nil { s.WriteError(c, err, http.StatusInternalServerError) } diff --git a/internal/handler/v1/sources.go b/internal/handler/v1/sources.go index 2297858..496be4c 100644 --- a/internal/handler/v1/sources.go +++ b/internal/handler/v1/sources.go @@ -10,22 +10,11 @@ import ( "git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" - "git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" "git.jamestombleson.com/jtom38/newsbot-api/internal/services" "github.com/google/uuid" "github.com/labstack/echo/v4" ) -type ListSources struct { - ApiStatusModel - Payload []models.SourceDto `json:"payload"` -} - -type GetSource struct { - ApiStatusModel - Payload models.SourceDto `json:"payload"` -} - // ListSources // @Summary Lists the top 50 records // @Param page query string false "page number" @@ -35,7 +24,7 @@ type GetSource struct { // @Success 200 {object} domain.SourcesResponse "ok" // @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" func (s *Handler) listSources(c echo.Context) error { - resp := domain.SourcesResponse { + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, }, @@ -64,8 +53,8 @@ func (s *Handler) listSources(c echo.Context) error { // @Tags Source // @Router /v1/sources/by/source [get] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) listSourcesBySource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -102,8 +91,8 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { // @Tags Source // @Router /v1/sources/{id} [get] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) getSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -137,8 +126,8 @@ func (s *Handler) getSource(c echo.Context) error { // @Tags Source // @Router /v1/sources/by/sourceAndName [get] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -172,8 +161,8 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { // @Tags Source // @Router /v1/sources/new/reddit [post] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) newRedditSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -339,8 +328,8 @@ func (s *Handler) newTwitchSource(c echo.Context) error { // @Tags Source // @Router /v1/sources/new/rss [post] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) newRssSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -434,10 +423,10 @@ func (s *Handler) deleteSources(c echo.Context) error { // @Tags Source // @Router /v1/sources/{id}/disable [post] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) disableSource(c echo.Context) error { - resp := domain.SourcesResponse { + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, }, @@ -476,10 +465,10 @@ func (s *Handler) disableSource(c echo.Context) error { // @Tags Source // @Router /v1/sources/{id}/enable [post] // @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) enableSource(c echo.Context) error { - resp := domain.SourcesResponse { + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, }, diff --git a/internal/repository/discordWebHooks.go b/internal/repository/discordWebHooks.go index e549897..b87ef86 100644 --- a/internal/repository/discordWebHooks.go +++ b/internal/repository/discordWebHooks.go @@ -9,8 +9,8 @@ import ( "github.com/huandu/go-sqlbuilder" ) -type DiscordWebHookRepo interface{ - Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) +type DiscordWebHookRepo interface { + Create(ctx context.Context, userId int64, url, server, channel string, enabled bool) (int64, error) Enable(ctx context.Context, id int64) (int64, error) Disable(ctx context.Context, id int64) (int64, error) SoftDelete(ctx context.Context, id int64) (int64, error) @@ -32,12 +32,12 @@ func NewDiscordWebHookRepository(conn *sql.DB) discordWebHookRepository { } } -func (r discordWebHookRepository) Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) { +func (r discordWebHookRepository) Create(ctx context.Context, userId int64, url, server, channel string, enabled bool) (int64, error) { dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("DiscordWebHooks") - queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "Url", "Server", "Channel", "Enabled") - queryBuilder.Values(dt, dt, timeZero, url, server, channel, enabled) + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "Url", "Server", "Channel", "Enabled") + queryBuilder.Values(dt, dt, timeZero, userId, url, server, channel, enabled) query, args := queryBuilder.Build() _, err := r.conn.ExecContext(ctx, query, args...) @@ -195,13 +195,14 @@ func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordW var createdAt time.Time var updatedAt time.Time var deletedAt time.Time + var userId int64 var url string var server string var channel string var enabled bool err := rows.Scan( &id, &createdAt, &updatedAt, - &deletedAt, &url, &server, + &deletedAt, &userId, &url, &server, &channel, &enabled, ) if err != nil { @@ -213,6 +214,7 @@ func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordW CreatedAt: createdAt, UpdatedAt: updatedAt, DeletedAt: deletedAt, + UserID: userId, Url: url, Server: server, Channel: channel, diff --git a/internal/repository/discordWebHooks_test.go b/internal/repository/discordWebHooks_test.go index 122299d..b79dd63 100644 --- a/internal/repository/discordWebHooks_test.go +++ b/internal/repository/discordWebHooks_test.go @@ -17,7 +17,7 @@ func TestCreateDiscordWebHookRecord(t *testing.T) { defer db.Close() r := repository.NewDiscordWebHookRepository(db) - created, err := r.Create(context.Background(), "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + created, err := r.Create(context.Background(), 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) if err != nil { t.Log(err) t.FailNow() @@ -38,7 +38,7 @@ func TestDiscordWebHookGetById(t *testing.T) { defer db.Close() ctx := context.Background() r := repository.NewDiscordWebHookRepository(db) - created, err := r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + created, err := r.Create(ctx, 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) if err != nil { t.Log(err) t.FailNow() @@ -71,7 +71,7 @@ func TestDiscordWebHookGetByUrl(t *testing.T) { ctx := context.Background() r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) item, err := r.GetByUrl(ctx, "www.discord.com/bad/webhook") if err != nil { t.Log(err) @@ -95,7 +95,7 @@ func TestDiscordWebHookListByServerName(t *testing.T) { ctx := context.Background() serverName := "Unit Testing" r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, "memes", true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, "memes", true) item, err := r.ListByServerName(ctx, serverName) if err != nil { @@ -121,7 +121,7 @@ func TestDiscordWebHookListByServerAndChannel(t *testing.T) { serverName := "Unit Testing" channel := "memes" r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true) item, err := r.ListByServerAndChannel(ctx, serverName, channel) if err != nil { @@ -152,7 +152,7 @@ func TestDiscordWebHookEnableRecord(t *testing.T) { serverName := "Unit Testing" channel := "memes" r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, false) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, false) item, err := r.GetById(ctx, 1) if err != nil { @@ -195,7 +195,7 @@ func TestDiscordWebHookDisableRecord(t *testing.T) { serverName := "Unit Testing" channel := "memes" r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true) item, err := r.GetById(ctx, 1) if err != nil { @@ -238,7 +238,7 @@ func TestDiscordWebHookSoftDelete(t *testing.T) { serverName := "Unit Testing" channel := "memes" r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true) _, err = r.SoftDelete(ctx, 1) if err != nil { t.Log(err) @@ -263,7 +263,7 @@ func TestDiscordWebHookRestore(t *testing.T) { timeZero := time.Time{} r := repository.NewDiscordWebHookRepository(db) - _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true) item, _ := r.GetById(ctx, 1) if item.DeletedAt != timeZero { t.Log("DeletedAt was not zero") -- 2.45.2 From c76522793292a7bcd2733a9c1643bcc41c53b603 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 5 May 2024 10:02:17 -0700 Subject: [PATCH 05/10] Getting the user and jwt stuff added to the api. Now to get swagger working --- Dockerfile | 15 +- cmd/server.go | 57 +- docs/docs.go | 490 +++--------------- docs/swagger.json | 490 +++--------------- docs/swagger.yaml | 301 ++--------- .../migrations/20240425083756_init.sql | 36 +- internal/domain/interfaces/source.go | 22 - internal/domain/models/dto.go | 129 ----- internal/domain/requests.go | 9 + internal/domain/responses.go | 7 + internal/handler/v1/articles.go | 70 +-- internal/handler/v1/auth.go | 207 ++++++++ internal/handler/v1/discordwebhooks.go | 2 +- internal/handler/v1/handler.go | 35 +- internal/handler/v1/jwt.go | 17 + internal/handler/v1/queue.go | 39 -- internal/handler/v1/sources.go | 163 +++--- internal/handler/v1/subscriptions.go | 225 -------- internal/repository/refreshTokens.go | 23 +- internal/repository/refreshTokens_test.go | 13 +- internal/repository/users.go | 39 +- internal/repository/users_test.go | 9 +- internal/respositoryServices/refreshTokens.go | 87 ++++ internal/respositoryServices/userService.go | 171 ++++++ internal/services/config.go | 3 +- internal/services/database.go | 9 +- 26 files changed, 945 insertions(+), 1723 deletions(-) delete mode 100644 internal/domain/interfaces/source.go delete mode 100644 internal/domain/models/dto.go create mode 100644 internal/handler/v1/auth.go delete mode 100644 internal/handler/v1/queue.go delete mode 100644 internal/handler/v1/subscriptions.go create mode 100644 internal/respositoryServices/refreshTokens.go create mode 100644 internal/respositoryServices/userService.go diff --git a/Dockerfile b/Dockerfile index 1d7645e..2be6722 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,10 @@ WORKDIR /app # Always make sure that swagger docs are updated RUN go install github.com/swaggo/swag/cmd/swag@latest -RUN /go/bin/swag i +RUN /go/bin/swag init -g cmd/server.go -# Always build the latest sql queries -RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest -RUN /go/bin/sqlc generate - -RUN go build . -RUN go install github.com/pressly/goose/v3/cmd/goose@latest +#RUN go build . +#RUN go install github.com/pressly/goose/v3/cmd/goose@latest FROM alpine:latest as app @@ -21,8 +17,7 @@ RUN apk --no-cache add libc6-compat RUN apk --no-cache add chromium RUN mkdir /app && mkdir /app/migrations -COPY --from=build /app/collector /app -COPY --from=build /go/bin/goose /app -COPY ./database/migrations/ /app/migrations +COPY --from=build /app/server /app +COPY ./internal/database/migrations/ /app/migrations CMD [ "/app/collector" ] \ No newline at end of file diff --git a/cmd/server.go b/cmd/server.go index 48f4826..dad9a4b 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -3,7 +3,9 @@ package main import ( "context" "database/sql" + "errors" "fmt" + "os" _ "github.com/glebarez/go-sqlite" "github.com/pressly/goose/v3" @@ -14,9 +16,13 @@ import ( "git.jamestombleson.com/jtom38/newsbot-api/internal/services/cron" ) -// @title NewsBot collector -// @version 0.1 -// @BasePath /api +// @title NewsBot collector +// @version 0.1 +// @BasePath /api +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and JWT token. func main() { ctx := context.Background() @@ -30,14 +36,9 @@ func main() { panic(err) } - err = goose.SetDialect("sqlite3") + err = migrateDatabase(db) if err != nil { - panic(err) - } - - err = goose.Up(db, "../internal/database/migrations") - if err != nil { - panic(err) + fmt.Print(err) } c := cron.NewScheduler(ctx, db) @@ -51,3 +52,39 @@ func main() { server.Router.Start(":8081") } + +func migrateDatabase(db *sql.DB) error { + err := goose.SetDialect("sqlite3") + if err != nil { + panic(err) + } + + err = goose.Up(db, "../internal/database/migrations") + if err != nil { + panic(err) + } + + _, err = os.Stat("./migrations") + if err == nil { + + err = goose.Up(db, "../internal/database/migrations") + if err != nil { + panic(err) + } + + return nil + } + + _, err = os.Stat("../internal/database/migrations") + if err == nil { + + err = goose.Up(db, "../internal/database/migrations") + if err != nil { + panic(err) + } + + return nil + } + + return errors.New("failed to find the migration files") +} diff --git a/docs/docs.go b/docs/docs.go index 4dfaa2c..463c253 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -18,6 +18,11 @@ const docTemplate = `{ "paths": { "/v1/articles": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -57,6 +62,11 @@ const docTemplate = `{ }, "/v1/articles/by/sourceid": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -103,6 +113,11 @@ const docTemplate = `{ }, "/v1/articles/{ID}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -143,6 +158,11 @@ const docTemplate = `{ }, "/v1/articles/{ID}/details": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -442,48 +462,13 @@ const docTemplate = `{ } } }, - "/v1/queue/discord/webhooks": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Queue" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListDiscordWebHooksQueueResults" - } - } - } - } - }, - "/v1/settings/{key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Settings" - ], - "summary": "Returns a object based on the Key that was given.", - "parameters": [ - { - "type": "string", - "description": "Settings Key value", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, "/v1/sources": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -517,6 +502,11 @@ const docTemplate = `{ }, "/v1/sources/by/source": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -563,6 +553,11 @@ const docTemplate = `{ }, "/v1/sources/by/sourceAndName": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -610,6 +605,11 @@ const docTemplate = `{ }, "/v1/sources/new/reddit": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -654,6 +654,11 @@ const docTemplate = `{ }, "/v1/sources/new/rss": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -698,6 +703,11 @@ const docTemplate = `{ }, "/v1/sources/new/twitch": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -716,6 +726,11 @@ const docTemplate = `{ }, "/v1/sources/new/youtube": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -741,6 +756,11 @@ const docTemplate = `{ }, "/v1/sources/{id}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -779,6 +799,11 @@ const docTemplate = `{ } }, "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -797,6 +822,11 @@ const docTemplate = `{ }, "/v1/sources/{id}/disable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -834,6 +864,11 @@ const docTemplate = `{ }, "/v1/sources/{id}/enable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -868,167 +903,6 @@ const docTemplate = `{ } } } - }, - "/v1/subscriptions": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - }, - "400": { - "description": "Unable to reach SQL.", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - }, - "500": { - "description": "Failed to process data from SQL.", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - } - } - } - }, - "/v1/subscriptions/by/SourceId": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - } - } - } - }, - "/v1/subscriptions/by/discordId": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - }, - "400": { - "description": "Unable to reach SQL or Data problems", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - }, - "500": { - "description": "Data problems", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - } - } - } - }, - "/v1/subscriptions/details": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 50 entries with full deatils on the source and output.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptionDetails" - } - } - } - } - }, - "/v1/subscriptions/discord/webhook/delete": { - "delete": { - "tags": [ - "Subscription" - ], - "summary": "Removes a Discord WebHook Subscription based on the Subscription ID.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": {} - } - }, - "/v1/subscriptions/discord/webhook/new": { - "post": { - "tags": [ - "Subscription" - ], - "summary": "Creates a new subscription to link a post from a Source to a DiscordWebHook.", - "parameters": [ - { - "type": "string", - "description": "discordWebHookId", - "name": "discordWebHookId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "sourceId", - "name": "sourceId", - "in": "query", - "required": true - } - ], - "responses": {} - } } }, "definitions": { @@ -1185,212 +1059,14 @@ const docTemplate = `{ } } } - }, - "models.ArticleDetailsDto": { - "type": "object", - "properties": { - "authorImage": { - "type": "string" - }, - "authorName": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "pubdate": { - "type": "string" - }, - "source": { - "$ref": "#/definitions/models.SourceDto" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - }, - "url": { - "type": "string" - }, - "video": { - "type": "string" - }, - "videoHeight": { - "type": "integer" - }, - "videoWidth": { - "type": "integer" - } - } - }, - "models.DiscordQueueDetailsDto": { - "type": "object", - "properties": { - "article": { - "$ref": "#/definitions/models.ArticleDetailsDto" - }, - "id": { - "type": "string" - } - } - }, - "models.DiscordWebHooksDto": { - "type": "object", - "properties": { - "ID": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "server": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "models.SourceDto": { - "type": "object", - "properties": { - "deleted": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "models.SubscriptionDetailsDto": { - "type": "object", - "properties": { - "discordwebhook": { - "$ref": "#/definitions/models.DiscordWebHooksDto" - }, - "id": { - "type": "string" - }, - "source": { - "$ref": "#/definitions/models.SourceDto" - } - } - }, - "models.SubscriptionDto": { - "type": "object", - "properties": { - "discordwebhookid": { - "type": "string" - }, - "id": { - "type": "string" - }, - "sourceid": { - "type": "string" - } - } - }, - "v1.ApiError": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListDiscordWebHooksQueueResults": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.DiscordQueueDetailsDto" - } - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListSubscriptionDetails": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.SubscriptionDetailsDto" - } - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListSubscriptions": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.SubscriptionDto" - } - }, - "status": { - "type": "integer" - } - } + } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" } } }` diff --git a/docs/swagger.json b/docs/swagger.json index 1457fe6..e9cb4ca 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -9,6 +9,11 @@ "paths": { "/v1/articles": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -48,6 +53,11 @@ }, "/v1/articles/by/sourceid": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -94,6 +104,11 @@ }, "/v1/articles/{ID}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -134,6 +149,11 @@ }, "/v1/articles/{ID}/details": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -433,48 +453,13 @@ } } }, - "/v1/queue/discord/webhooks": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Queue" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListDiscordWebHooksQueueResults" - } - } - } - } - }, - "/v1/settings/{key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Settings" - ], - "summary": "Returns a object based on the Key that was given.", - "parameters": [ - { - "type": "string", - "description": "Settings Key value", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, "/v1/sources": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -508,6 +493,11 @@ }, "/v1/sources/by/source": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -554,6 +544,11 @@ }, "/v1/sources/by/sourceAndName": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -601,6 +596,11 @@ }, "/v1/sources/new/reddit": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -645,6 +645,11 @@ }, "/v1/sources/new/rss": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -689,6 +694,11 @@ }, "/v1/sources/new/twitch": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -707,6 +717,11 @@ }, "/v1/sources/new/youtube": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -732,6 +747,11 @@ }, "/v1/sources/{id}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -770,6 +790,11 @@ } }, "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -788,6 +813,11 @@ }, "/v1/sources/{id}/disable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -825,6 +855,11 @@ }, "/v1/sources/{id}/enable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "Source" ], @@ -859,167 +894,6 @@ } } } - }, - "/v1/subscriptions": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - }, - "400": { - "description": "Unable to reach SQL.", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - }, - "500": { - "description": "Failed to process data from SQL.", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - } - } - } - }, - "/v1/subscriptions/by/SourceId": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - } - } - } - }, - "/v1/subscriptions/by/discordId": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 100 entries from the queue to be processed.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptions" - } - }, - "400": { - "description": "Unable to reach SQL or Data problems", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - }, - "500": { - "description": "Data problems", - "schema": { - "$ref": "#/definitions/v1.ApiError" - } - } - } - } - }, - "/v1/subscriptions/details": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Subscription" - ], - "summary": "Returns the top 50 entries with full deatils on the source and output.", - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/v1.ListSubscriptionDetails" - } - } - } - } - }, - "/v1/subscriptions/discord/webhook/delete": { - "delete": { - "tags": [ - "Subscription" - ], - "summary": "Removes a Discord WebHook Subscription based on the Subscription ID.", - "parameters": [ - { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true - } - ], - "responses": {} - } - }, - "/v1/subscriptions/discord/webhook/new": { - "post": { - "tags": [ - "Subscription" - ], - "summary": "Creates a new subscription to link a post from a Source to a DiscordWebHook.", - "parameters": [ - { - "type": "string", - "description": "discordWebHookId", - "name": "discordWebHookId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "sourceId", - "name": "sourceId", - "in": "query", - "required": true - } - ], - "responses": {} - } } }, "definitions": { @@ -1176,212 +1050,14 @@ } } } - }, - "models.ArticleDetailsDto": { - "type": "object", - "properties": { - "authorImage": { - "type": "string" - }, - "authorName": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "pubdate": { - "type": "string" - }, - "source": { - "$ref": "#/definitions/models.SourceDto" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - }, - "url": { - "type": "string" - }, - "video": { - "type": "string" - }, - "videoHeight": { - "type": "integer" - }, - "videoWidth": { - "type": "integer" - } - } - }, - "models.DiscordQueueDetailsDto": { - "type": "object", - "properties": { - "article": { - "$ref": "#/definitions/models.ArticleDetailsDto" - }, - "id": { - "type": "string" - } - } - }, - "models.DiscordWebHooksDto": { - "type": "object", - "properties": { - "ID": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "server": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "models.SourceDto": { - "type": "object", - "properties": { - "deleted": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "models.SubscriptionDetailsDto": { - "type": "object", - "properties": { - "discordwebhook": { - "$ref": "#/definitions/models.DiscordWebHooksDto" - }, - "id": { - "type": "string" - }, - "source": { - "$ref": "#/definitions/models.SourceDto" - } - } - }, - "models.SubscriptionDto": { - "type": "object", - "properties": { - "discordwebhookid": { - "type": "string" - }, - "id": { - "type": "string" - }, - "sourceid": { - "type": "string" - } - } - }, - "v1.ApiError": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListDiscordWebHooksQueueResults": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.DiscordQueueDetailsDto" - } - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListSubscriptionDetails": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.SubscriptionDetailsDto" - } - }, - "status": { - "type": "integer" - } - } - }, - "v1.ListSubscriptions": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "payload": { - "type": "array", - "items": { - "$ref": "#/definitions/models.SubscriptionDto" - } - }, - "status": { - "type": "integer" - } - } + } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a77fcff..ad8429d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -102,140 +102,6 @@ definitions: $ref: '#/definitions/domain.SourceDto' type: array type: object - models.ArticleDetailsDto: - properties: - authorImage: - type: string - authorName: - type: string - description: - type: string - id: - type: string - pubdate: - type: string - source: - $ref: '#/definitions/models.SourceDto' - tags: - items: - type: string - type: array - thumbnail: - type: string - title: - type: string - url: - type: string - video: - type: string - videoHeight: - type: integer - videoWidth: - type: integer - type: object - models.DiscordQueueDetailsDto: - properties: - article: - $ref: '#/definitions/models.ArticleDetailsDto' - id: - type: string - type: object - models.DiscordWebHooksDto: - properties: - ID: - type: string - channel: - type: string - enabled: - type: boolean - server: - type: string - url: - type: string - type: object - models.SourceDto: - properties: - deleted: - type: boolean - enabled: - type: boolean - id: - type: string - name: - type: string - site: - type: string - source: - type: string - tags: - items: - type: string - type: array - type: - type: string - url: - type: string - value: - type: string - type: object - models.SubscriptionDetailsDto: - properties: - discordwebhook: - $ref: '#/definitions/models.DiscordWebHooksDto' - id: - type: string - source: - $ref: '#/definitions/models.SourceDto' - type: object - models.SubscriptionDto: - properties: - discordwebhookid: - type: string - id: - type: string - sourceid: - type: string - type: object - v1.ApiError: - properties: - message: - type: string - status: - type: integer - type: object - v1.ListDiscordWebHooksQueueResults: - properties: - message: - type: string - payload: - items: - $ref: '#/definitions/models.DiscordQueueDetailsDto' - type: array - status: - type: integer - type: object - v1.ListSubscriptionDetails: - properties: - message: - type: string - payload: - items: - $ref: '#/definitions/models.SubscriptionDetailsDto' - type: array - status: - type: integer - type: object - v1.ListSubscriptions: - properties: - message: - type: string - payload: - items: - $ref: '#/definitions/models.SubscriptionDto' - type: array - status: - type: integer - type: object info: contact: {} title: NewsBot collector @@ -263,6 +129,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Lists the top 25 records ordering from newest to oldest. tags: - Articles @@ -289,6 +157,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns an article based on defined ID. tags: - Articles @@ -315,6 +185,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns an article and source based on defined ID. tags: - Articles @@ -345,6 +217,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Finds the articles based on the SourceID provided. Returns the top 25. tags: @@ -520,32 +394,6 @@ paths: summary: Creates a new record for a discord web hook to post data to. tags: - DiscordWebhook - /v1/queue/discord/webhooks: - get: - produces: - - application/json - responses: - "200": - description: ok - schema: - $ref: '#/definitions/v1.ListDiscordWebHooksQueueResults' - summary: Returns the top 100 entries from the queue to be processed. - tags: - - Queue - /v1/settings/{key}: - get: - parameters: - - description: Settings Key value - in: path - name: key - required: true - type: string - produces: - - application/json - responses: {} - summary: Returns a object based on the Key that was given. - tags: - - Settings /v1/sources: get: parameters: @@ -564,6 +412,8 @@ paths: description: Unable to reach SQL or Data problems schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Lists the top 50 records tags: - Source @@ -590,6 +440,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns a single entity by ID tags: - Source @@ -601,6 +453,8 @@ paths: required: true type: string responses: {} + security: + - Bearer: [] summary: Marks a source as deleted based on its ID value. tags: - Source @@ -625,6 +479,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Disables a source from processing. tags: - Source @@ -649,6 +505,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Enables a source to continue processing. tags: - Source @@ -679,6 +537,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: 'Lists the top 50 records based on the name given. Example: reddit' tags: - Source @@ -710,6 +570,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns a single entity by ID tags: - Source @@ -739,6 +601,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Creates a new reddit source to monitor. tags: - Source @@ -768,6 +632,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Creates a new rss source to monitor. tags: - Source @@ -780,6 +646,8 @@ paths: required: true type: string responses: {} + security: + - Bearer: [] summary: Creates a new twitch source to monitor. tags: - Source @@ -797,112 +665,15 @@ paths: required: true type: string responses: {} + security: + - Bearer: [] summary: Creates a new youtube source to monitor. tags: - Source - /v1/subscriptions: - get: - produces: - - application/json - responses: - "200": - description: ok - schema: - $ref: '#/definitions/v1.ListSubscriptions' - "400": - description: Unable to reach SQL. - schema: - $ref: '#/definitions/v1.ApiError' - "500": - description: Failed to process data from SQL. - schema: - $ref: '#/definitions/v1.ApiError' - summary: Returns the top 100 entries from the queue to be processed. - tags: - - Subscription - /v1/subscriptions/by/SourceId: - get: - parameters: - - description: id - in: query - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: ok - schema: - $ref: '#/definitions/v1.ListSubscriptions' - summary: Returns the top 100 entries from the queue to be processed. - tags: - - Subscription - /v1/subscriptions/by/discordId: - get: - parameters: - - description: id - in: query - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: ok - schema: - $ref: '#/definitions/v1.ListSubscriptions' - "400": - description: Unable to reach SQL or Data problems - schema: - $ref: '#/definitions/v1.ApiError' - "500": - description: Data problems - schema: - $ref: '#/definitions/v1.ApiError' - summary: Returns the top 100 entries from the queue to be processed. - tags: - - Subscription - /v1/subscriptions/details: - get: - produces: - - application/json - responses: - "200": - description: ok - schema: - $ref: '#/definitions/v1.ListSubscriptionDetails' - summary: Returns the top 50 entries with full deatils on the source and output. - tags: - - Subscription - /v1/subscriptions/discord/webhook/delete: - delete: - parameters: - - description: id - in: query - name: id - required: true - type: string - responses: {} - summary: Removes a Discord WebHook Subscription based on the Subscription ID. - tags: - - Subscription - /v1/subscriptions/discord/webhook/new: - post: - parameters: - - description: discordWebHookId - in: query - name: discordWebHookId - required: true - type: string - - description: sourceId - in: query - name: sourceId - required: true - type: string - responses: {} - summary: Creates a new subscription to link a post from a Source to a DiscordWebHook. - tags: - - Subscription +securityDefinitions: + Bearer: + description: Type "Bearer" followed by a space and JWT token. + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/internal/database/migrations/20240425083756_init.sql b/internal/database/migrations/20240425083756_init.sql index 8ab36dd..fa53a97 100644 --- a/internal/database/migrations/20240425083756_init.sql +++ b/internal/database/migrations/20240425083756_init.sql @@ -18,23 +18,12 @@ CREATE TABLE Articles ( AuthorImageUrl TEXT NOT NULL ); -CREATE Table DiscordQueue ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - CreatedAt DATETIME NOT NULL, - UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, - ArticleId NUMBER NOT NULL, - SourceId NUMBER NOT NULL -); - CREATE Table DiscordWebHooks ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, DeletedAt DATETIME NOT NULL, UserID INTEGER NOT NULL, - --Name TEXT NOT NULL, -- Defines webhook purpose - --Key TEXT, Url TEXT NOT NULL, -- Webhook Url Server TEXT NOT NULL, -- Defines the server its bound it. Used for reference Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for reference @@ -72,18 +61,6 @@ CREATE Table Sources ( Tags TEXT NOT NULL ); -/* -CREATE TABLE Subscriptions ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - CreatedAt DATETIME NOT NULL, - UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME NOT NULL, - DiscordWebHookID NUMBER NOT NULL, - SourceID NUMBER NOT NULL, - UserID NUMBER NOT NULL -); -*/ - CREATE TABLE UserSourceSubscriptions ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, @@ -107,7 +84,7 @@ CREATE TABLE Users ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, Name TEXT NOT NULL, Hash TEXT NOT NULL, Scopes TEXT NOT NULL @@ -117,7 +94,7 @@ CREATE TABLE RefreshTokens ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, Username TEXT NOT NULL, Token TEXT NOT NULL ); @@ -127,13 +104,12 @@ CREATE TABLE RefreshTokens ( -- +goose Down -- +goose StatementBegin +DROP TABLE AlertDiscord; Drop Table Articles; -Drop Table DiscordQueue; Drop Table DiscordWebHooks; Drop Table Icons; -Drop Table Settings; -Drop Table Sources; -DROP TABLE Subscriptions; -DROP TABLE Users; DROP TABLE RefreshTokens; +Drop Table Sources; +DROP TABLE Users; +DROP TABLE UserSourceSubscriptions; -- +goose StatementEnd diff --git a/internal/domain/interfaces/source.go b/internal/domain/interfaces/source.go deleted file mode 100644 index 59bcc64..0000000 --- a/internal/domain/interfaces/source.go +++ /dev/null @@ -1,22 +0,0 @@ -package interfaces - -import ( - "github.com/go-rod/rod" - "github.com/mmcdole/gofeed" -) - -type Sources interface { - CheckSource() error - PullFeed() (*gofeed.Feed, error) - - GetBrowser() *rod.Browser - GetPage(parser *rod.Browser, url string) *rod.Page - - ExtractThumbnail(page *rod.Page) (string, error) - ExtractPubDate(page *rod.Page) (string, error) - ExtractDescription(page *rod.Page) (string, error) - ExtractAuthor(page *rod.Page) (string, error) - ExtractAuthorImage(page *rod.Page) (string, error) - ExtractTags(page *rod.Page) (string, error) - ExtractTitle(page *rod.Page) (string, error) -} diff --git a/internal/domain/models/dto.go b/internal/domain/models/dto.go deleted file mode 100644 index 4fc48e5..0000000 --- a/internal/domain/models/dto.go +++ /dev/null @@ -1,129 +0,0 @@ -package models - -import ( - "strings" - "time" - - "github.com/google/uuid" - - "git.jamestombleson.com/jtom38/newsbot-api/internal/database" -) - -type ArticleDto struct { - ID uuid.UUID `json:"id"` - Source uuid.UUID `json:"sourceid"` - Tags []string `json:"tags"` - Title string `json:"title"` - Url string `json:"url"` - Pubdate time.Time `json:"pubdate"` - Video string `json:"video"` - Videoheight int32 `json:"videoHeight"` - Videowidth int32 `json:"videoWidth"` - Thumbnail string `json:"thumbnail"` - Description string `json:"description"` - Authorname string `json:"authorName"` - Authorimage string `json:"authorImage"` -} - -type ArticleDetailsDto struct { - ID uuid.UUID `json:"id"` - Source SourceDto `json:"source"` - Tags []string `json:"tags"` - Title string `json:"title"` - Url string `json:"url"` - Pubdate time.Time `json:"pubdate"` - Video string `json:"video"` - Videoheight int32 `json:"videoHeight"` - Videowidth int32 `json:"videoWidth"` - Thumbnail string `json:"thumbnail"` - Description string `json:"description"` - Authorname string `json:"authorName"` - Authorimage string `json:"authorImage"` -} - -type DiscordWebHooksDto struct { - ID uuid.UUID `json:"ID"` - Url string `json:"url"` - Server string `json:"server"` - Channel string `json:"channel"` - Enabled bool `json:"enabled"` -} - -func ConvertToDiscordWebhookDto(i database.Discordwebhook) DiscordWebHooksDto { - return DiscordWebHooksDto{ - ID: i.ID, - Url: i.Url, - Server: i.Server, - Channel: i.Channel, - Enabled: i.Enabled, - } -} - -type SourceDto struct { - ID uuid.UUID `json:"id"` - Site string `json:"site"` - Name string `json:"name"` - Source string `json:"source"` - Type string `json:"type"` - Value string `json:"value"` - Enabled bool `json:"enabled"` - Url string `json:"url"` - Tags []string `json:"tags"` - Deleted bool `json:"deleted"` -} - -func ConvertToSourceDto(i database.Source) SourceDto { - var deleted bool - if !i.Deleted.Valid { - deleted = true - } - - return SourceDto{ - ID: i.ID, - Site: i.Site, - Name: i.Name, - Source: i.Source, - Type: i.Type, - Value: i.Value.String, - Enabled: i.Enabled, - Url: i.Url, - Tags: splitTags(i.Tags), - Deleted: deleted, - } -} - -type DiscordQueueDto struct { - ID uuid.UUID `json:"id"` - Articleid uuid.UUID `json:"articleId"` -} - -type DiscordQueueDetailsDto struct { - ID uuid.UUID `json:"id"` - Article ArticleDetailsDto `json:"article"` -} - -type SubscriptionDto struct { - ID uuid.UUID `json:"id"` - DiscordWebhookId uuid.UUID `json:"discordwebhookid"` - SourceId uuid.UUID `json:"sourceid"` -} - -func ConvertToSubscriptionDto(i database.Subscription) SubscriptionDto { - c := SubscriptionDto{ - ID: i.ID, - DiscordWebhookId: i.Discordwebhookid, - SourceId: i.Sourceid, - } - return c -} - -type SubscriptionDetailsDto struct { - ID uuid.UUID `json:"id"` - Source SourceDto `json:"source"` - DiscordWebHook DiscordWebHooksDto `json:"discordwebhook"` -} - -func splitTags(t string) []string { - items := strings.Split(t, ", ") - return items -} diff --git a/internal/domain/requests.go b/internal/domain/requests.go index b7bd1ad..4be9e81 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -11,3 +11,12 @@ type NewSourceParamRequest struct { Tags string `query:"tags"` } +type RefreshTokenRequest struct { + Username string `json:"username"` + RefreshToken string `json:"refreshToken"` +} + +type UpdateScopesRequest struct { + Username string `json:"username"` + Scopes []string `json:"scopes" validate:"required"` +} \ No newline at end of file diff --git a/internal/domain/responses.go b/internal/domain/responses.go index f6ca7ef..1ee7a22 100644 --- a/internal/domain/responses.go +++ b/internal/domain/responses.go @@ -5,6 +5,13 @@ type BaseResponse struct { Message string `json:"message"` } +type LoginResponse struct { + BaseResponse + Token string `json:"token"` + Type string `json:"type"` + RefreshToken string `json:"refreshToken"` +} + type ArticleResponse struct { BaseResponse Payload []ArticleDto `json:"payload"` diff --git a/internal/handler/v1/articles.go b/internal/handler/v1/articles.go index 0febc5d..21ff796 100644 --- a/internal/handler/v1/articles.go +++ b/internal/handler/v1/articles.go @@ -10,14 +10,15 @@ import ( ) // ListArticles -// @Summary Lists the top 25 records ordering from newest to oldest. -// @Produce application/json -// @Param page query string false "page number" -// @Tags Articles -// @Router /v1/articles [get] -// @Success 200 {object} domain.ArticleResponse -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Lists the top 25 records ordering from newest to oldest. +// @Produce application/json +// @Param page query string false "page number" +// @Tags Articles +// @Router /v1/articles [get] +// @Success 200 {object} domain.ArticleResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) listArticles(c echo.Context) error { resp := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ @@ -40,14 +41,15 @@ func (s *Handler) listArticles(c echo.Context) error { } // GetArticle -// @Summary Returns an article based on defined ID. -// @Param ID path string true "int" -// @Produce application/json -// @Tags Articles -// @Router /v1/articles/{ID} [get] -// @Success 200 {object} domain.ArticleResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns an article based on defined ID. +// @Param ID path string true "int" +// @Produce application/json +// @Tags Articles +// @Router /v1/articles/{ID} [get] +// @Success 200 {object} domain.ArticleResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) getArticle(c echo.Context) error { p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ @@ -74,14 +76,15 @@ func (s *Handler) getArticle(c echo.Context) error { } // GetArticleDetails -// @Summary Returns an article and source based on defined ID. -// @Param ID path string true "int" -// @Produce application/json -// @Tags Articles -// @Router /v1/articles/{ID}/details [get] -// @Success 200 {object} domain.ArticleDetailedResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns an article and source based on defined ID. +// @Param ID path string true "int" +// @Produce application/json +// @Tags Articles +// @Router /v1/articles/{ID}/details [get] +// @Success 200 {object} domain.ArticleDetailedResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) getArticleDetails(c echo.Context) error { p := domain.ArticleDetailedResponse{ BaseResponse: domain.BaseResponse{ @@ -114,15 +117,16 @@ func (s *Handler) getArticleDetails(c echo.Context) error { } // ListArticlesBySourceID -// @Summary Finds the articles based on the SourceID provided. Returns the top 25. -// @Param id query string true "source id" -// @Param page query int false "Page to query" -// @Produce application/json -// @Tags Articles -// @Router /v1/articles/by/sourceid [get] -// @Success 200 {object} domain.ArticleResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Finds the articles based on the SourceID provided. Returns the top 25. +// @Param id query string true "source id" +// @Param page query int false "Page to query" +// @Produce application/json +// @Tags Articles +// @Router /v1/articles/by/sourceid [get] +// @Success 200 {object} domain.ArticleResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) ListArticlesBySourceId(c echo.Context) error { p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ diff --git a/internal/handler/v1/auth.go b/internal/handler/v1/auth.go new file mode 100644 index 0000000..7847f56 --- /dev/null +++ b/internal/handler/v1/auth.go @@ -0,0 +1,207 @@ +package v1 + +import ( + "net/http" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" + + "github.com/labstack/echo/v4" +) + +const ( + ErrUserNotFound = "requested user does not exist" + ErrUsernameAlreadyExists = "the requested username already exists" +) + +// Register +// @Summary Creates a new user +// @Tags Users +// @Success 200 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +func (h *Handler) AuthRegister(c echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + //username := c.QueryParam("username") + exists, err := h.repo.Users.GetUser(c.Request().Context(), username) + if err != nil { + // if we have an err, validate that if its not user not found. + // if the user is not found, we can use that name + if err.Error() != repository.ErrUserNotFound { + return h.WriteError(c, err, http.StatusBadRequest) + } + } + if exists.Username == username { + return h.InternalServerErrorResponse(c, ErrUsernameAlreadyExists) + } + + //password := c.QueryParam("password") + err = h.repo.Users.CheckPasswordForRequirements(password) + if err != nil { + return h.WriteError(c, err, http.StatusInternalServerError) + } + + _, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeRead) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusCreated, domain.BaseResponse{ + Message: "OK", + }) +} + +func (h *Handler) AuthLogin(c echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + // Check to see if they are trying to login with the admin token + if username == "" { + return h.validateAdminToken(c, password) + } + + // check if the user exists + err := h.repo.Users.DoesUserExist(c.Request().Context(), username) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + // make sure the hash matches + err = h.repo.Users.DoesPasswordMatchHash(c.Request().Context(), username, password) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + // TODO think about moving this down some? + expiresAt := time.Now().Add(time.Hour * 48) + + jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, expiresAt) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + refresh, err := h.repo.RefreshTokens.Create(c.Request().Context(), username) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, domain.LoginResponse{ + BaseResponse: domain.BaseResponse{ + Message: "OK", + }, + Token: jwt, + Type: "Bearer", + RefreshToken: refresh, + }) +} + +func (h *Handler) validateAdminToken(c echo.Context, password string) error { + // if the admin token is blank, then the admin wanted this disabled. + // this will fail right away and not progress. + if h.config.AdminSecret == "" { + return h.InternalServerErrorResponse(c, ErrUserNotFound) + } + + if h.config.AdminSecret != password { + return h.UnauthorizedResponse(c, ErrUserNotFound) + } + + token, err := h.generateJwt("admin", h.config.ServerAddress) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, token) +} + +// This will take collect some information about the requested refresh, validate and then return a new jwt token if approved. +func (h *Handler) RefreshJwtToken(c echo.Context) error { + // Check the context for the refresh token + var request domain.RefreshTokenRequest + err := (&echo.DefaultBinder{}).BindBody(c, &request) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + err = h.repo.RefreshTokens.IsRequestValid(c.Request().Context(), request.Username, request.RefreshToken) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, time.Now().Add(time.Hour*48)) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + newRefreshToken, err := h.repo.RefreshTokens.Create(c.Request().Context(), request.Username) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, domain.LoginResponse{ + BaseResponse: domain.BaseResponse{ + Message: "OK", + }, + Token: jwt, + Type: "Bearer", + RefreshToken: newRefreshToken, + }) +} + +func (h *Handler) AddScopes(c echo.Context) error { + token, err := h.getJwtToken(c) + if err != nil { + return h.UnauthorizedResponse(c, err.Error()) + } + + err = token.IsValid(domain.ScopeAll) + if err != nil { + return h.UnauthorizedResponse(c, err.Error()) + } + + request := domain.UpdateScopesRequest{} + err = (&echo.DefaultBinder{}).BindBody(c, &request) + if err != nil { + h.WriteError(c, err, http.StatusBadRequest) + } + + err = h.repo.Users.AddScopes(c.Request().Context(), request.Username, request.Scopes) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, domain.BaseResponse{ + Message: "OK", + }) +} + +func (h *Handler) RemoveScopes(c echo.Context) error { + token, err := h.getJwtToken(c) + if err != nil { + return h.WriteError(c, err, http.StatusUnauthorized) + } + + err = token.IsValid(domain.ScopeAll) + if err != nil { + return h.WriteError(c, err, http.StatusUnauthorized) + } + + request := domain.UpdateScopesRequest{} + err = (&echo.DefaultBinder{}).BindBody(c, &request) + if err != nil { + h.WriteError(c, err, http.StatusBadRequest) + + } + + err = h.repo.Users.RemoveScopes(c.Request().Context(), request.Username, request.Scopes) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + return c.JSON(http.StatusOK, domain.BaseResponse{ + Message: "OK", + }) +} diff --git a/internal/handler/v1/discordwebhooks.go b/internal/handler/v1/discordwebhooks.go index 275a256..65fd3ee 100644 --- a/internal/handler/v1/discordwebhooks.go +++ b/internal/handler/v1/discordwebhooks.go @@ -143,7 +143,7 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { }) } - user, err := s.repo.Users.GetByName(token.UserName) + user, err := s.repo.Users.GetUser(c.Request().Context(), token.UserName) if err != nil { s.WriteMessage(c, ErrUserUnknown, http.StatusBadRequest) } diff --git a/internal/handler/v1/handler.go b/internal/handler/v1/handler.go index f39ad3d..2bc9d9e 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -3,7 +3,7 @@ package v1 import ( "context" "database/sql" - "errors" + "net/http" "github.com/golang-jwt/jwt/v5" echojwt "github.com/labstack/echo-jwt/v4" @@ -100,15 +100,6 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han sources.POST("/:ID/disable", s.disableSource) sources.POST("/:ID/enable", s.enableSource) - subs := v1.Group("/subscriptions") - subs.Use(echojwt.WithConfig(jwtConfig)) - subs.GET("/", s.ListSubscriptions) - subs.GET("/details", s.ListSubscriptionDetails) - subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId) - subs.GET("/by/sourceId", s.GetSubscriptionsBySourceId) - subs.POST("/discord/webhook/new", s.newDiscordWebHookSubscription) - subs.DELETE("/discord/webhook/delete", s.DeleteDiscordWebHookSubscription) - s.Router = router return s } @@ -134,18 +125,14 @@ func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) e }) } -func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) { - // Make sure that the request came with a jwtToken - token, ok := c.Get("user").(*jwt.Token) - if !ok { - return JwtToken{}, errors.New(ErrJwtMissing) - } - - // Generate the claims from the token - claims, ok := token.Claims.(*JwtToken) - if !ok { - return JwtToken{}, errors.New(ErrJwtClaimsMissing) - } - - return *claims, nil +func (s *Handler) InternalServerErrorResponse(c echo.Context, msg string) error { + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ + Message: msg, + }) +} + +func (s *Handler) UnauthorizedResponse(c echo.Context, msg string) error { + return c.JSON(http.StatusUnauthorized, domain.BaseResponse{ + Message: msg, + }) } diff --git a/internal/handler/v1/jwt.go b/internal/handler/v1/jwt.go index b7f1222..92f77be 100644 --- a/internal/handler/v1/jwt.go +++ b/internal/handler/v1/jwt.go @@ -7,6 +7,7 @@ import ( "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" ) const ( @@ -97,3 +98,19 @@ func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Tim return tokenString, nil } + +func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) { + // Make sure that the request came with a jwtToken + token, ok := c.Get("user").(*jwt.Token) + if !ok { + return JwtToken{}, errors.New(ErrJwtMissing) + } + + // Generate the claims from the token + claims, ok := token.Claims.(*JwtToken) + if !ok { + return JwtToken{}, errors.New(ErrJwtClaimsMissing) + } + + return *claims, nil +} diff --git a/internal/handler/v1/queue.go b/internal/handler/v1/queue.go deleted file mode 100644 index 92878ae..0000000 --- a/internal/handler/v1/queue.go +++ /dev/null @@ -1,39 +0,0 @@ -package v1 - -import ( - "net/http" - - "git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" - "github.com/labstack/echo/v4" -) - -type ListDiscordWebHooksQueueResults struct { - ApiStatusModel - Payload []models.DiscordQueueDetailsDto `json:"payload"` -} - -// GetDiscordQueue -// @Summary Returns the top 100 entries from the queue to be processed. -// @Produce application/json -// @Tags Queue -// @Router /v1/queue/discord/webhooks [get] -// @Success 200 {object} ListDiscordWebHooksQueueResults "ok" -func (s *Handler) ListDiscordWebhookQueue(c echo.Context) error { - p := ListDiscordWebHooksQueueResults{ - ApiStatusModel: ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, - }, - } - - // Get the raw resp from sql - //res, err := s.dto.ListDiscordWebhookQueueDetails(c.Request().Context(), 50) - //if err != nil { - // return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - // Message: err.Error(), - // }) - //} - - //p.Payload = res - return c.JSON(http.StatusOK, p) -} diff --git a/internal/handler/v1/sources.go b/internal/handler/v1/sources.go index 496be4c..7379f2c 100644 --- a/internal/handler/v1/sources.go +++ b/internal/handler/v1/sources.go @@ -16,13 +16,14 @@ import ( ) // ListSources -// @Summary Lists the top 50 records -// @Param page query string false "page number" -// @Produce application/json -// @Tags Source -// @Router /v1/sources [get] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" +// @Summary Lists the top 50 records +// @Param page query string false "page number" +// @Produce application/json +// @Tags Source +// @Router /v1/sources [get] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" +// @Security Bearer func (s *Handler) listSources(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -46,15 +47,16 @@ func (s *Handler) listSources(c echo.Context) error { } // ListSourcesBySource -// @Summary Lists the top 50 records based on the name given. Example: reddit -// @Param source query string true "Source Name" -// @Param page query string false "page number" -// @Produce application/json -// @Tags Source -// @Router /v1/sources/by/source [get] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Lists the top 50 records based on the name given. Example: reddit +// @Param source query string true "Source Name" +// @Param page query string false "page number" +// @Produce application/json +// @Tags Source +// @Router /v1/sources/by/source [get] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) listSourcesBySource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -85,14 +87,15 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { } // GetSource -// @Summary Returns a single entity by ID -// @Param id path int true "uuid" -// @Produce application/json -// @Tags Source -// @Router /v1/sources/{id} [get] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns a single entity by ID +// @Param id path int true "uuid" +// @Produce application/json +// @Tags Source +// @Router /v1/sources/{id} [get] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) getSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -119,15 +122,16 @@ func (s *Handler) getSource(c echo.Context) error { } // GetSourceByNameAndSource -// @Summary Returns a single entity by ID -// @Param name query string true "dadjokes" -// @Param source query string true "reddit" -// @Produce application/json -// @Tags Source -// @Router /v1/sources/by/sourceAndName [get] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns a single entity by ID +// @Param name query string true "dadjokes" +// @Param source query string true "reddit" +// @Produce application/json +// @Tags Source +// @Router /v1/sources/by/sourceAndName [get] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -155,14 +159,15 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { } // NewRedditSource -// @Summary Creates a new reddit source to monitor. -// @Param name query string true "name" -// @Param url query string true "url" -// @Tags Source -// @Router /v1/sources/new/reddit [post] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Creates a new reddit source to monitor. +// @Param name query string true "name" +// @Param url query string true "url" +// @Tags Source +// @Router /v1/sources/new/reddit [post] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) newRedditSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -211,11 +216,12 @@ func (s *Handler) newRedditSource(c echo.Context) error { } // NewYoutubeSource -// @Summary Creates a new youtube source to monitor. -// @Param name query string true "name" -// @Param url query string true "url" -// @Tags Source -// @Router /v1/sources/new/youtube [post] +// @Summary Creates a new youtube source to monitor. +// @Param name query string true "name" +// @Param url query string true "url" +// @Tags Source +// @Router /v1/sources/new/youtube [post] +// @Security Bearer func (s *Handler) newYoutubeSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) @@ -275,10 +281,11 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { } // NewTwitchSource -// @Summary Creates a new twitch source to monitor. -// @Param name query string true "name" -// @Tags Source -// @Router /v1/sources/new/twitch [post] +// @Summary Creates a new twitch source to monitor. +// @Param name query string true "name" +// @Tags Source +// @Router /v1/sources/new/twitch [post] +// @Security Bearer func (s *Handler) newTwitchSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) @@ -322,14 +329,15 @@ func (s *Handler) newTwitchSource(c echo.Context) error { } // NewRssSource -// @Summary Creates a new rss source to monitor. -// @Param name query string true "Site Name" -// @Param url query string true "RSS Url" -// @Tags Source -// @Router /v1/sources/new/rss [post] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Creates a new rss source to monitor. +// @Param name query string true "Site Name" +// @Param url query string true "RSS Url" +// @Tags Source +// @Router /v1/sources/new/rss [post] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) newRssSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -373,10 +381,11 @@ func (s *Handler) newRssSource(c echo.Context) error { } // DeleteSource -// @Summary Marks a source as deleted based on its ID value. -// @Param id path string true "id" -// @Tags Source -// @Router /v1/sources/{id} [POST] +// @Summary Marks a source as deleted based on its ID value. +// @Param id path string true "id" +// @Tags Source +// @Router /v1/sources/{id} [POST] +// @Security Bearer func (s *Handler) deleteSources(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) @@ -418,13 +427,14 @@ func (s *Handler) deleteSources(c echo.Context) error { } // DisableSource -// @Summary Disables a source from processing. -// @Param id path int true "id" -// @Tags Source -// @Router /v1/sources/{id}/disable [post] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Disables a source from processing. +// @Param id path int true "id" +// @Tags Source +// @Router /v1/sources/{id}/disable [post] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) disableSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -460,13 +470,14 @@ func (s *Handler) disableSource(c echo.Context) error { } // EnableSource -// @Summary Enables a source to continue processing. -// @Param id path string true "id" -// @Tags Source -// @Router /v1/sources/{id}/enable [post] -// @Success 200 {object} domain.SourcesResponse "ok" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Enables a source to continue processing. +// @Param id path string true "id" +// @Tags Source +// @Router /v1/sources/{id}/enable [post] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) enableSource(c echo.Context) error { resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ diff --git a/internal/handler/v1/subscriptions.go b/internal/handler/v1/subscriptions.go deleted file mode 100644 index dcb7440..0000000 --- a/internal/handler/v1/subscriptions.go +++ /dev/null @@ -1,225 +0,0 @@ -package v1 - -import ( - "context" - "encoding/json" - "errors" - "net/http" - - "git.jamestombleson.com/jtom38/newsbot-api/internal/database" - "git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -type ListSubscriptions struct { - ApiStatusModel - Payload []models.SubscriptionDto `json:"payload"` -} - -type GetSubscription struct { - ApiStatusModel - Payload models.SubscriptionDto `json:"payload"` -} - -type ListSubscriptionDetails struct { - ApiStatusModel - Payload []models.SubscriptionDetailsDto `json:"payload"` -} - -// GetSubscriptions -// @Summary Returns the top 100 entries from the queue to be processed. -// @Produce application/json -// @Tags Subscription -// @Router /v1/subscriptions [get] -// @Success 200 {object} ListSubscriptions "ok" -// @Failure 400 {object} ApiError "Unable to reach SQL." -// @Failure 500 {object} ApiError "Failed to process data from SQL." -func (s *Handler) ListSubscriptions(c echo.Context) error { - payload := ListSubscriptions{ - ApiStatusModel: ApiStatusModel{ - StatusCode: http.StatusOK, - Message: "OK", - }, - } - - //res, err := s.dto.ListSubscriptions(c.Request().Context(), 50) - //if err != nil { - // return s.WriteError(c, err, http.StatusBadRequest) - //} - //payload.Payload = res - return c.JSON(http.StatusOK, payload) -} - -// ListSubscriptionDetails -// @Summary Returns the top 50 entries with full deatils on the source and output. -// @Produce application/json -// @Tags Subscription -// @Router /v1/subscriptions/details [get] -// @Success 200 {object} ListSubscriptionDetails "ok" -func (s *Handler) ListSubscriptionDetails(c echo.Context) error { - payload := ListSubscriptionDetails{ - ApiStatusModel: ApiStatusModel{ - StatusCode: http.StatusOK, - Message: "OK", - }, - } - - //res, err := s.dto.ListSubscriptionDetails(c.Request().Context(), 50) - //if err != nil { - // return s.WriteError(c, err, http.StatusInternalServerError) - //} - //payload.Payload = res - return c.JSON(http.StatusOK, payload) -} - -// GetSubscriptionsByDiscordId -// @Summary Returns the top 100 entries from the queue to be processed. -// @Produce application/json -// @Param id query string true "id" -// @Tags Subscription -// @Router /v1/subscriptions/by/discordId [get] -// @Success 200 {object} ListSubscriptions "ok" -// @Failure 400 {object} ApiError "Unable to reach SQL or Data problems" -// @Failure 500 {object} ApiError "Data problems" -func (s *Handler) GetSubscriptionsByDiscordId(c echo.Context) error { - p := ListSubscriptions{ - ApiStatusModel: ApiStatusModel{ - StatusCode: http.StatusOK, - Message: "OK", - }, - } - - id := c.QueryParam("id") - if id == "" { - return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest) - } - - //uuid, err := uuid.Parse(id) - //if err != nil { - // return s.WriteError(c, errors.New(ErrValueNotUuid), http.StatusBadRequest) - //} - - //res, err := s.dto.ListSubscriptionsByDiscordWebhookId(context.Background(), uuid) - //if err != nil { - // return s.WriteError(c, err, http.StatusNoContent) - //} - //p.Payload = res - return c.JSON(http.StatusOK, p) -} - -// GetSubscriptionsBySourceId -// @Summary Returns the top 100 entries from the queue to be processed. -// @Produce application/json -// @Param id query string true "id" -// @Tags Subscription -// @Router /v1/subscriptions/by/SourceId [get] -// @Success 200 {object} ListSubscriptions "ok" -func (s *Handler) GetSubscriptionsBySourceId(c echo.Context) error { - p := ListSubscriptions{ - ApiStatusModel: ApiStatusModel{ - StatusCode: http.StatusOK, - Message: "OK", - }, - } - - _id := c.QueryParam("id") - if _id == "" { - return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest) - } - - //uuid, err := uuid.Parse(_id) - //if err != nil { - // return s.WriteError(c, err, http.StatusBadRequest) - //} - - //res, err := s.dto.ListSubscriptionsBySourceId(context.Background(), uuid) - //if err != nil { - // return s.WriteError(c, err, http.StatusNoContent) - //} - //p.Payload = res - return c.JSON(http.StatusOK, p) -} - -// NewDiscordWebHookSubscription -// @Summary Creates a new subscription to link a post from a Source to a DiscordWebHook. -// @Param discordWebHookId query string true "discordWebHookId" -// @Param sourceId query string true "sourceId" -// @Tags Subscription -// @Router /v1/subscriptions/discord/webhook/new [post] -func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error { - // Extract the values given - discordWebHookId := c.QueryParam("discordWebHookId") - sourceId := c.QueryParam("sourceId") - - // Check to make we didn't get a null - if discordWebHookId == "" { - return s.WriteError(c, errors.New("invalid discordWebHooksId given"), http.StatusBadRequest) - } - if sourceId == "" { - return s.WriteError(c, errors.New("invalid sourceID given"), http.StatusBadRequest) - } - - // Validate they are UUID values - uHook, err := uuid.Parse(discordWebHookId) - if err != nil { - return s.WriteError(c, err, http.StatusBadRequest) - } - uSource, err := uuid.Parse(sourceId) - if err != nil { - return s.WriteError(c, err, http.StatusBadRequest) - } - - // Check if the sub already exists - _, err = s.Db.QuerySubscriptions(c.Request().Context(), database.QuerySubscriptionsParams{ - Discordwebhookid: uHook, - Sourceid: uSource, - }) - if err == nil { - return s.WriteError(c, errors.New("a subscription already exists between these two entities"), http.StatusBadRequest) - } - - // Does not exist, so make it. - params := database.CreateSubscriptionParams{ - ID: uuid.New(), - Discordwebhookid: uHook, - Sourceid: uSource, - } - err = s.Db.CreateSubscription(context.Background(), params) - if err != nil { - return s.WriteError(c, err, http.StatusInternalServerError) - } - - bJson, err := json.Marshal(¶ms) - if err != nil { - return s.WriteError(c, err, http.StatusInternalServerError) - } - - return c.JSON(http.StatusOK, bJson) -} - -// DeleteDiscordWebHookSubscription -// @Summary Removes a Discord WebHook Subscription based on the Subscription ID. -// @Param id query string true "id" -// @Tags Subscription -// @Router /v1/subscriptions/discord/webhook/delete [delete] -func (s *Handler) DeleteDiscordWebHookSubscription(c echo.Context) error { - var ErrMissingSubscriptionID string = "the request was missing a 'Id'" - - id := c.QueryParam("id") - if id == "" { - return s.WriteError(c, errors.New(ErrMissingSubscriptionID), http.StatusBadRequest) - } - - uid, err := uuid.Parse(id) - if err != nil { - return s.WriteError(c, err, http.StatusBadRequest) - } - - err = s.Db.DeleteSubscription(context.Background(), uid) - if err != nil { - return s.WriteError(c, err, http.StatusInternalServerError) - } - - return c.JSON(http.StatusOK, nil) -} diff --git a/internal/repository/refreshTokens.go b/internal/repository/refreshTokens.go index fc6ce83..ec7fc6f 100644 --- a/internal/repository/refreshTokens.go +++ b/internal/repository/refreshTokens.go @@ -1,6 +1,7 @@ package repository import ( + "context" "database/sql" "errors" "fmt" @@ -15,9 +16,9 @@ const ( ) type RefreshToken interface { - Create(username string, token string) (int64, error) - GetByUsername(name string) (domain.RefreshTokenEntity, error) - DeleteById(id int64) (int64, error) + Create(ctx context.Context, username string, token string) (int64, error) + GetByUsername(ctx context.Context, name string) (domain.RefreshTokenEntity, error) + DeleteById(ctx context.Context, id int64) (int64, error) } type RefreshTokenRepository struct { @@ -30,15 +31,15 @@ func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository { } } -func (rt RefreshTokenRepository) Create(username string, token string) (int64, error) { +func (rt RefreshTokenRepository) Create(ctx context.Context, username string, token string) (int64, error) { dt := time.Now() builder := sqlbuilder.NewInsertBuilder() builder.InsertInto(refreshTokenTableName) - builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt") - builder.Values(username, token, dt, dt) + builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt", "DeletedAt") + builder.Values(username, token, dt, dt, time.Time{}) query, args := builder.Build() - _, err := rt.connection.Exec(query, args...) + _, err := rt.connection.ExecContext(ctx, query, args...) if err != nil { return 0, err } @@ -46,14 +47,14 @@ func (rt RefreshTokenRepository) Create(username string, token string) (int64, e return 1, nil } -func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshTokenEntity, error) { +func (rt RefreshTokenRepository) GetByUsername(ctx context.Context, name string) (domain.RefreshTokenEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*").From(refreshTokenTableName).Where( builder.E("Username", name), ) query, args := builder.Build() - rows, err := rt.connection.Query(query, args...) + rows, err := rt.connection.QueryContext(ctx, query, args...) if err != nil { return domain.RefreshTokenEntity{}, err } @@ -66,7 +67,7 @@ func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshToken return data[0], nil } -func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) { +func (rt RefreshTokenRepository) DeleteById(ctx context.Context, id int64) (int64, error) { builder := sqlbuilder.NewDeleteBuilder() builder.DeleteFrom(refreshTokenTableName) builder.Where( @@ -74,7 +75,7 @@ func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) { ) query, args := builder.Build() - rows, err := rt.connection.Exec(query, args...) + rows, err := rt.connection.ExecContext(ctx, query, args...) if err != nil { return -1, err } diff --git a/internal/repository/refreshTokens_test.go b/internal/repository/refreshTokens_test.go index 2e44ef3..c182b63 100644 --- a/internal/repository/refreshTokens_test.go +++ b/internal/repository/refreshTokens_test.go @@ -1,6 +1,7 @@ package repository_test import ( + "context" "testing" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" @@ -14,7 +15,7 @@ func TestRefreshTokenCreate(t *testing.T) { } client := repository.NewRefreshTokenRepository(conn) - rows, err := client.Create("tester", "BadTokenDontUse") + rows, err := client.Create(context.Background(), "tester", "BadTokenDontUse") if err != nil { t.Log(err) t.FailNow() @@ -33,7 +34,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) { } client := repository.NewRefreshTokenRepository(conn) - rows, err := client.Create("tester", "BadTokenDoNotUse") + rows, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse") if err != nil { t.Log(err) t.FailNow() @@ -44,7 +45,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) { t.FailNow() } - model, err := client.GetByUsername("tester") + model, err := client.GetByUsername(context.Background(), "tester") if err != nil { t.Log(err) t.FailNow() @@ -64,7 +65,7 @@ func TestRefreshTokenDeleteById(t *testing.T) { } client := repository.NewRefreshTokenRepository(conn) - created, err := client.Create("tester", "BadTokenDoNotUse") + created, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse") if err != nil { t.Log(err) t.FailNow() @@ -73,13 +74,13 @@ func TestRefreshTokenDeleteById(t *testing.T) { t.Log("Unexpected number back for rows created") } - model, err := client.GetByUsername("tester") + model, err := client.GetByUsername(context.Background(), "tester") if err != nil { t.Log(err) t.FailNow() } - updated, err := client.DeleteById(model.ID) + updated, err := client.DeleteById(context.Background(), model.ID) if err != nil { t.Log(err) t.FailNow() diff --git a/internal/repository/users.go b/internal/repository/users.go index 209c6f5..c6a4d1e 100644 --- a/internal/repository/users.go +++ b/internal/repository/users.go @@ -1,6 +1,7 @@ package repository import ( + "context" "database/sql" "errors" "fmt" @@ -18,12 +19,12 @@ const ( ) type Users interface { - GetByName(name string) (domain.UserEntity, error) - Create(name, password, scope string) (int64, error) - Update(id int, entity domain.UserEntity) error - UpdatePassword(name, password string) error - CheckUserHash(name, password string) error - UpdateScopes(name, scope string) error + GetByName(ctx context.Context, name string) (domain.UserEntity, error) + Create(ctx context.Context, name, password, scope string) (int64, error) + Update(ctx context.Context, id int, entity domain.UserEntity) error + UpdatePassword(ctx context.Context, name, password string) error + CheckUserHash(ctx context.Context, name, password string) error + UpdateScopes(ctx context.Context, name, scope string) error } // Creates a new instance of UserRepository with the bound sql @@ -37,14 +38,14 @@ type userRepository struct { connection *sql.DB } -func (ur userRepository) GetByName(name string) (domain.UserEntity, error) { +func (ur userRepository) GetByName(ctx context.Context, name string) (domain.UserEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*").From("users").Where( builder.E("Name", name), ) query, args := builder.Build() - rows, err := ur.connection.Query(query, args...) + rows, err := ur.connection.QueryContext(ctx, query, args...) if err != nil { return domain.UserEntity{}, err } @@ -57,7 +58,7 @@ func (ur userRepository) GetByName(name string) (domain.UserEntity, error) { return data[0], nil } -func (ur userRepository) Create(name, password, scope string) (int64, error) { +func (ur userRepository) Create(ctx context.Context,name, password, scope string) (int64, error) { passwordBytes := []byte(password) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) if err != nil { @@ -67,11 +68,11 @@ func (ur userRepository) Create(name, password, scope string) (int64, error) { dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("users") - queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "Scopes") - queryBuilder.Values(name, string(hash), dt, dt, scope) + queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "DeletedAt", "Scopes") + queryBuilder.Values(name, string(hash), dt, dt, time.Time{}, scope) query, args := queryBuilder.Build() - _, err = ur.connection.Exec(query, args...) + _, err = ur.connection.ExecContext(ctx, query, args...) if err != nil { return 0, err } @@ -79,12 +80,12 @@ func (ur userRepository) Create(name, password, scope string) (int64, error) { return 1, nil } -func (ur userRepository) Update(id int, entity domain.UserEntity) error { +func (ur userRepository) Update(ctx context.Context, id int, entity domain.UserEntity) error { return errors.New("not implemented") } -func (ur userRepository) UpdatePassword(name, password string) error { - _, err := ur.GetByName(name) +func (ur userRepository) UpdatePassword(ctx context.Context, name, password string) error { + _, err := ur.GetByName(ctx, name) if err != nil { return nil } @@ -97,8 +98,8 @@ func (ur userRepository) UpdatePassword(name, password string) error { // If the hash matches what we have in the database, an error will not be returned. // If the user does not exist or the hash does not match, an error will be returned -func (ur userRepository) CheckUserHash(name, password string) error { - record, err := ur.GetByName(name) +func (ur userRepository) CheckUserHash(ctx context.Context,name, password string) error { + record, err := ur.GetByName(ctx, name) if err != nil { return err } @@ -111,7 +112,7 @@ func (ur userRepository) CheckUserHash(name, password string) error { return nil } -func (ur userRepository) UpdateScopes(name, scope string) error { +func (ur userRepository) UpdateScopes(ctx context.Context,name, scope string) error { builder := sqlbuilder.NewUpdateBuilder() builder.Update("users") builder.Set( @@ -122,7 +123,7 @@ func (ur userRepository) UpdateScopes(name, scope string) error { ) query, args := builder.Build() - _, err := ur.connection.Exec(query, args...) + _, err := ur.connection.ExecContext(ctx, query, args...) if err != nil { return err } diff --git a/internal/repository/users_test.go b/internal/repository/users_test.go index 849338e..84fe23a 100644 --- a/internal/repository/users_test.go +++ b/internal/repository/users_test.go @@ -1,6 +1,7 @@ package repository_test import ( + "context" "database/sql" "log" "testing" @@ -20,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) { defer db.Close() repo := repository.NewUserRepository(db) - updated, err := repo.Create("testing", "NotSecure", "placeholder") + updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder") if err != nil { log.Println(err) t.FailNow() @@ -37,7 +38,7 @@ func TestCanFindUserInTable(t *testing.T) { defer db.Close() repo := repository.NewUserRepository(db) - updated, err := repo.Create("testing", "NotSecure", "placeholder") + updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder") if err != nil { t.Log(err) t.FailNow() @@ -48,7 +49,7 @@ func TestCanFindUserInTable(t *testing.T) { t.FailNow() } - user, err := repo.GetByName("testing") + user, err := repo.GetByName(context.Background(), "testing") if err != nil { log.Println(err) t.FailNow() @@ -65,7 +66,7 @@ func TestCheckUserHash(t *testing.T) { defer db.Close() repo := repository.NewUserRepository(db) - repo.CheckUserHash("testing", "NotSecure") + repo.CheckUserHash(context.Background(), "testing", "NotSecure") } func setupInMemoryDb() (*sql.DB, error) { diff --git a/internal/respositoryServices/refreshTokens.go b/internal/respositoryServices/refreshTokens.go new file mode 100644 index 0000000..ddd6033 --- /dev/null +++ b/internal/respositoryServices/refreshTokens.go @@ -0,0 +1,87 @@ +package respositoryservices + +import ( + "context" + "database/sql" + "errors" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" + "github.com/google/uuid" +) + +const ( + ErrUnexpectedAmountOfRowsUpdated = "got a unexpected of rows updated" +) + +type RefreshToken interface { + Create(ctx context.Context, username string) (string, error) + GetByName(ctx context.Context, name string) (domain.RefreshTokenEntity, error) + Delete(ctx context.Context, id int64) (int64, error) + IsRequestValid(ctx context.Context, username, refreshToken string) error +} + +// A new jwt token can be made if the user has the correct refresh token for the user. +// It will also require the old JWT token so the expire time is pulled and part of the validation +type RefreshTokenService struct { + table repository.RefreshTokenRepository +} + +func NewRefreshTokenService(conn *sql.DB) RefreshTokenService { + return RefreshTokenService{ + table: repository.NewRefreshTokenRepository(conn), + } +} + +func (rt RefreshTokenService) Create(ctx context.Context, username string) (string, error) { + //if a refresh token already exists for a user, reuse + existingToken, err := rt.GetByName(ctx, username) + if err == nil { + rowsRemoved, err := rt.Delete(ctx, existingToken.ID) + if err != nil { + return "", err + } + + if rowsRemoved != 1 { + return "", errors.New(ErrUnexpectedAmountOfRowsUpdated) + } + } + + token, err := uuid.NewV7() + if err != nil { + return "", err + } + + rows, err := rt.table.Create(ctx, username, token.String()) + if err != nil { + return "", err + } + + if rows != 1 { + return "", errors.New("expected one row but got none") + } + return token.String(), nil +} + +// Find the saved refresh token for a user and return it if it exists +func (rt RefreshTokenService) GetByName(ctx context.Context, name string) (domain.RefreshTokenEntity, error) { + return rt.table.GetByUsername(ctx, name) +} + +// This will request that a object is removed from the database +func (rt RefreshTokenService) Delete(ctx context.Context, id int64) (int64, error) { + return rt.table.DeleteById(ctx, id) +} + +func (rt RefreshTokenService) IsRequestValid(ctx context.Context, username, refreshToken string) error { + token, err := rt.GetByName(ctx, username) + if err != nil { + return err + } + + if token.Token != refreshToken { + return errors.New("the refresh token given does not match") + } + + return nil +} diff --git a/internal/respositoryServices/userService.go b/internal/respositoryServices/userService.go new file mode 100644 index 0000000..e187ca0 --- /dev/null +++ b/internal/respositoryServices/userService.go @@ -0,0 +1,171 @@ +package respositoryservices + +import ( + "context" + "database/sql" + "errors" + "strings" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" + + "golang.org/x/crypto/bcrypt" +) + +const ( + ErrPasswordNotLongEnough = "password needs to be 8 character or longer" + ErrPasswordMissingSpecialCharacter = "password needs to contain one of the following: !, @, #" + ErrInvalidPassword = "invalid password" +) + +type UserServices interface { + DoesUserExist(ctx context.Context, username string) error + DoesPasswordMatchHash(ctx context.Context, username, password string) error + GetUser(ctx context.Context, username string) (domain.UserEntity, error) + AddScopes(ctx context.Context, username string, scopes []string) error + RemoveScopes(ctx context.Context, username string, scopes []string) error + Create(ctx context.Context, name, password, scope string) (domain.UserEntity, error) + CheckPasswordForRequirements(password string) error +} + +// This will handle operations that are user related, but one layer higher then the repository +type UserService struct { + repo repository.Users +} + +// This is a layer on top of the Users Repository. +// Use this over directly talking to the table when ever possible. +func NewUserService(conn *sql.DB) UserService { + return UserService{ + repo: repository.NewUserRepository(conn), + } +} + +func (us UserService) DoesUserExist(ctx context.Context, username string) error { + _, err := us.repo.GetByName(ctx, username) + if err != nil { + return err + } + return nil +} + +func (us UserService) DoesPasswordMatchHash(ctx context.Context, username, password string) error { + model, err := us.GetUser(ctx, username) + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword([]byte(model.Hash), []byte(password)) + if err != nil { + return errors.New(ErrInvalidPassword) + } + + return nil +} + +func (us UserService) GetUser(ctx context.Context, username string) (domain.UserEntity, error) { + return us.repo.GetByName(ctx, username) +} + +func (us UserService) AddScopes(ctx context.Context, username string, scopes []string) error { + usr, err := us.repo.GetByName(ctx, username) + if err != nil { + return err + } + + if usr.Username != username { + return errors.New(repository.ErrUserNotFound) + } + + currentScopes := strings.Split(usr.Scopes, ",") + + // check the current scopes + for _, item := range scopes { + if !strings.Contains(usr.Scopes, item) { + currentScopes = append(currentScopes, item) + } + } + return us.repo.UpdateScopes(ctx, username, strings.Join(currentScopes, ",")) +} + +func (us UserService) RemoveScopes(ctx context.Context, username string, scopes []string) error { + usr, err := us.repo.GetByName(ctx, username) + if err != nil { + return err + } + + if usr.Username != username { + return errors.New(repository.ErrUserNotFound) + } + + var newScopes []string + + // check all the scopes that are currently assigned + for _, item := range strings.Split(usr.Scopes, ",") { + + // check the scopes given, if one matches skip it + if us.doesScopeExist(scopes, item) { + continue + } + + // did not match, add it + newScopes = append(newScopes, item) + } + + return us.repo.UpdateScopes(ctx, username, strings.Join(newScopes, ",")) +} + +func (us UserService) doesScopeExist(scopes []string, target string) bool { + for _, item := range scopes { + if item == target { + return true + } + } + return false +} + +func (us UserService) Create(ctx context.Context, name, password, scope string) (domain.UserEntity, error) { + err := us.CheckPasswordForRequirements(password) + if err != nil { + return domain.UserEntity{}, err + } + + us.repo.Create(ctx, name, password, domain.ScopeRead) + return domain.UserEntity{}, nil +} + +func (us UserService) CheckPasswordForRequirements(password string) error { + err := us.checkPasswordLength(password) + if err != nil { + return err + } + + err = us.checkPasswordForSpecialCharacters(password) + if err != nil { + return err + } + + return nil +} + +func (us UserService) checkPasswordLength(password string) error { + if len(password) < 8 { + return errors.New(ErrPasswordNotLongEnough) + } + return nil +} + +func (us UserService) checkPasswordForSpecialCharacters(password string) error { + var chars []string + chars = append(chars, "!") + chars = append(chars, "@") + chars = append(chars, "#") + + for _, char := range chars { + if strings.Contains(password, char) { + return nil + } + } + + return errors.New(ErrPasswordMissingSpecialCharacter) +} diff --git a/internal/services/config.go b/internal/services/config.go index 8a923a9..6b34302 100644 --- a/internal/services/config.go +++ b/internal/services/config.go @@ -35,6 +35,7 @@ const ( type Configs struct { ServerAddress string JwtSecret string + AdminSecret string RedditEnabled bool RedditPullTop bool @@ -65,7 +66,7 @@ func NewConfig() ConfigClient { func GetEnvConfig() Configs { return Configs{ ServerAddress: os.Getenv(ServerAddress), - JwtSecret: os.Getenv("JwtSecret"), + JwtSecret: os.Getenv("JwtSecret"), RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)), RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)), diff --git a/internal/services/database.go b/internal/services/database.go index d5db9cd..900c1a0 100644 --- a/internal/services/database.go +++ b/internal/services/database.go @@ -4,15 +4,16 @@ import ( "database/sql" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" + repositoryservice "git.jamestombleson.com/jtom38/newsbot-api/internal/respositoryServices" ) type RepositoryService struct { AlertDiscord repository.AlertDiscordRepo Articles repository.ArticlesRepo DiscordWebHooks repository.DiscordWebHookRepo - RefreshTokens repository.RefreshToken + RefreshTokens repositoryservice.RefreshToken Sources repository.Sources - Users repository.Users + Users repositoryservice.UserServices UserSourceSubscriptions repository.UserSourceRepo } @@ -21,9 +22,9 @@ func NewRepositoryService(conn *sql.DB) RepositoryService { AlertDiscord: repository.NewAlertDiscordRepository(conn), Articles: repository.NewArticleRepository(conn), DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), - RefreshTokens: repository.NewRefreshTokenRepository(conn), + RefreshTokens: repositoryservice.NewRefreshTokenService(conn), Sources: repository.NewSourceRepository(conn), - Users: repository.NewUserRepository(conn), + Users: repositoryservice.NewUserService(conn), UserSourceSubscriptions: repository.NewUserSourceRepository(conn), } } -- 2.45.2 From c539a20cc78e5a795cc05dd93234ffb6616698ac Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 7 May 2024 18:19:41 -0700 Subject: [PATCH 06/10] got the user routes exposed with swagger, added jwt support to swagger and also updated how the scopes are validated --- docs/docs.go | 265 +++++++++++++++++++++++++++++++++ docs/swagger.json | 265 +++++++++++++++++++++++++++++++++ docs/swagger.yaml | 168 +++++++++++++++++++++ internal/domain/scopes.go | 6 +- internal/handler/v1/auth.go | 78 ++++++++-- internal/handler/v1/handler.go | 34 +++++ internal/handler/v1/jwt.go | 37 +++-- internal/services/config.go | 1 + 8 files changed, 822 insertions(+), 32 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 463c253..58e97ea 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -903,6 +903,226 @@ const docTemplate = `{ } } } + }, + "/v1/users/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Logs into the API and returns a bearer token if successful", + "parameters": [ + { + "type": "string", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "name": "username", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/refreshToken": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Generates a new token", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Creates a new user", + "parameters": [ + { + "type": "string", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "name": "username", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/scopes/add": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Adds a new scope to a user account", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateScopesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/scopes/remove": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Adds a new scope to a user account", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateScopesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } } }, "definitions": { @@ -1023,6 +1243,34 @@ const docTemplate = `{ } } }, + "domain.LoginResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "domain.RefreshTokenRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "domain.SourceDto": { "type": "object", "properties": { @@ -1059,6 +1307,23 @@ const docTemplate = `{ } } } + }, + "domain.UpdateScopesRequest": { + "type": "object", + "required": [ + "scopes" + ], + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "username": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index e9cb4ca..2e61798 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -894,6 +894,226 @@ } } } + }, + "/v1/users/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Logs into the API and returns a bearer token if successful", + "parameters": [ + { + "type": "string", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "name": "username", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/refreshToken": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "tags": [ + "Users" + ], + "summary": "Generates a new token", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Creates a new user", + "parameters": [ + { + "type": "string", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "name": "username", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/scopes/add": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Adds a new scope to a user account", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateScopesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } + }, + "/v1/users/scopes/remove": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Adds a new scope to a user account", + "parameters": [ + { + "description": "body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateScopesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } + } } }, "definitions": { @@ -1014,6 +1234,34 @@ } } }, + "domain.LoginResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "token": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "domain.RefreshTokenRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "domain.SourceDto": { "type": "object", "properties": { @@ -1050,6 +1298,23 @@ } } } + }, + "domain.UpdateScopesRequest": { + "type": "object", + "required": [ + "scopes" + ], + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "username": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ad8429d..a60e835 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -78,6 +78,24 @@ definitions: $ref: '#/definitions/domain.DiscordWebHookDto' type: array type: object + domain.LoginResponse: + properties: + message: + type: string + refreshToken: + type: string + token: + type: string + type: + type: string + type: object + domain.RefreshTokenRequest: + properties: + refreshToken: + type: string + username: + type: string + type: object domain.SourceDto: properties: enabled: @@ -102,6 +120,17 @@ definitions: $ref: '#/definitions/domain.SourceDto' type: array type: object + domain.UpdateScopesRequest: + properties: + scopes: + items: + type: string + type: array + username: + type: string + required: + - scopes + type: object info: contact: {} title: NewsBot collector @@ -670,6 +699,145 @@ paths: summary: Creates a new youtube source to monitor. tags: - Source + /v1/users/login: + post: + parameters: + - in: formData + name: password + type: string + - in: formData + name: username + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' + summary: Logs into the API and returns a bearer token if successful + tags: + - Users + /v1/users/refreshToken: + post: + parameters: + - description: body + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.RefreshTokenRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] + summary: Generates a new token + tags: + - Users + /v1/users/register: + post: + parameters: + - in: formData + name: password + type: string + - in: formData + name: username + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' + summary: Creates a new user + tags: + - Users + /v1/users/scopes/add: + post: + consumes: + - application/json + parameters: + - description: body + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateScopesRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' + summary: Adds a new scope to a user account + tags: + - Users + /v1/users/scopes/remove: + post: + consumes: + - application/json + parameters: + - description: body + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateScopesRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' + summary: Adds a new scope to a user account + tags: + - Users securityDefinitions: Bearer: description: Type "Bearer" followed by a space and JWT token. diff --git a/internal/domain/scopes.go b/internal/domain/scopes.go index c3ef7df..23f4bbf 100644 --- a/internal/domain/scopes.go +++ b/internal/domain/scopes.go @@ -1,6 +1,8 @@ package domain const ( - ScopeAll = "newsbot:all" - ScopeRead = "newsbot:read" + ScopeAll = "newsbot:all" + ScopeArticleRead = "newsbot:article:read" + ScopeSourceCreate = "newsbot:source:create" + ScopeDiscordWebHookCreate = "newsbot:discordwebhook:create" ) diff --git a/internal/handler/v1/auth.go b/internal/handler/v1/auth.go index 7847f56..458c320 100644 --- a/internal/handler/v1/auth.go +++ b/internal/handler/v1/auth.go @@ -15,12 +15,15 @@ const ( ErrUsernameAlreadyExists = "the requested username already exists" ) -// Register // @Summary Creates a new user -// @Tags Users +// @Router /v1/users/register [post] +// @Param request formData domain.LoginFormRequest true "form" +// @Accepts x-www-form-urlencoded +// @Produce json +// @Tags Users // @Success 200 {object} domain.BaseResponse -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (h *Handler) AuthRegister(c echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") @@ -44,7 +47,7 @@ func (h *Handler) AuthRegister(c echo.Context) error { return h.WriteError(c, err, http.StatusInternalServerError) } - _, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeRead) + _, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeArticleRead) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -54,17 +57,26 @@ func (h *Handler) AuthRegister(c echo.Context) error { }) } +// @Summary Logs into the API and returns a bearer token if successful +// @Router /v1/users/login [post] +// @Param request formData domain.LoginFormRequest true "form" +// @Accepts x-www-form-urlencoded +// @Produce json +// @Tags Users +// @Success 200 {object} domain.LoginResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (h *Handler) AuthLogin(c echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") // Check to see if they are trying to login with the admin token if username == "" { - return h.validateAdminToken(c, password) + return h.createAdminToken(c, password) } // check if the user exists - err := h.repo.Users.DoesUserExist(c.Request().Context(), username) + user, err := h.repo.Users.GetUser(c.Request().Context(), username) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -78,7 +90,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { // TODO think about moving this down some? expiresAt := time.Now().Add(time.Hour * 48) - jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, expiresAt) + jwt, err := h.generateJwtWithExp(username, user.Scopes, h.config.ServerAddress, expiresAt) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -98,7 +110,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { }) } -func (h *Handler) validateAdminToken(c echo.Context, password string) error { +func (h *Handler) createAdminToken(c echo.Context, password string) error { // if the admin token is blank, then the admin wanted this disabled. // this will fail right away and not progress. if h.config.AdminSecret == "" { @@ -109,15 +121,30 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error { return h.UnauthorizedResponse(c, ErrUserNotFound) } - token, err := h.generateJwt("admin", h.config.ServerAddress) + token, err := h.generateJwt("admin", domain.ScopeAll, h.config.ServerAddress) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } - return c.JSON(http.StatusOK, token) + return c.JSON(http.StatusOK, domain.LoginResponse{ + BaseResponse: domain.BaseResponse{ + Message: "OK", + }, + Token: token, + Type: "Bearer", + }) } // This will take collect some information about the requested refresh, validate and then return a new jwt token if approved. +// Register +// @Summary Generates a new token +// @Router /v1/users/refreshToken [post] +// @Param request body domain.RefreshTokenRequest true "body" +// @Tags Users +// @Success 200 {object} domain.LoginResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (h *Handler) RefreshJwtToken(c echo.Context) error { // Check the context for the refresh token var request domain.RefreshTokenRequest @@ -131,7 +158,12 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { return h.InternalServerErrorResponse(c, err.Error()) } - jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, time.Now().Add(time.Hour*48)) + user, err := h.repo.Users.GetUser(c.Request().Context(), request.Username) + if err != nil { + return h.InternalServerErrorResponse(c, err.Error()) + } + + jwt, err := h.generateJwtWithExp(request.Username, user.Scopes, h.config.ServerAddress, time.Now().Add(time.Hour*48)) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -151,8 +183,17 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { }) } +// @Summary Adds a new scope to a user account +// @Router /v1/users/scopes/add [post] +// @Param request body domain.UpdateScopesRequest true "body" +// @Tags Users +// @Accept json +// @Produce json +// @Success 200 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (h *Handler) AddScopes(c echo.Context) error { - token, err := h.getJwtToken(c) + token, err := h.getJwtTokenFromContext(c) if err != nil { return h.UnauthorizedResponse(c, err.Error()) } @@ -178,8 +219,17 @@ func (h *Handler) AddScopes(c echo.Context) error { }) } +// @Summary Adds a new scope to a user account +// @Router /v1/users/scopes/remove [post] +// @Param request body domain.UpdateScopesRequest true "body" +// @Tags Users +// @Accept json +// @Produce json +// @Success 200 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (h *Handler) RemoveScopes(c echo.Context) error { - token, err := h.getJwtToken(c) + token, err := h.getJwtTokenFromContext(c) if err != nil { return h.WriteError(c, err, http.StatusUnauthorized) } diff --git a/internal/handler/v1/handler.go b/internal/handler/v1/handler.go index 2bc9d9e..56e0da7 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -100,6 +100,14 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han sources.POST("/:ID/disable", s.disableSource) sources.POST("/:ID/enable", s.enableSource) + users := v1.Group("/users") + users.POST("/login", s.AuthLogin) + users.POST("/register", s.AuthRegister) + users.Use(echojwt.WithConfig(jwtConfig)) + users.POST("/scopes/add", s.AddScopes) + users.POST("/scopes/remove", s.RemoveScopes) + users.POST("/refreshToken", s.RefreshJwtToken) + s.Router = router return s } @@ -136,3 +144,29 @@ func (s *Handler) UnauthorizedResponse(c echo.Context, msg string) error { Message: msg, }) } + +// 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 { + token, err := s.getJwtTokenFromContext(c) + if err != nil { + s.WriteMessage(c, ErrJwtMissing, http.StatusUnauthorized) + } + + err = token.hasScope(requiredScope) + if err != nil { + s.WriteMessage(c, ErrJwtScopeMissing, http.StatusUnauthorized) + } + + if token.Iss != s.config.ServerAddress { + s.WriteMessage(c, ErrJwtInvalidIssuer, http.StatusUnauthorized) + } + + err = token.hasExpired() + if err != nil { + s.WriteMessage(c, ErrJwtExpired, http.StatusUnauthorized) + } + + return token +} \ No newline at end of file diff --git a/internal/handler/v1/jwt.go b/internal/handler/v1/jwt.go index 92f77be..5ded140 100644 --- a/internal/handler/v1/jwt.go +++ b/internal/handler/v1/jwt.go @@ -15,6 +15,7 @@ const ( ErrJwtClaimsMissing = "claims missing on token" ErrJwtExpired = "auth token has expired" ErrJwtScopeMissing = "required scope is missing" + ErrJwtInvalidIssuer = "incorrect server issued the token" ) type JwtToken struct { @@ -32,6 +33,13 @@ func (j JwtToken) IsValid(scope string) error { return err } + // Check to see if they have the scope to do anything + // if they do, let them pass + err = j.hasScope(domain.ScopeAll) + if err == nil { + return nil + } + err = j.hasScope(scope) if err != nil { return err @@ -53,25 +61,27 @@ func (j JwtToken) hasExpired() error { return nil } +// This will check the users token to make sure they have the correct scope to access the handler. +// It will evaluate if you have the admin scope or the required scope for the handler. func (j JwtToken) hasScope(scope string) error { // they have the scope to access everything, so let them pass. - if strings.Contains(domain.ScopeAll, scope) { + userScopes := strings.Join(j.Scopes, "") + if strings.Contains(domain.ScopeAll, userScopes) { return nil } - for _, s := range j.Scopes { - if strings.Contains(s, scope) { - return nil - } + if strings.Contains(userScopes, scope) { + return nil } + return errors.New(ErrJwtScopeMissing) } -func (h *Handler) generateJwt(username, issuer string) (string, error) { - return h.generateJwtWithExp(username, issuer, time.Now().Add(10*time.Minute)) +func (h *Handler) generateJwt(username, scopes, issuer string) (string, error) { + return h.generateJwtWithExp(username, scopes, issuer, time.Now().Add(10*time.Minute)) } -func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Time) (string, error) { +func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, expiresAt time.Time) (string, error) { secret := []byte(h.config.JwtSecret) // Anyone who wants to decrypt the key needs to use the same method @@ -83,13 +93,8 @@ func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Tim claims["iss"] = issuer var scopes []string - if username == "admin" { - scopes = append(scopes, domain.ScopeAll) - claims["scopes"] = scopes - } else { - scopes = append(scopes, domain.ScopeRead) - claims["scopes"] = scopes - } + scopes = append(scopes, domain.ScopeAll) + claims["scopes"] = scopes tokenString, err := token.SignedString(secret) if err != nil { @@ -99,7 +104,7 @@ func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Tim return tokenString, nil } -func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) { +func (h *Handler) getJwtTokenFromContext(c echo.Context) (JwtToken, error) { // Make sure that the request came with a jwtToken token, ok := c.Get("user").(*jwt.Token) if !ok { diff --git a/internal/services/config.go b/internal/services/config.go index 6b34302..f181aa1 100644 --- a/internal/services/config.go +++ b/internal/services/config.go @@ -67,6 +67,7 @@ func GetEnvConfig() Configs { return Configs{ ServerAddress: os.Getenv(ServerAddress), JwtSecret: os.Getenv("JwtSecret"), + AdminSecret: os.Getenv("AdminSecret"), RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)), RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)), -- 2.45.2 From 471ef4fdd897723f1d48dcb000ed9475a5fee72e Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 7 May 2024 19:14:37 -0700 Subject: [PATCH 07/10] adding userId to the jwt and updated routes to use scope requirements --- internal/domain/requests.go | 5 + internal/domain/scopes.go | 12 +- internal/handler/v1/articles.go | 9 +- internal/handler/v1/auth.go | 10 +- internal/handler/v1/discordwebhooks.go | 37 +++- internal/handler/v1/handler.go | 26 ++- internal/handler/v1/jwt.go | 12 +- internal/handler/v1/sources.go | 190 ++++++++++---------- internal/respositoryServices/userService.go | 2 +- 9 files changed, 176 insertions(+), 127 deletions(-) diff --git a/internal/domain/requests.go b/internal/domain/requests.go index 4be9e81..557e3b5 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -1,5 +1,10 @@ package domain +type LoginFormRequest struct { + Username string `form:"username"` + Password string `form:"password"` +} + type GetSourceBySourceAndNameParamRequest struct { Name string `query:"name"` Source string `query:"source"` diff --git a/internal/domain/scopes.go b/internal/domain/scopes.go index 23f4bbf..15e9dc8 100644 --- a/internal/domain/scopes.go +++ b/internal/domain/scopes.go @@ -1,8 +1,14 @@ package domain const ( - ScopeAll = "newsbot:all" - ScopeArticleRead = "newsbot:article:read" - ScopeSourceCreate = "newsbot:source:create" + ScopeAll = "newsbot:all" + + ScopeArticleRead = "newsbot:article:read" + ScopeArticleDisable = "newsbot:article:disable" + + ScopeSourceRead = "newsbot:source:read" + ScopeSourceCreate = "newsbot:source:create" + ScopeDiscordWebHookCreate = "newsbot:discordwebhook:create" + ScopeDiscordWebhookRead = "newsbot:discordwebhook:read" ) diff --git a/internal/handler/v1/articles.go b/internal/handler/v1/articles.go index 21ff796..870180a 100644 --- a/internal/handler/v1/articles.go +++ b/internal/handler/v1/articles.go @@ -20,6 +20,8 @@ import ( // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) listArticles(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeArticleRead) + resp := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -51,6 +53,7 @@ func (s *Handler) listArticles(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getArticle(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeArticleRead) p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -86,13 +89,12 @@ func (s *Handler) getArticle(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getArticleDetails(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeArticleRead) p := domain.ArticleDetailedResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, }, - Payload: domain.ArticleAndSourceModel{ - - }, + Payload: domain.ArticleAndSourceModel{}, } id, err := strconv.Atoi(c.Param("ID")) @@ -128,6 +130,7 @@ func (s *Handler) getArticleDetails(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) ListArticlesBySourceId(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeArticleRead) p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, diff --git a/internal/handler/v1/auth.go b/internal/handler/v1/auth.go index 458c320..bfd8496 100644 --- a/internal/handler/v1/auth.go +++ b/internal/handler/v1/auth.go @@ -90,7 +90,7 @@ func (h *Handler) AuthLogin(c echo.Context) error { // TODO think about moving this down some? expiresAt := time.Now().Add(time.Hour * 48) - jwt, err := h.generateJwtWithExp(username, user.Scopes, h.config.ServerAddress, expiresAt) + jwt, err := h.generateJwtWithExp(username, user.Scopes, h.config.ServerAddress, user.ID, expiresAt) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -121,7 +121,7 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error { return h.UnauthorizedResponse(c, ErrUserNotFound) } - token, err := h.generateJwt("admin", domain.ScopeAll, h.config.ServerAddress) + token, err := h.generateJwt("admin", domain.ScopeAll, h.config.ServerAddress, -1) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -130,8 +130,8 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error { BaseResponse: domain.BaseResponse{ Message: "OK", }, - Token: token, - Type: "Bearer", + Token: token, + Type: "Bearer", }) } @@ -163,7 +163,7 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { return h.InternalServerErrorResponse(c, err.Error()) } - jwt, err := h.generateJwtWithExp(request.Username, user.Scopes, h.config.ServerAddress, time.Now().Add(time.Hour*48)) + jwt, err := h.generateJwtWithExp(request.Username, user.Scopes, h.config.ServerAddress, user.ID, time.Now().Add(time.Hour*48)) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } diff --git a/internal/handler/v1/discordwebhooks.go b/internal/handler/v1/discordwebhooks.go index 65fd3ee..7e2e6f6 100644 --- a/internal/handler/v1/discordwebhooks.go +++ b/internal/handler/v1/discordwebhooks.go @@ -18,7 +18,9 @@ import ( // @Success 200 {object} domain.DiscordWebhookResponse // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) ListDiscordWebHooks(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -42,7 +44,9 @@ func (s *Handler) ListDiscordWebHooks(c echo.Context) error { // @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -74,7 +78,9 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { // @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -110,13 +116,9 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { // @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) NewDiscordWebHook(c echo.Context) error { - token, err := s.getJwtToken(c) - if err != nil { - return c.JSON(http.StatusUnauthorized, domain.BaseResponse{ - Message: ErrJwtMissing, - }) - } + token := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) _url := c.QueryParam("url") _server := c.QueryParam("server") @@ -181,7 +183,9 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { // @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (s *Handler) disableDiscordWebHook(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) id, err := strconv.Atoi(c.Param("ID")) if err != nil { return c.JSON(http.StatusBadRequest, domain.BaseResponse{ @@ -190,11 +194,15 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { } // Check to make sure we can find the record - _, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) + record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { s.WriteError(c, err, http.StatusInternalServerError) } + if record.UserID != s.GetUserIdFromJwtToken(c) { + s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + } + // flip the it updated, err := s.repo.DiscordWebHooks.Disable(c.Request().Context(), int64(id)) if err != nil { @@ -226,18 +234,24 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { // @Param id path int true "id" // @Tags DiscordWebhook // @Router /v1/discord/webhooks/{ID}/enable [post] +// @Security Bearer func (s *Handler) enableDiscordWebHook(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) id, err := strconv.Atoi(c.Param("ID")) if err != nil { s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record - _, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) + record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { s.WriteError(c, err, http.StatusBadRequest) } + if record.UserID != s.GetUserIdFromJwtToken(c) { + s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + } + updated, err := s.repo.DiscordWebHooks.Enable(c.Request().Context(), int64(id)) if err != nil { s.WriteError(c, err, http.StatusInternalServerError) @@ -271,17 +285,22 @@ func (s *Handler) enableDiscordWebHook(c echo.Context) error { // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse func (s *Handler) deleteDiscordWebHook(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) id, err := strconv.Atoi(c.Param("ID")) if err != nil { return c.JSON(http.StatusBadRequest, err.Error()) } // Check to make sure we can find the record - _, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) + record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { return c.JSON(http.StatusInternalServerError, err.Error()) } + if record.UserID != s.GetUserIdFromJwtToken(c) { + s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + } + // Soft delete the record updated, err := s.repo.DiscordWebHooks.SoftDelete(c.Request().Context(), int64(id)) if err != nil { diff --git a/internal/handler/v1/handler.go b/internal/handler/v1/handler.go index 56e0da7..d8369f0 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -12,25 +12,28 @@ import ( swagger "github.com/swaggo/echo-swagger" _ "git.jamestombleson.com/jtom38/newsbot-api/docs" - "git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/services" ) type Handler struct { Router *echo.Echo - Db *database.Queries + //Db *database.Queries config services.Configs repo services.RepositoryService } const ( - ErrParameterIdMissing = "The requested parameter ID was not found." - ErrParameterMissing = "The requested parameter was not found found:" - ErrUnableToParseId = "Unable to parse the requested ID" + ErrParameterIdMissing = "The requested parameter ID was not found." + ErrParameterMissing = "The requested parameter was not found found:" + ErrUnableToParseId = "Unable to parse the requested ID" + ErrRecordMissing = "The requested record was not found" ErrFailedToCreateRecord = "The record was not created due to a database problem" - ErrUserUnknown = "User is unknown" + ErrFailedToUpdateRecord = "The requested record was not updated due to a database problem" + + ErrUserUnknown = "User is unknown" + ErrYouDontOwnTheRecord = "The record requested does not belong to you" ResponseMessageSuccess = "Success" ) @@ -169,4 +172,13 @@ func (s *Handler) ValidateJwtToken(c echo.Context, requiredScope string) JwtToke } return token -} \ No newline at end of file +} + +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() +} diff --git a/internal/handler/v1/jwt.go b/internal/handler/v1/jwt.go index 5ded140..22aaa74 100644 --- a/internal/handler/v1/jwt.go +++ b/internal/handler/v1/jwt.go @@ -23,6 +23,7 @@ type JwtToken struct { Iss string `json:"iss"` Authorized bool `json:"authorized"` UserName string `json:"username"` + UserId int64 `json:"userId"` Scopes []string `json:"scopes"` jwt.RegisteredClaims } @@ -52,6 +53,10 @@ func (j JwtToken) GetUsername() string { return j.UserName } +func (j JwtToken) GetUserId() int64 { + return j.UserId +} + func (j JwtToken) hasExpired() error { // Check to see if the token has expired hasExpired := j.Exp.Compare(time.Now()) @@ -77,11 +82,11 @@ func (j JwtToken) hasScope(scope string) error { return errors.New(ErrJwtScopeMissing) } -func (h *Handler) generateJwt(username, scopes, issuer string) (string, error) { - return h.generateJwtWithExp(username, scopes, issuer, time.Now().Add(10*time.Minute)) +func (h *Handler) generateJwt(username, scopes, issuer string, userId int64) (string, error) { + return h.generateJwtWithExp(username, scopes, issuer, userId, time.Now().Add(10*time.Minute)) } -func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, expiresAt time.Time) (string, error) { +func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId int64, expiresAt time.Time) (string, error) { secret := []byte(h.config.JwtSecret) // Anyone who wants to decrypt the key needs to use the same method @@ -91,6 +96,7 @@ func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, expire claims["authorized"] = true claims["username"] = username claims["iss"] = issuer + claims["userId"] = userId var scopes []string scopes = append(scopes, domain.ScopeAll) diff --git a/internal/handler/v1/sources.go b/internal/handler/v1/sources.go index 7379f2c..f5f1543 100644 --- a/internal/handler/v1/sources.go +++ b/internal/handler/v1/sources.go @@ -1,17 +1,13 @@ package v1 import ( - "context" - "encoding/json" "fmt" "net/http" "strconv" "strings" - "git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/services" - "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -25,6 +21,7 @@ import ( // @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" // @Security Bearer func (s *Handler) listSources(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceRead) resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -58,6 +55,7 @@ func (s *Handler) listSources(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) listSourcesBySource(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceRead) resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -97,6 +95,7 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getSource(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceRead) resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -133,6 +132,7 @@ func (s *Handler) getSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceRead) resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -169,6 +169,8 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) newRedditSource(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceCreate) + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -178,20 +180,13 @@ func (s *Handler) newRedditSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusBadRequest) } - if param.Url == "" { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: "Url is missing a value", - }) + s.WriteMessage(c, "url is missing", http.StatusBadRequest) } if !strings.Contains(param.Url, "reddit.com") { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: "Invalid URL given", - }) + s.WriteMessage(c, "invalid url", http.StatusBadRequest) } tags := fmt.Sprintf("twitch, %v, %s", param.Name, param.Tags) @@ -223,61 +218,54 @@ func (s *Handler) newRedditSource(c echo.Context) error { // @Router /v1/sources/new/youtube [post] // @Security Bearer func (s *Handler) newYoutubeSource(c echo.Context) error { + // Validate the jwt + s.ValidateJwtToken(c, domain.ScopeSourceCreate) + var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusBadRequest) } - - //query := r.URL.Query() - //_name := query["name"][0] - //_url := query["url"][0] - ////_tags := query["tags"][0] - if param.Url == "" { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: "url is missing a value", - }) + s.WriteMessage(c, "url is missing a value", http.StatusBadRequest) } if !strings.Contains(param.Url, "youtube.com") { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: "Invalid URL", - }) + s.WriteMessage(c, "invalid url", http.StatusBadRequest) + } + + resp := domain.SourcesResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, + }, + } + + item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name) + if err == nil { + var dto []domain.SourceDto + dto = append(dto, services.SourceToDto(item)) + resp.Payload = dto + return c.JSON(http.StatusOK, resp) } - /* - if _tags == "" { - tags = fmt.Sprintf("twitch, %v", _name) - } else { - } - */ tags := fmt.Sprintf("twitch, %v", param.Name) - - params := database.CreateSourceParams{ - ID: uuid.New(), - Site: "youtube", - Name: param.Name, - Source: "youtube", - Type: "feed", - Enabled: true, - Url: param.Url, - Tags: tags, - } - err = s.Db.CreateSource(context.Background(), params) + rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorYoutube, param.Name, param.Url, tags, true) if err != nil { return c.JSON(http.StatusInternalServerError, err.Error()) } - bJson, err := json.Marshal(¶ms) - if err != nil { - return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - Message: err.Error(), - }) + if rows != 1 { + s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } - return c.JSON(http.StatusOK, bJson) + item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name) + if err == nil { + var dto []domain.SourceDto + dto = append(dto, services.SourceToDto(item)) + resp.Payload = dto + return c.JSON(http.StatusOK, resp) + } + + return c.JSON(http.StatusOK, resp) } // NewTwitchSource @@ -287,6 +275,8 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { // @Router /v1/sources/new/twitch [post] // @Security Bearer func (s *Handler) newTwitchSource(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceCreate) + var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { @@ -295,37 +285,41 @@ func (s *Handler) newTwitchSource(c echo.Context) error { }) } - //query := r.URL.Query() - //_name := query["name"][0] + resp := domain.SourcesResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, + }, + } tags := fmt.Sprintf("twitch, %v", param.Name) - _url := fmt.Sprintf("https://twitch.tv/%v", param.Name) + url := fmt.Sprintf("https://twitch.tv/%v", param.Name) - params := database.CreateSourceParams{ - ID: uuid.New(), - Site: "twitch", - Name: param.Name, - Source: "twitch", - Type: "api", - Enabled: true, - Url: _url, - Tags: tags, + // Check if the record already exists + item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name) + if err == nil { + var dto []domain.SourceDto + dto = append(dto, services.SourceToDto(item)) + resp.Payload = dto + return c.JSON(http.StatusOK, resp) } - err = s.Db.CreateSource(c.Request().Context(), params) + + rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorTwitch, param.Name, url, tags, true) if err != nil { return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } - bJson, err := json.Marshal(¶ms) - if err != nil { - return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - Message: err.Error(), - }) + if rows != 1 { + s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } - return c.JSON(http.StatusOK, bJson) + item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name) + var dto []domain.SourceDto + dto = append(dto, services.SourceToDto(item)) + resp.Payload = dto + + return c.JSON(http.StatusOK, resp) } // NewRssSource @@ -339,6 +333,8 @@ func (s *Handler) newTwitchSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) newRssSource(c echo.Context) error { + s.ValidateJwtToken(c, domain.ScopeSourceCreate) + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -382,48 +378,50 @@ func (s *Handler) newRssSource(c echo.Context) error { // DeleteSource // @Summary Marks a source as deleted based on its ID value. -// @Param id path string true "id" +// @Param id path int true "id" // @Tags Source // @Router /v1/sources/{id} [POST] +// @Success 200 {object} domain.SourcesResponse "ok" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) deleteSources(c echo.Context) error { - id := c.Param("ID") - uuid, err := uuid.Parse(id) + s.ValidateJwtToken(c, domain.ScopeAll) + id, err := strconv.Atoi(c.Param("ID")) if err != nil { - return c.JSON(http.StatusBadRequest, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record - _, err = s.Db.GetSourceByID(c.Request().Context(), uuid) + _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusInternalServerError) } // Delete the record - err = s.Db.DeleteSource(c.Request().Context(), uuid) + rows, err := s.repo.Sources.SoftDelete(c.Request().Context(), int64(id)) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusInternalServerError) + } + if rows != 1 { + s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError) } - p := ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, - } - - b, err := json.Marshal(p) + // pull the record with its updated value + item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusInternalServerError) } - return c.JSON(http.StatusOK, b) + var items []domain.SourceDto + items = append(items, services.SourceToDto(item)) + + return c.JSON(http.StatusOK, domain.SourcesResponse{ + BaseResponse: domain.BaseResponse{ + Message: "OK", + }, + Payload: items, + }) } // DisableSource diff --git a/internal/respositoryServices/userService.go b/internal/respositoryServices/userService.go index e187ca0..ea02f51 100644 --- a/internal/respositoryServices/userService.go +++ b/internal/respositoryServices/userService.go @@ -130,7 +130,7 @@ func (us UserService) Create(ctx context.Context, name, password, scope string) return domain.UserEntity{}, err } - us.repo.Create(ctx, name, password, domain.ScopeRead) + us.repo.Create(ctx, name, password, domain.ScopeArticleRead) return domain.UserEntity{}, nil } -- 2.45.2 From 5ff6a8ddae398da8230785131479f0e41dcfb793 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 7 May 2024 22:01:32 -0700 Subject: [PATCH 08/10] updated error handling and refined how the jwt gets used and validated --- internal/handler/v1/articles.go | 35 +++-- internal/handler/v1/auth.go | 29 ++-- internal/handler/v1/discordwebhooks.go | 175 ++++++++++++++----------- internal/handler/v1/handler.go | 36 ++--- internal/handler/v1/jwt.go | 13 +- internal/handler/v1/sources.go | 132 ++++++++++++------- 6 files changed, 257 insertions(+), 163 deletions(-) diff --git a/internal/handler/v1/articles.go b/internal/handler/v1/articles.go index 870180a..b77f4d9 100644 --- a/internal/handler/v1/articles.go +++ b/internal/handler/v1/articles.go @@ -20,7 +20,10 @@ import ( // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) listArticles(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeArticleRead) + _, err := s.ValidateJwtToken(c, domain.ScopeArticleRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } resp := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ @@ -35,7 +38,7 @@ func (s *Handler) listArticles(c echo.Context) error { res, err := s.repo.Articles.ListByPage(c.Request().Context(), page, 25) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } resp.Payload = services.ArticlesToDto(res) @@ -53,7 +56,11 @@ func (s *Handler) listArticles(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getArticle(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeArticleRead) + _, err := s.ValidateJwtToken(c, domain.ScopeArticleRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -63,7 +70,7 @@ func (s *Handler) getArticle(c echo.Context) error { id := c.Param("ID") idNumber, err := strconv.Atoi(id) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } item, err := s.repo.Articles.GetById(c.Request().Context(), int64(idNumber)) @@ -89,7 +96,11 @@ func (s *Handler) getArticle(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getArticleDetails(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeArticleRead) + _, err := s.ValidateJwtToken(c, domain.ScopeArticleRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.ArticleDetailedResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -99,17 +110,17 @@ func (s *Handler) getArticleDetails(c echo.Context) error { id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } article, err := s.repo.Articles.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } source, err := s.repo.Sources.GetById(c.Request().Context(), article.SourceID) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } p.Payload.Article = services.ArticleToDto(article) @@ -130,7 +141,11 @@ func (s *Handler) getArticleDetails(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) ListArticlesBySourceId(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeArticleRead) + _, err := s.ValidateJwtToken(c, domain.ScopeArticleRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.ArticleResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -139,7 +154,7 @@ func (s *Handler) ListArticlesBySourceId(c echo.Context) error { id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // if the page number is missing, default to 0 diff --git a/internal/handler/v1/auth.go b/internal/handler/v1/auth.go index bfd8496..9320cb6 100644 --- a/internal/handler/v1/auth.go +++ b/internal/handler/v1/auth.go @@ -2,6 +2,7 @@ package v1 import ( "net/http" + "strings" "time" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" @@ -89,8 +90,9 @@ func (h *Handler) AuthLogin(c echo.Context) error { // TODO think about moving this down some? expiresAt := time.Now().Add(time.Hour * 48) + userScopes := strings.Split(user.Scopes, ",") - jwt, err := h.generateJwtWithExp(username, user.Scopes, h.config.ServerAddress, user.ID, expiresAt) + jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, userScopes, user.ID, expiresAt) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -120,8 +122,10 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error { if h.config.AdminSecret != password { return h.UnauthorizedResponse(c, ErrUserNotFound) } + var userScopes []string + userScopes = append(userScopes, domain.ScopeAll) - token, err := h.generateJwt("admin", domain.ScopeAll, h.config.ServerAddress, -1) + token, err := h.generateJwt("admin", h.config.ServerAddress, userScopes, -1) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -146,9 +150,14 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (h *Handler) RefreshJwtToken(c echo.Context) error { + _, err := h.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + if err != nil { + return h.WriteError(c, err, http.StatusBadRequest) + } + // Check the context for the refresh token var request domain.RefreshTokenRequest - err := (&echo.DefaultBinder{}).BindBody(c, &request) + err = (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -162,8 +171,9 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } + userScopes := strings.Split(user.Scopes, ",") - jwt, err := h.generateJwtWithExp(request.Username, user.Scopes, h.config.ServerAddress, user.ID, time.Now().Add(time.Hour*48)) + jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, userScopes, user.ID, time.Now().Add(time.Hour*48)) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -193,20 +203,15 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse func (h *Handler) AddScopes(c echo.Context) error { - token, err := h.getJwtTokenFromContext(c) + _, err := h.ValidateJwtToken(c, domain.ScopeAll) if err != nil { - return h.UnauthorizedResponse(c, err.Error()) - } - - err = token.IsValid(domain.ScopeAll) - if err != nil { - return h.UnauthorizedResponse(c, err.Error()) + return h.WriteError(c, err, http.StatusBadRequest) } request := domain.UpdateScopesRequest{} err = (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { - h.WriteError(c, err, http.StatusBadRequest) + return h.WriteError(c, err, http.StatusBadRequest) } err = h.repo.Users.AddScopes(c.Request().Context(), request.Username, request.Scopes) diff --git a/internal/handler/v1/discordwebhooks.go b/internal/handler/v1/discordwebhooks.go index 7e2e6f6..4328904 100644 --- a/internal/handler/v1/discordwebhooks.go +++ b/internal/handler/v1/discordwebhooks.go @@ -11,16 +11,20 @@ import ( ) // ListDiscordWebhooks -// @Summary Returns the top 100 -// @Produce application/json -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks [get] -// @Success 200 {object} domain.DiscordWebhookResponse -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns the top 100 +// @Produce application/json +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks [get] +// @Success 200 {object} domain.DiscordWebhookResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) ListDiscordWebHooks(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -36,17 +40,21 @@ func (s *Handler) ListDiscordWebHooks(c echo.Context) error { } // GetDiscordWebHook -// @Summary Returns the top 100 entries from the queue to be processed. -// @Produce application/json -// @Param id path int true "id" -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks/{id} [get] -// @Success 200 {object} domain.DiscordWebhookResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Param id path int true "id" +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks/{id} [get] +// @Success 200 {object} domain.DiscordWebhookResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -55,12 +63,12 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } res, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dtos []domain.DiscordWebHookDto dtos = append(dtos, services.DiscordWebhookToDto(res)) @@ -69,18 +77,22 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { } // GetDiscordWebHookByServerAndChannel -// @Summary Returns all the known web hooks based on the Server and Channel given. -// @Produce application/json -// @Param server query string true "Fancy Server" -// @Param channel query string true "memes" -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks/by/serverAndChannel [get] -// @Success 200 {object} domain.DiscordWebhookResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Returns all the known web hooks based on the Server and Channel given. +// @Produce application/json +// @Param server query string true "Fancy Server" +// @Param channel query string true "memes" +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks/by/serverAndChannel [get] +// @Success 200 {object} domain.DiscordWebhookResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + p := domain.DiscordWebhookResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -89,17 +101,17 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { _server := c.QueryParam("server") if _server == "" { - s.WriteMessage(c, "server was not defined", http.StatusBadRequest) + return s.WriteMessage(c, "server was not defined", http.StatusBadRequest) } _channel := c.QueryParam("channel") if _channel == "" { - s.WriteMessage(c, "channel was not defined", http.StatusBadRequest) + return s.WriteMessage(c, "channel was not defined", http.StatusBadRequest) } res, err := s.repo.DiscordWebHooks.ListByServerAndChannel(c.Request().Context(), _server, _channel) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } p.Payload = services.DiscordWebhooksToDto(res) @@ -107,18 +119,21 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { } // NewDiscordWebHook -// @Summary Creates a new record for a discord web hook to post data to. -// @Param url query string true "url" -// @Param server query string true "Server name" -// @Param channel query string true "Channel name" -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks/new [post] -// @Success 200 {object} domain.DiscordWebhookResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Creates a new record for a discord web hook to post data to. +// @Param url query string true "url" +// @Param server query string true "Server name" +// @Param channel query string true "Channel name" +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks/new [post] +// @Success 200 {object} domain.DiscordWebhookResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) NewDiscordWebHook(c echo.Context) error { - token := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + token, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } _url := c.QueryParam("url") _server := c.QueryParam("server") @@ -147,21 +162,21 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { user, err := s.repo.Users.GetUser(c.Request().Context(), token.UserName) if err != nil { - s.WriteMessage(c, ErrUserUnknown, http.StatusBadRequest) + return s.WriteMessage(c, ErrUserUnknown, http.StatusBadRequest) } rows, err := s.repo.DiscordWebHooks.Create(c.Request().Context(), user.ID, _url, _server, _channel, true) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if rows != 1 { - s.WriteMessage(c, "data was not written to database", http.StatusInternalServerError) + return s.WriteMessage(c, "data was not written to database", http.StatusInternalServerError) } item, err := s.repo.DiscordWebHooks.GetByUrl(c.Request().Context(), _url) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dtos []domain.DiscordWebHookDto @@ -176,16 +191,20 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { } // DisableDiscordWebHooks -// @Summary Disables a Webhook from being used. -// @Param id path int true "id" -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks/{ID}/disable [post] -// @Success 200 {object} domain.DiscordWebhookResponse "OK" -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Summary Disables a Webhook from being used. +// @Param id path int true "id" +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks/{ID}/disable [post] +// @Success 200 {object} domain.DiscordWebhookResponse "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) disableDiscordWebHook(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + id, err := strconv.Atoi(c.Param("ID")) if err != nil { return c.JSON(http.StatusBadRequest, domain.BaseResponse{ @@ -196,27 +215,27 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { // Check to make sure we can find the record record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if record.UserID != s.GetUserIdFromJwtToken(c) { - s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + return s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) } // flip the it updated, err := s.repo.DiscordWebHooks.Disable(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } // make sure we got a row updated if updated != 1 { - s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) + return s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) } item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dtos []domain.DiscordWebHookDto @@ -230,40 +249,44 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { } // EnableDiscordWebHook -// @Summary Enables a source to continue processing. -// @Param id path int true "id" -// @Tags DiscordWebhook -// @Router /v1/discord/webhooks/{ID}/enable [post] +// @Summary Enables a source to continue processing. +// @Param id path int true "id" +// @Tags DiscordWebhook +// @Router /v1/discord/webhooks/{ID}/enable [post] // @Security Bearer func (s *Handler) enableDiscordWebHook(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } if record.UserID != s.GetUserIdFromJwtToken(c) { - s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + return s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) } updated, err := s.repo.DiscordWebHooks.Enable(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if updated != 1 { - s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError) } item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dtos []domain.DiscordWebHookDto @@ -285,7 +308,11 @@ func (s *Handler) enableDiscordWebHook(c echo.Context) error { // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse func (s *Handler) deleteDiscordWebHook(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + id, err := strconv.Atoi(c.Param("ID")) if err != nil { return c.JSON(http.StatusBadRequest, err.Error()) @@ -298,7 +325,7 @@ func (s *Handler) deleteDiscordWebHook(c echo.Context) error { } if record.UserID != s.GetUserIdFromJwtToken(c) { - s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) + return s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest) } // Soft delete the record @@ -308,12 +335,12 @@ func (s *Handler) deleteDiscordWebHook(c echo.Context) error { } if updated != 1 { - s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError) } item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dtos []domain.DiscordWebHookDto diff --git a/internal/handler/v1/handler.go b/internal/handler/v1/handler.go index d8369f0..d3e76ad 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -3,6 +3,7 @@ package v1 import ( "context" "database/sql" + "errors" "net/http" "github.com/golang-jwt/jwt/v5" @@ -115,14 +116,14 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han return s } -type ApiStatusModel struct { - StatusCode int `json:"status"` - Message string `json:"message"` -} +//type ApiStatusModel struct { +// StatusCode int `json:"status"` +// Message string `json:"message"` +//} -type ApiError struct { - *ApiStatusModel -} +//type ApiError struct { +// *ApiStatusModel +//} func (s *Handler) WriteError(c echo.Context, errMessage error, HttpStatusCode int) error { return c.JSON(HttpStatusCode, domain.BaseResponse{ @@ -151,27 +152,30 @@ func (s *Handler) UnauthorizedResponse(c echo.Context, msg string) error { // 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 { +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 { - s.WriteMessage(c, ErrJwtScopeMissing, http.StatusUnauthorized) + return JwtToken{}, errors.New(ErrJwtScopeMissing) + //s.WriteMessage(c, ErrJwtScopeMissing, http.StatusUnauthorized) } if token.Iss != s.config.ServerAddress { - s.WriteMessage(c, ErrJwtInvalidIssuer, http.StatusUnauthorized) + return JwtToken{}, errors.New(ErrJwtInvalidIssuer) + //s.WriteMessage(c, ErrJwtInvalidIssuer, http.StatusUnauthorized) } - err = token.hasExpired() - if err != nil { - s.WriteMessage(c, ErrJwtExpired, http.StatusUnauthorized) - } - - return token + return token, nil } func (s *Handler) GetUserIdFromJwtToken(c echo.Context) int64 { diff --git a/internal/handler/v1/jwt.go b/internal/handler/v1/jwt.go index 22aaa74..1a8d565 100644 --- a/internal/handler/v1/jwt.go +++ b/internal/handler/v1/jwt.go @@ -59,8 +59,9 @@ func (j JwtToken) GetUserId() int64 { func (j JwtToken) hasExpired() error { // Check to see if the token has expired - hasExpired := j.Exp.Compare(time.Now()) - if hasExpired == -1 { + //hasExpired := j.Exp.Compare(time.Now()) + hasExpired := time.Now().Compare(j.Exp) + if hasExpired == 1 { return errors.New(ErrJwtExpired) } return nil @@ -82,11 +83,11 @@ func (j JwtToken) hasScope(scope string) error { return errors.New(ErrJwtScopeMissing) } -func (h *Handler) generateJwt(username, scopes, issuer string, userId int64) (string, error) { - return h.generateJwtWithExp(username, scopes, issuer, userId, time.Now().Add(10*time.Minute)) +func (h *Handler) generateJwt(username, issuer string, userScopes []string, userId int64) (string, error) { + return h.generateJwtWithExp(username, issuer, userScopes, userId, time.Now().Add(10*time.Minute)) } -func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId int64, expiresAt time.Time) (string, error) { +func (h *Handler) generateJwtWithExp(username, issuer string, userScopes []string, userId int64, expiresAt time.Time) (string, error) { secret := []byte(h.config.JwtSecret) // Anyone who wants to decrypt the key needs to use the same method @@ -99,7 +100,7 @@ func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId claims["userId"] = userId var scopes []string - scopes = append(scopes, domain.ScopeAll) + scopes = append(scopes, userScopes...) claims["scopes"] = scopes tokenString, err := token.SignedString(secret) diff --git a/internal/handler/v1/sources.go b/internal/handler/v1/sources.go index f5f1543..000cfde 100644 --- a/internal/handler/v1/sources.go +++ b/internal/handler/v1/sources.go @@ -21,7 +21,11 @@ import ( // @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" // @Security Bearer func (s *Handler) listSources(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceRead) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -36,7 +40,7 @@ func (s *Handler) listSources(c echo.Context) error { // Default way of showing all sources items, err := s.repo.Sources.List(c.Request().Context(), page, 25) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } resp.Payload = services.SourcesToDto(items) @@ -55,7 +59,11 @@ func (s *Handler) listSources(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) listSourcesBySource(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceRead) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -64,7 +72,7 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { source := c.QueryParam("source") if source == "" { - s.WriteMessage(c, fmt.Sprintf("%s source", ErrParameterMissing), http.StatusBadRequest) + return s.WriteMessage(c, fmt.Sprintf("%s source", ErrParameterMissing), http.StatusBadRequest) } page, err := strconv.Atoi(c.QueryParam("page")) @@ -95,7 +103,11 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) getSource(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceRead) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -111,7 +123,7 @@ func (s *Handler) getSource(c echo.Context) error { item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dto []domain.SourceDto @@ -132,7 +144,11 @@ func (s *Handler) getSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceRead) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -140,7 +156,7 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { } var param domain.GetSourceBySourceAndNameParamRequest - err := c.Bind(¶m) + err = c.Bind(¶m) if err != nil { return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), @@ -169,7 +185,10 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) newRedditSource(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -178,30 +197,30 @@ func (s *Handler) newRedditSource(c echo.Context) error { } var param domain.NewSourceParamRequest - err := c.Bind(¶m) + err = c.Bind(¶m) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } if param.Url == "" { - s.WriteMessage(c, "url is missing", http.StatusBadRequest) + return s.WriteMessage(c, "url is missing", http.StatusBadRequest) } if !strings.Contains(param.Url, "reddit.com") { - s.WriteMessage(c, "invalid url", http.StatusBadRequest) + return s.WriteMessage(c, "invalid url", http.StatusBadRequest) } tags := fmt.Sprintf("twitch, %v, %s", param.Name, param.Tags) rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorReddit, param.Name, param.Url, tags, true) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if rows != 1 { - s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorReddit, param.Name) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dto []domain.SourceDto @@ -219,18 +238,21 @@ func (s *Handler) newRedditSource(c echo.Context) error { // @Security Bearer func (s *Handler) newYoutubeSource(c echo.Context) error { // Validate the jwt - s.ValidateJwtToken(c, domain.ScopeSourceCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } var param domain.NewSourceParamRequest - err := c.Bind(¶m) + err = c.Bind(¶m) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } if param.Url == "" { - s.WriteMessage(c, "url is missing a value", http.StatusBadRequest) + return s.WriteMessage(c, "url is missing a value", http.StatusBadRequest) } if !strings.Contains(param.Url, "youtube.com") { - s.WriteMessage(c, "invalid url", http.StatusBadRequest) + return s.WriteMessage(c, "invalid url", http.StatusBadRequest) } resp := domain.SourcesResponse{ @@ -254,7 +276,7 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { } if rows != 1 { - s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name) @@ -275,10 +297,13 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { // @Router /v1/sources/new/twitch [post] // @Security Bearer func (s *Handler) newTwitchSource(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } var param domain.NewSourceParamRequest - err := c.Bind(¶m) + err = c.Bind(¶m) if err != nil { return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), @@ -311,10 +336,10 @@ func (s *Handler) newTwitchSource(c echo.Context) error { } if rows != 1 { - s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } - item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name) + item, _ = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name) var dto []domain.SourceDto dto = append(dto, services.SourceToDto(item)) resp.Payload = dto @@ -333,7 +358,10 @@ func (s *Handler) newTwitchSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) newRssSource(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeSourceCreate) + _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ @@ -342,7 +370,7 @@ func (s *Handler) newRssSource(c echo.Context) error { } var param domain.NewSourceParamRequest - err := c.Bind(¶m) + err = c.Bind(¶m) if err != nil { return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), @@ -358,16 +386,16 @@ func (s *Handler) newRssSource(c echo.Context) error { tags := fmt.Sprintf("rss, %v, %s", param.Name, param.Tags) rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorRss, param.Name, param.Url, tags, true) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if rows != 1 { - s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) } item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorRss, param.Name) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dto []domain.SourceDto @@ -386,31 +414,35 @@ func (s *Handler) newRssSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) deleteSources(c echo.Context) error { - s.ValidateJwtToken(c, domain.ScopeAll) + _, err := s.ValidateJwtToken(c, domain.ScopeAll) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } // Delete the record rows, err := s.repo.Sources.SoftDelete(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } if rows != 1 { - s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError) + return s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError) } // pull the record with its updated value item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var items []domain.SourceDto @@ -434,6 +466,11 @@ func (s *Handler) deleteSources(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) disableSource(c echo.Context) error { + _, err := s.ValidateJwtToken(c, domain.ScopeAll) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -442,23 +479,23 @@ func (s *Handler) disableSource(c echo.Context) error { id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } _, err = s.repo.Sources.Disable(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dto []domain.SourceDto @@ -477,6 +514,11 @@ func (s *Handler) disableSource(c echo.Context) error { // @Failure 500 {object} domain.BaseResponse // @Security Bearer func (s *Handler) enableSource(c echo.Context) error { + _, err := s.ValidateJwtToken(c, domain.ScopeAll) + if err != nil { + return s.WriteError(c, err, http.StatusBadRequest) + } + resp := domain.SourcesResponse{ BaseResponse: domain.BaseResponse{ Message: ResponseMessageSuccess, @@ -485,23 +527,23 @@ func (s *Handler) enableSource(c echo.Context) error { id, err := strconv.Atoi(c.Param("ID")) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // Check to make sure we can find the record _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } _, err = s.repo.Sources.Enable(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) if err != nil { - s.WriteError(c, err, http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } var dto []domain.SourceDto -- 2.45.2 From c0fb43df7d6c30007158c3ff301f47805c466248 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 7 May 2024 22:10:17 -0700 Subject: [PATCH 09/10] More testing done around jwt and things are looking ok. Should be able to work on the portal now some. --- .vscode/launch.json | 15 --------- docs/docs.go | 63 +++++++++++++++++++++++++++++++++++-- docs/swagger.json | 63 +++++++++++++++++++++++++++++++++++-- docs/swagger.yaml | 32 +++++++++++++++++-- internal/handler/v1/auth.go | 38 +++++++++++----------- 5 files changed, 172 insertions(+), 39 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f944eb2..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "." - } - ] -} \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 58e97ea..f7ce7b5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -203,6 +203,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -234,6 +239,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks/by/serverAndChannel": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -281,6 +291,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks/new": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -369,6 +384,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks/{ID}/disable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -406,6 +426,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks/{ID}/enable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -424,6 +449,11 @@ const docTemplate = `{ }, "/v1/discord/webhooks/{id}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -810,14 +840,33 @@ const docTemplate = `{ "summary": "Marks a source as deleted based on its ID value.", "parameters": [ { - "type": "string", + "type": "integer", "description": "id", "name": "id", "in": "path", "required": true } ], - "responses": {} + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/domain.SourcesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } } }, "/v1/sources/{id}/disable": { @@ -1036,6 +1085,11 @@ const docTemplate = `{ }, "/v1/users/scopes/add": { "post": { + "security": [ + { + "Bearer": [] + } + ], "consumes": [ "application/json" ], @@ -1081,6 +1135,11 @@ const docTemplate = `{ }, "/v1/users/scopes/remove": { "post": { + "security": [ + { + "Bearer": [] + } + ], "consumes": [ "application/json" ], diff --git a/docs/swagger.json b/docs/swagger.json index 2e61798..89172d1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -194,6 +194,11 @@ }, "/v1/discord/webhooks": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -225,6 +230,11 @@ }, "/v1/discord/webhooks/by/serverAndChannel": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -272,6 +282,11 @@ }, "/v1/discord/webhooks/new": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -360,6 +375,11 @@ }, "/v1/discord/webhooks/{ID}/disable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -397,6 +417,11 @@ }, "/v1/discord/webhooks/{ID}/enable": { "post": { + "security": [ + { + "Bearer": [] + } + ], "tags": [ "DiscordWebhook" ], @@ -415,6 +440,11 @@ }, "/v1/discord/webhooks/{id}": { "get": { + "security": [ + { + "Bearer": [] + } + ], "produces": [ "application/json" ], @@ -801,14 +831,33 @@ "summary": "Marks a source as deleted based on its ID value.", "parameters": [ { - "type": "string", + "type": "integer", "description": "id", "name": "id", "in": "path", "required": true } ], - "responses": {} + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/domain.SourcesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.BaseResponse" + } + } + } } }, "/v1/sources/{id}/disable": { @@ -1027,6 +1076,11 @@ }, "/v1/users/scopes/add": { "post": { + "security": [ + { + "Bearer": [] + } + ], "consumes": [ "application/json" ], @@ -1072,6 +1126,11 @@ }, "/v1/users/scopes/remove": { "post": { + "security": [ + { + "Bearer": [] + } + ], "consumes": [ "application/json" ], diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a60e835..9eddc06 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -269,6 +269,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns the top 100 tags: - DiscordWebhook @@ -317,6 +319,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Disables a Webhook from being used. tags: - DiscordWebhook @@ -329,6 +333,8 @@ paths: required: true type: integer responses: {} + security: + - Bearer: [] summary: Enables a source to continue processing. tags: - DiscordWebhook @@ -355,6 +361,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns the top 100 entries from the queue to be processed. tags: - DiscordWebhook @@ -386,6 +394,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Returns all the known web hooks based on the Server and Channel given. tags: - DiscordWebhook @@ -420,6 +430,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Creates a new record for a discord web hook to post data to. tags: - DiscordWebhook @@ -480,8 +492,20 @@ paths: in: path name: id required: true - type: string - responses: {} + type: integer + responses: + "200": + description: ok + schema: + $ref: '#/definitions/domain.SourcesResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.BaseResponse' security: - Bearer: [] summary: Marks a source as deleted based on its ID value. @@ -806,6 +830,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Adds a new scope to a user account tags: - Users @@ -835,6 +861,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.BaseResponse' + security: + - Bearer: [] summary: Adds a new scope to a user account tags: - Users diff --git a/internal/handler/v1/auth.go b/internal/handler/v1/auth.go index 9320cb6..f73f57e 100644 --- a/internal/handler/v1/auth.go +++ b/internal/handler/v1/auth.go @@ -20,9 +20,9 @@ const ( // @Router /v1/users/register [post] // @Param request formData domain.LoginFormRequest true "form" // @Accepts x-www-form-urlencoded -// @Produce json +// @Produce json // @Tags Users -// @Success 200 {object} domain.BaseResponse +// @Success 200 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse func (h *Handler) AuthRegister(c echo.Context) error { @@ -62,11 +62,11 @@ func (h *Handler) AuthRegister(c echo.Context) error { // @Router /v1/users/login [post] // @Param request formData domain.LoginFormRequest true "form" // @Accepts x-www-form-urlencoded -// @Produce json -// @Tags Users +// @Produce json +// @Tags Users // @Success 200 {object} domain.LoginResponse -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (h *Handler) AuthLogin(c echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") @@ -144,10 +144,10 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error { // @Summary Generates a new token // @Router /v1/users/refreshToken [post] // @Param request body domain.RefreshTokenRequest true "body" -// @Tags Users +// @Tags Users // @Success 200 {object} domain.LoginResponse -// @Failure 400 {object} domain.BaseResponse -// @Failure 500 {object} domain.BaseResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse // @Security Bearer func (h *Handler) RefreshJwtToken(c echo.Context) error { _, err := h.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate) @@ -193,15 +193,16 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error { }) } -// @Summary Adds a new scope to a user account -// @Router /v1/users/scopes/add [post] -// @Param request body domain.UpdateScopesRequest true "body" +// @Summary Adds a new scope to a user account +// @Router /v1/users/scopes/add [post] +// @Param request body domain.UpdateScopesRequest true "body" // @Tags Users -// @Accept json +// @Accept json // @Produce json -// @Success 200 {object} domain.BaseResponse +// @Success 200 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (h *Handler) AddScopes(c echo.Context) error { _, err := h.ValidateJwtToken(c, domain.ScopeAll) if err != nil { @@ -224,15 +225,16 @@ func (h *Handler) AddScopes(c echo.Context) error { }) } -// @Summary Adds a new scope to a user account -// @Router /v1/users/scopes/remove [post] -// @Param request body domain.UpdateScopesRequest true "body" +// @Summary Adds a new scope to a user account +// @Router /v1/users/scopes/remove [post] +// @Param request body domain.UpdateScopesRequest true "body" // @Tags Users -// @Accept json +// @Accept json // @Produce json // @Success 200 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse +// @Security Bearer func (h *Handler) RemoveScopes(c echo.Context) error { token, err := h.getJwtTokenFromContext(c) if err != nil { -- 2.45.2 From e38643938a322583864e097ceac83aee7764a0e8 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Tue, 7 May 2024 22:20:50 -0700 Subject: [PATCH 10/10] dumb rename and attempting to get CI working --- .drone.yaml | 60 +++++++++++++++++++ .../refreshTokens.go | 2 +- .../userService.go | 2 +- internal/services/database.go | 10 ++-- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 .drone.yaml rename internal/{respositoryServices => repositoryServices}/refreshTokens.go (98%) rename internal/{respositoryServices => repositoryServices}/userService.go (99%) diff --git a/.drone.yaml b/.drone.yaml new file mode 100644 index 0000000..3a51629 --- /dev/null +++ b/.drone.yaml @@ -0,0 +1,60 @@ +--- +kind: pipeline +type: docker +name: buildLatestImage + +steps: + - name: buildLatestImage + image: plugins/docker + settings: + repo: jtom38/newsbot-collector + username: jtom38 + password: + from_secret: DockerPushPat +trigger: + branch: + include: + - main + + event: + exclude: + - pull_request +--- +kind: pipeline +type: docker +name: buildReleaseImage + +steps: + - name: buildReleaseImage + image: plugins/docker + settings: + repo: jtom38/newsbot-collector + username: jtom38 + password: + from_secret: DockerPushPat +trigger: + branch: + include: + - releases/* + ref: + include: + - refs/tags/** + event: + exclude: + - pull_request + + +--- +kind: pipeline +type: docker +name: PullRequestCompileTest +steps: + - name: Compile project + image: golang:1.22 + commands: + - go test ./internal/repository + - go build ./cmd/server.go + - +trigger: + event: + - pull_request \ No newline at end of file diff --git a/internal/respositoryServices/refreshTokens.go b/internal/repositoryServices/refreshTokens.go similarity index 98% rename from internal/respositoryServices/refreshTokens.go rename to internal/repositoryServices/refreshTokens.go index ddd6033..ca5b800 100644 --- a/internal/respositoryServices/refreshTokens.go +++ b/internal/repositoryServices/refreshTokens.go @@ -1,4 +1,4 @@ -package respositoryservices +package repositoryservices import ( "context" diff --git a/internal/respositoryServices/userService.go b/internal/repositoryServices/userService.go similarity index 99% rename from internal/respositoryServices/userService.go rename to internal/repositoryServices/userService.go index ea02f51..7a4bed2 100644 --- a/internal/respositoryServices/userService.go +++ b/internal/repositoryServices/userService.go @@ -1,4 +1,4 @@ -package respositoryservices +package repositoryservices import ( "context" diff --git a/internal/services/database.go b/internal/services/database.go index 900c1a0..94a1506 100644 --- a/internal/services/database.go +++ b/internal/services/database.go @@ -4,16 +4,16 @@ import ( "database/sql" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" - repositoryservice "git.jamestombleson.com/jtom38/newsbot-api/internal/respositoryServices" + repositoryservices "git.jamestombleson.com/jtom38/newsbot-api/internal/repositoryServices" ) type RepositoryService struct { AlertDiscord repository.AlertDiscordRepo Articles repository.ArticlesRepo DiscordWebHooks repository.DiscordWebHookRepo - RefreshTokens repositoryservice.RefreshToken + RefreshTokens repositoryservices.RefreshToken Sources repository.Sources - Users repositoryservice.UserServices + Users repositoryservices.UserServices UserSourceSubscriptions repository.UserSourceRepo } @@ -22,9 +22,9 @@ func NewRepositoryService(conn *sql.DB) RepositoryService { AlertDiscord: repository.NewAlertDiscordRepository(conn), Articles: repository.NewArticleRepository(conn), DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), - RefreshTokens: repositoryservice.NewRefreshTokenService(conn), + RefreshTokens: repositoryservices.NewRefreshTokenService(conn), Sources: repository.NewSourceRepository(conn), - Users: repositoryservice.NewUserService(conn), + Users: repositoryservices.NewUserService(conn), UserSourceSubscriptions: repository.NewUserSourceRepository(conn), } } -- 2.45.2