diff --git a/.golangci.yml b/.golangci.yml index b90195e..9014132 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,16 +19,16 @@ linters: - errcheck - errchkjson - errname -# - errorlint -# - exhaustive -# - fatcontext -# - forbidigo -# - forcetypeassert -# - funlen -# - ginkgolinter -# - gocheckcompilerdirectives -# - gochecknoinits -# - gochecksumtype + - errorlint + - exhaustive + - fatcontext + - forbidigo + - forcetypeassert + - funlen + - ginkgolinter + - gocheckcompilerdirectives + - gochecknoinits + - gochecksumtype # - gocognit # - goconst # - gocritic @@ -103,6 +103,10 @@ linters: - third_party$ - builtin$ - examples$ + rules: + - path: cmd/migrate/main.go + linters: + - forbidigo formatters: enable: - gofmt diff --git a/e2e-tests/http.go b/e2e-tests/http.go index ccf1106..2928880 100644 --- a/e2e-tests/http.go +++ b/e2e-tests/http.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base64" "encoding/json" - "fmt" "io" "net/http" "net/url" @@ -123,7 +122,7 @@ func doRequest(t *testing.T, base, uri, method string, payload []byte, resp inte response, err := http.DefaultClient.Do(request) require.NoError(t, err) - logRequest(request, response) + logRequest(t, request, response) rawBody, err := io.ReadAll(response.Body) require.NoError(t, err) @@ -152,14 +151,15 @@ func createRequest(method, uri string, payload []byte) *http.Request { return request } -func logRequest(req *http.Request, resp *http.Response) { +func logRequest(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() if DebugHttpRequests { rawBody, _ := io.ReadAll(resp.Body) resp.Body.Close() resp.Body = io.NopCloser(bytes.NewBuffer(rawBody)) - fmt.Printf("Request: %s: %s %v \n", req.Method, req.URL.RequestURI(), req.Body) - fmt.Println("Response: ", req.URL.RequestURI(), resp.StatusCode, string(rawBody)) + t.Logf("Request: %s: %s %v", req.Method, req.URL.RequestURI(), req.Body) + t.Log("Response: ", req.URL.RequestURI(), resp.StatusCode, string(rawBody)) } } diff --git a/internal/api/browser_extension/app/command/request_2fa_token.go b/internal/api/browser_extension/app/command/request_2fa_token.go index 2ac3334..e9304e4 100644 --- a/internal/api/browser_extension/app/command/request_2fa_token.go +++ b/internal/api/browser_extension/app/command/request_2fa_token.go @@ -77,21 +77,11 @@ func (h *Request2FaTokenHandler) Handle(ctx context.Context, cmd *Request2FaToke log := logging.FromContext(ctx) extId, _ := uuid.Parse(cmd.ExtensionId) - browserExtension, err := h.BrowserExtensionsRepository.FindById(extId) + pairedDevices, err := h.findPairedDevices(extId, cmd) if err != nil { return nil, err } - tokenRequestId, _ := uuid.Parse(cmd.Id) - browserExtension2FaRequest := domain.NewBrowserExtension2FaRequest(tokenRequestId, browserExtension.Id, cmd.Domain) - - err = h.BrowserExtension2FaRequestRepository.Save(browserExtension2FaRequest) - if err != nil { - return nil, err - } - - pairedDevices := h.PairedDevicesRepository.FindAll(browserExtension.Id) - data := map[string]interface{}{ "extension_id": extId.String(), "request_id": cmd.Id, @@ -115,23 +105,7 @@ func (h *Request2FaTokenHandler) Handle(ctx context.Context, cmd *Request2FaToke continue } - var err error - var notification *messaging.Message - - switch device.Platform { - case domain.Android: - notification = createPushNotificationForAndroid(device.FcmToken, data) - case domain.IOS: - notification = createPushNotificationForIos(device.FcmToken, data) - } - - err = retry.Do( - func() error { - return h.Pusher.Send(ctx, notification) - }, - retry.Attempts(5), - retry.LastErrorOnly(true), - ) + err := h.sendNotification(ctx, device, data) if err == nil { result[device.Id.String()] = PushNotificationStatusOK } else if messaging.IsUnregistered(err) { @@ -153,6 +127,46 @@ func (h *Request2FaTokenHandler) Handle(ctx context.Context, cmd *Request2FaToke return result, nil } +func (h *Request2FaTokenHandler) findPairedDevices(extId uuid.UUID, cmd *Request2FaToken) ([]*domain.ExtensionDevice, error) { + browserExtension, err := h.BrowserExtensionsRepository.FindById(extId) + if err != nil { + return nil, err + } + + tokenRequestId, err := uuid.Parse(cmd.Id) + if err != nil { + return nil, fmt.Errorf("failed to parse token request id: %w", err) + } + + browserExtension2FaRequest := domain.NewBrowserExtension2FaRequest(tokenRequestId, browserExtension.Id, cmd.Domain) + + err = h.BrowserExtension2FaRequestRepository.Save(browserExtension2FaRequest) + if err != nil { + return nil, err + } + + return h.PairedDevicesRepository.FindAll(browserExtension.Id), nil +} + +func (h *Request2FaTokenHandler) sendNotification(ctx context.Context, device *domain.ExtensionDevice, data map[string]interface{}) error { + var notification *messaging.Message + + switch device.Platform { + case domain.Android: + notification = createPushNotificationForAndroid(device.FcmToken, data) + case domain.IOS: + notification = createPushNotificationForIos(device.FcmToken, data) + } + + return retry.Do( + func() error { + return h.Pusher.Send(ctx, notification) + }, + retry.Attempts(5), + retry.LastErrorOnly(true), + ) +} + func createPushNotificationForIos(token string, data map[string]interface{}) *messaging.Message { ttl := time.Now().Add(tokenPushNotificationTtl) diff --git a/internal/api/browser_extension/service/service.go b/internal/api/browser_extension/service/service.go index 2f85e09..5e1386d 100644 --- a/internal/api/browser_extension/service/service.go +++ b/internal/api/browser_extension/service/service.go @@ -28,7 +28,7 @@ type BrowserExtensionModule struct { Config config.Configuration } -func NewBrowserExtensionModule( +func NewBrowserExtensionModule( //nolint:funlen // This is an initialization function. config config.Configuration, gorm *gorm.DB, database *sql.DB, diff --git a/internal/api/icons/app/command/icons_requests.go b/internal/api/icons/app/command/icons_requests.go index b5f6610..77ef05a 100644 --- a/internal/api/icons/app/command/icons_requests.go +++ b/internal/api/icons/app/command/icons_requests.go @@ -148,78 +148,9 @@ func (h *UpdateWebServiceFromIconRequestHandler) Handle(cmd *UpdateWebServiceFro return err } - lightIconStoragePath := filepath.Join(iconsStoragePath, filepath.Base(iconRequest.LightIconUrl)) - - lightIconImg, err := h.IconsStorage.Get(lightIconStoragePath) - if err != nil { - return fmt.Errorf("failed to get the icon from the storage: %w", err) - } - - lightIconPng, err := png.Decode(lightIconImg) + iconsIds, err := saveIcons(iconRequest, h.IconsStorage, h.IconsRepository) if err != nil { - return fmt.Errorf("failed to decode the icon as pgn: %w", err) - } - - lightIconId := uuid.New() - lightIconNewPath := filepath.Join(iconsStoragePath, lightIconId.String()+".png") - newLightIconLocation, err := h.IconsStorage.Move(lightIconStoragePath, lightIconNewPath) - if err != nil { - return fmt.Errorf("failed to move icons storage: %w", err) - } - - lightIcon := &domain.Icon{ - Id: lightIconId, - Name: iconRequest.ServiceName, - Url: newLightIconLocation, - Width: lightIconPng.Bounds().Dx(), - Height: lightIconPng.Bounds().Dy(), - Type: domain.Light, - } - - err = h.IconsRepository.Save(lightIcon) - if err != nil { - return fmt.Errorf("failed to save light icon: %w", err) - } - - iconsIds := []string{ - lightIcon.Id.String(), - } - - if iconRequest.DarkIconUrl != "" { //nolint:dupl - darkIconStoragePath := filepath.Join(iconsStoragePath, filepath.Base(iconRequest.DarkIconUrl)) - - darkIconImg, err := h.IconsStorage.Get(darkIconStoragePath) - if err != nil { - return fmt.Errorf("failed to get dark icon: %w", err) - } - - darkIconPng, err := png.Decode(darkIconImg) - if err != nil { - return fmt.Errorf("failed to decode dark icon: %w", err) - } - - darkIconId := uuid.New() - darkIconNewPath := filepath.Join(iconsStoragePath, darkIconId.String()+".png") - newDarkIconLocation, err := h.IconsStorage.Move(darkIconStoragePath, darkIconNewPath) - if err != nil { - return fmt.Errorf("failed to move dark icon: %w", err) - } - - darkIcon := &domain.Icon{ - Id: darkIconId, - Name: iconRequest.ServiceName, - Url: newDarkIconLocation, - Width: darkIconPng.Bounds().Dx(), - Height: darkIconPng.Bounds().Dy(), - Type: domain.Dark, - } - - err = h.IconsRepository.Save(darkIcon) - if err != nil { - return fmt.Errorf("failed to save dark icon: %w", err) - } - - iconsIds = append(iconsIds, darkIconId.String()) + return err } iconsJson, err := json.Marshal(iconsIds) @@ -255,6 +186,64 @@ func (h *UpdateWebServiceFromIconRequestHandler) Handle(cmd *UpdateWebServiceFro return nil } +func saveIcons(iconRequest *domain.IconRequest, iconsStorage storage.FileSystemStorage, iconsRepository domain.IconsRepository) ([]string, error) { + lightIconID, err := saveIcon(iconRequest.LightIconUrl, iconRequest.ServiceName, domain.Light, iconsStorage, iconsRepository) + if err != nil { + return nil, fmt.Errorf("failed to save light icon: %w", err) + } + + iconsIds := []string{ + lightIconID.String(), + } + + if iconRequest.DarkIconUrl != "" { //nolint:dupl + darkIconId, err := saveIcon(iconRequest.DarkIconUrl, iconRequest.ServiceName, domain.Dark, iconsStorage, iconsRepository) + if err != nil { + return nil, fmt.Errorf("failed to save dark icon: %w", err) + } + + iconsIds = append(iconsIds, darkIconId.String()) + } + return iconsIds, nil +} + +func saveIcon(iconURL, serviceName, iconType string, iconsStorage storage.FileSystemStorage, iconsRepository domain.IconsRepository) (uuid.UUID, error) { + iconFileName := filepath.Base(iconURL) + storagePath := filepath.Join(iconsStoragePath, iconFileName) + + iconImg, err := iconsStorage.Get(storagePath) + if err != nil { + return uuid.UUID{}, fmt.Errorf("failed to get icon: %w", err) + } + + iconPng, err := png.Decode(iconImg) + if err != nil { + return uuid.UUID{}, fmt.Errorf("failed to decode icon: %w", err) + } + + iconID := uuid.New() + newPath := filepath.Join(iconsStoragePath, iconID.String()+".png") + newLocation, err := iconsStorage.Move(storagePath, newPath) + if err != nil { + return uuid.UUID{}, fmt.Errorf("failed to move icon: %w", err) + } + + icon := &domain.Icon{ + Id: iconID, + Name: serviceName, + Url: newLocation, + Width: iconPng.Bounds().Dx(), + Height: iconPng.Bounds().Dy(), + Type: iconType, + } + + err = iconsRepository.Save(icon) + if err != nil { + return uuid.UUID{}, fmt.Errorf("failed to save icon: %w", err) + } + return iconID, nil +} + func (h *UpdateWebServiceFromIconRequestHandler) updateIconsCollection(iconsCollectionID string, name string, iconsJson []byte) error { id, err := uuid.Parse(iconsCollectionID) if err != nil { @@ -362,91 +351,13 @@ func (h *TransformIconRequestToWebServiceHandler) Handle(cmd *TransformIconReque return err } - _, err = h.WebServiceRepository.FindByName(iconRequest.ServiceName) - if err == nil { - return domain.WebServiceAlreadyExistsError{Name: iconRequest.ServiceName} - } else { - var notFound adapters.WebServiceCouldNotBeFoundError - if !errors.As(err, ¬Found) { - fmt.Printf("Error is: %T %+v\n", err, err) - return fmt.Errorf("failed to find web service by name: %w", err) - } - } - - iconsCollectionId := uuid.New() - - lightIconStoragePath := filepath.Join(iconsStoragePath, filepath.Base(iconRequest.LightIconUrl)) - - lightIconImg, err := h.IconsStorage.Get(lightIconStoragePath) - if err != nil { - return fmt.Errorf("failed to get light icon: %w", err) - } - - lightIconPng, err := png.Decode(lightIconImg) - if err != nil { - return fmt.Errorf("failed to decode light icon: %w", err) - } - - lightIconId := uuid.New() - lightIconNewPath := filepath.Join(iconsStoragePath, lightIconId.String()+".png") - newLightIconLocation, err := h.IconsStorage.Move(lightIconStoragePath, lightIconNewPath) - if err != nil { - return fmt.Errorf("failed to move light icon: %w", err) - } - - lightIcon := &domain.Icon{ - Id: lightIconId, - Name: iconRequest.ServiceName, - Url: newLightIconLocation, - Width: lightIconPng.Bounds().Dx(), - Height: lightIconPng.Bounds().Dy(), - Type: domain.Light, + if err := h.checkServiceDoesNotExist(iconRequest); err != nil { + return err } - err = h.IconsRepository.Save(lightIcon) + iconsIds, err := saveIcons(iconRequest, h.IconsStorage, h.IconsRepository) if err != nil { - return fmt.Errorf("failed to save light icon: %w", err) - } - - iconsIds := []string{ - lightIcon.Id.String(), - } - - if iconRequest.DarkIconUrl != "" { //nolint:dupl - darkIconStoragePath := filepath.Join(iconsStoragePath, filepath.Base(iconRequest.DarkIconUrl)) - - darkIconImg, err := h.IconsStorage.Get(darkIconStoragePath) - if err != nil { - return fmt.Errorf("failed to get dark icon: %w", err) - } - - darkIconPng, err := png.Decode(darkIconImg) - if err != nil { - return fmt.Errorf("failed to decode dark icon: %w", err) - } - - darkIconId := uuid.New() - darkIconNewPath := filepath.Join(iconsStoragePath, darkIconId.String()+".png") - newDarkIconLocation, err := h.IconsStorage.Move(darkIconStoragePath, darkIconNewPath) - if err != nil { - return fmt.Errorf("failed to move dark icon: %w", err) - } - - darkIcon := &domain.Icon{ - Id: darkIconId, - Name: iconRequest.ServiceName, - Url: newDarkIconLocation, - Width: darkIconPng.Bounds().Dx(), - Height: darkIconPng.Bounds().Dy(), - Type: domain.Dark, - } - - err = h.IconsRepository.Save(darkIcon) - if err != nil { - return fmt.Errorf("failed to save dark icon: %w", err) - } - - iconsIds = append(iconsIds, darkIconId.String()) + return err } iconsJson, err := json.Marshal(iconsIds) @@ -454,6 +365,7 @@ func (h *TransformIconRequestToWebServiceHandler) Handle(cmd *TransformIconReque return fmt.Errorf("failed to encode icon ids: %w", err) } + iconsCollectionId := uuid.New() iconsCollection := &domain.IconsCollection{ Id: iconsCollectionId, Name: iconRequest.ServiceName, @@ -486,3 +398,17 @@ func (h *TransformIconRequestToWebServiceHandler) Handle(cmd *TransformIconReque return nil } + +func (h *TransformIconRequestToWebServiceHandler) checkServiceDoesNotExist(iconRequest *domain.IconRequest) error { + _, err := h.WebServiceRepository.FindByName(iconRequest.ServiceName) + if err == nil { + // No error means service was found, which is an error in this case. + return domain.WebServiceAlreadyExistsError{Name: iconRequest.ServiceName} + } + // We got an error, but we need to make sure it is "not found" error, not something else. + var notFound adapters.WebServiceCouldNotBeFoundError + if !errors.As(err, ¬Found) { + return fmt.Errorf("failed to find web service by name: %w", err) + } + return nil +} diff --git a/internal/api/icons/service/service.go b/internal/api/icons/service/service.go index f7a4bb2..6528093 100644 --- a/internal/api/icons/service/service.go +++ b/internal/api/icons/service/service.go @@ -24,7 +24,7 @@ type IconsModule struct { Config config.Configuration } -func NewIconsModule(config config.Configuration, gorm *gorm.DB, database *sql.DB, validate *validator.Validate, iconsStorage storage.FileSystemStorage) *IconsModule { +func NewIconsModule(config config.Configuration, gorm *gorm.DB, database *sql.DB, validate *validator.Validate, iconsStorage storage.FileSystemStorage) *IconsModule { //nolint:funlen // This is an initialization function. queryBuilder := db.NewQueryBuilder(database) webServicesRepository := adapters.NewWebServiceMysqlRepository(gorm) diff --git a/internal/api/mobile/service/service.go b/internal/api/mobile/service/service.go index f372ae8..8a9cb8b 100644 --- a/internal/api/mobile/service/service.go +++ b/internal/api/mobile/service/service.go @@ -30,7 +30,7 @@ type MobileModule struct { Redis *redis.Client } -func NewMobileModule(config config.Configuration, gorm *gorm.DB, database *sql.DB, validate *validator.Validate, redisClient *redis.Client) *MobileModule { +func NewMobileModule(config config.Configuration, gorm *gorm.DB, database *sql.DB, validate *validator.Validate, redisClient *redis.Client) *MobileModule { //nolint:funlen // This is an initialization function. queryBuilder := db.NewQueryBuilder(database) mobileDeviceRepository := adapters.NewMobileDeviceMysqlRepository(gorm) diff --git a/internal/api/support/ports/http.go b/internal/api/support/ports/http.go index 0773fea..8a30c93 100644 --- a/internal/api/support/ports/http.go +++ b/internal/api/support/ports/http.go @@ -96,30 +96,8 @@ func (r *RoutesHandler) CreateDebugLogsAudit(c *gin.Context) { logging.LogCommand(cmd) err := r.cqrs.Commands.CreateDebugLogsAudit.Handle(cmd) - if err != nil { - var notFoundErr adapters3.DebugLogsAuditCouldNotBeFoundError - - if errors.As(err, ¬FoundErr) { - c.JSON(404, api.NotFoundError(err)) - return - } - - var expiredErr domain.DebugLogsAuditClaimIsHasBeenExpiredError - - if errors.As(err, &expiredErr) { - c.JSON(410, api.GoneError(err)) - return - } - - var completedErr domain.DebugLogsAuditClaimIsAlreadyCompletedError - - if errors.As(err, &completedErr) { - c.JSON(410, api.GoneError(err)) - return - } - - c.JSON(400, api.NewBadRequestError(err)) + r.handleError(c, err) return } @@ -128,7 +106,6 @@ func (r *RoutesHandler) CreateDebugLogsAudit(c *gin.Context) { } presenter, err := r.cqrs.Queries.DebugLogsAuditQuery.Find(q) - if err != nil { c.JSON(404, api.NotFoundError(err)) return @@ -137,6 +114,32 @@ func (r *RoutesHandler) CreateDebugLogsAudit(c *gin.Context) { c.JSON(200, presenter) } +func (r *RoutesHandler) handleError(c *gin.Context, err error) { + var notFoundErr adapters3.DebugLogsAuditCouldNotBeFoundError + + if errors.As(err, ¬FoundErr) { + c.JSON(404, api.NotFoundError(err)) + return + } + + var expiredErr domain.DebugLogsAuditClaimIsHasBeenExpiredError + + if errors.As(err, &expiredErr) { + c.JSON(410, api.GoneError(err)) + return + } + + var completedErr domain.DebugLogsAuditClaimIsAlreadyCompletedError + + if errors.As(err, &completedErr) { + c.JSON(410, api.GoneError(err)) + return + } + + c.JSON(400, api.NewBadRequestError(err)) + return +} + func (r *RoutesHandler) UpdateDebugLogsAuditClaim(c *gin.Context) { cmd := &command.UpdateDebugLogsAudit{} diff --git a/internal/common/http/validate.go b/internal/common/http/validate.go index e1a8a99..a7a7c4c 100644 --- a/internal/common/http/validate.go +++ b/internal/common/http/validate.go @@ -1,6 +1,7 @@ package http import ( + "errors" "fmt" "github.com/gin-gonic/gin" @@ -14,7 +15,8 @@ func Validate(c *gin.Context, v *validator.Validate, a any) bool { err := v.Struct(a) if err != nil { - validationErrors, ok := err.(validator.ValidationErrors) + var validationErrors validator.ValidationErrors + ok := errors.As(err, &validationErrors) if !ok { logging.FromContext(c.Request.Context()).Errorf("unexpected validation error: %v", err) c.JSON(500, api.NewInternalServerError(fmt.Errorf("unexpected validation error"))) diff --git a/internal/websocket/common/hub.go b/internal/websocket/common/hub.go index 73ca617..005c830 100644 --- a/internal/websocket/common/hub.go +++ b/internal/websocket/common/hub.go @@ -47,7 +47,7 @@ func (h *Hub) sendToClient(c *Client, msg []byte) { func (h *Hub) broadcastMsg(msg []byte) { h.clients.Range(func(key, value any) bool { - c := key.(*Client) //nolint:errcheck // We only store *Client types in the map. + c := key.(*Client) //nolint:errcheck,forcetypeassert // We only store *Client types in the map. h.sendToClient(c, msg) return true }) diff --git a/internal/websocket/common/hub_pool_test.go b/internal/websocket/common/hub_pool_test.go index 79a26cc..f1dba3a 100644 --- a/internal/websocket/common/hub_pool_test.go +++ b/internal/websocket/common/hub_pool_test.go @@ -97,7 +97,7 @@ func TestCreateRemoveConcurrently(t *testing.T) { } hubs.Range(func(key, value any) bool { - h1 := key.(*Hub) //nolint:errcheck // We only store *Hub types as keys. + h1 := key.(*Hub) //nolint:errcheck,forcetypeassert // We only store *Hub types as keys. if !h1.isEmpty() { if h2, ok := hp.hubs[h1.id]; !ok || h1 != h2 { t.Fatalf("Non-empty hub was evicted from hub pool: %q", h1.id)