diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11c997f1..691d42d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,10 +133,9 @@ jobs: - name: Run Golangci-lint uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.13.2 + version-file: .golangci-lint-version args: --config=.golangci.yml working-directory: backend - only-new-issues: ${{ github.event_name == 'pull_request' }} backend-test: name: Backend tests (${{ matrix.name }}) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e153101..f8a133c7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,11 @@ "editor.defaultFormatter": "oxc.oxc-vscode" }, "[go]": { - "editor.defaultFormatter": "golang.go" + "editor.defaultFormatter": "golang.go", + "editor.formatOnSave": true + }, + "[go.mod]": { + "editor.defaultFormatter": "golang.go", + "editor.formatOnSave": true } -} +} \ No newline at end of file diff --git a/backend/.golangci-lint-version b/backend/.golangci-lint-version new file mode 100644 index 00000000..ff76dbc5 --- /dev/null +++ b/backend/.golangci-lint-version @@ -0,0 +1 @@ +v2.14.0 \ No newline at end of file diff --git a/backend/.golangci.yml b/backend/.golangci.yml index b00be316..b56dcf5d 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -19,9 +19,9 @@ linters: - errchkjson - errorlint - exhaustive + - exptostd - gocheckcompilerdirectives - gochecksumtype - - gocognit - gocritic - gosec - gosmopolitan @@ -30,9 +30,11 @@ linters: - loggercheck - makezero - musttag + - nakedret - nilerr - nilnesserr - noctx + - perfsprint - protogetter - reassign - recvcheck diff --git a/backend/internal/apperror/constructors.go b/backend/internal/apperror/constructors.go index e9d691b7..c621265f 100644 --- a/backend/internal/apperror/constructors.go +++ b/backend/internal/apperror/constructors.go @@ -79,7 +79,7 @@ func UnsupportedFileType(expected string) *Error { } func FileTooLarge(maxSize string) *Error { - return New(CodeFileTooLarge, http.StatusRequestEntityTooLarge, fmt.Sprintf("File must not exceed %s", maxSize)).WithDetail("max_size", maxSize) + return New(CodeFileTooLarge, http.StatusRequestEntityTooLarge, "File must not exceed "+maxSize).WithDetail("max_size", maxSize) } func NotSignedIn() *Error { @@ -281,7 +281,7 @@ func LogoTypeNotSupported() *Error { } func LogoTooLarge(maxSize string) *Error { - return New(CodeLogoTooLarge, http.StatusUnprocessableEntity, fmt.Sprintf("Downloaded logo must not exceed %s", maxSize)). + return New(CodeLogoTooLarge, http.StatusUnprocessableEntity, "Downloaded logo must not exceed "+maxSize). WithDetail("max_size", maxSize) } diff --git a/backend/internal/email/templates.go b/backend/internal/email/templates.go index 9a40cedf..2b1de427 100644 --- a/backend/internal/email/templates.go +++ b/backend/internal/email/templates.go @@ -1,7 +1,6 @@ package email import ( - "fmt" "time" ) @@ -10,7 +9,7 @@ import ( var newLoginTemplate = template[newLoginTemplateData]{ path: "login-with-new-device", title: func(data *templateData[newLoginTemplateData]) string { - return fmt.Sprintf("New device login with %s", data.AppName) + return "New device login with " + data.AppName }, } @@ -31,7 +30,7 @@ var testTemplate = template[struct{}]{ var apiKeyExpiringSoonTemplate = template[apiKeyExpiringSoonTemplateData]{ path: "api-key-expiring-soon", title: func(data *templateData[apiKeyExpiringSoonTemplateData]) string { - return fmt.Sprintf("API Key \"%s\" Expiring Soon", data.Data.ApiKeyName) + return `API Key "` + data.Data.ApiKeyName + `" Expiring Soon` }, } diff --git a/backend/internal/emailverification/service.go b/backend/internal/emailverification/service.go index 6163f67c..b89de110 100644 --- a/backend/internal/emailverification/service.go +++ b/backend/internal/emailverification/service.go @@ -2,6 +2,7 @@ package emailverification import ( "context" + "errors" "fmt" "log/slog" "time" @@ -97,7 +98,7 @@ func (s *Service) Verify(ctx context.Context, userID, token string) error { var result consumeResponse if response == nil { - return fmt.Errorf("email verification actor returned an empty response") + return errors.New("email verification actor returned an empty response") } err = response.Decode(&result) if err != nil { diff --git a/backend/internal/ldapsync/service.go b/backend/internal/ldapsync/service.go index 585feacf..119e5e1a 100644 --- a/backend/internal/ldapsync/service.go +++ b/backend/internal/ldapsync/service.go @@ -759,12 +759,12 @@ func (s *Service) saveProfilePicture(parentCtx context.Context, userId string, p const maxProfilePictureSize int64 = 2 * 1024 * 1024 // 2MB if res.ContentLength > maxProfilePictureSize { - return fmt.Errorf("profile picture must not exceed 2 MB") + return errors.New("profile picture must not exceed 2 MB") } data, err := io.ReadAll(utils.NewLimitReader(res.Body, maxProfilePictureSize+1)) if errors.Is(err, utils.ErrSizeExceeded) { - return fmt.Errorf("profile picture must not exceed 2 MB") + return errors.New("profile picture must not exceed 2 MB") } else if err != nil { return fmt.Errorf("failed to read profile picture: %w", err) } diff --git a/backend/internal/middleware/error_handler.go b/backend/internal/middleware/error_handler.go index e5336fcf..7b59bb22 100644 --- a/backend/internal/middleware/error_handler.go +++ b/backend/internal/middleware/error_handler.go @@ -3,10 +3,10 @@ package middleware import ( "context" "errors" - "fmt" "log/slog" "net/http" "reflect" + "strconv" "strings" "time" "unicode" @@ -254,7 +254,7 @@ func formatRetryAfter(retryAfter time.Duration) string { seconds = 1 } - return fmt.Sprintf("%d", seconds) + return strconv.Itoa(seconds) } func capitalizeFirst(message string) string { diff --git a/backend/internal/scimsync/service.go b/backend/internal/scimsync/service.go index 89174c35..93b64782 100644 --- a/backend/internal/scimsync/service.go +++ b/backend/internal/scimsync/service.go @@ -476,7 +476,7 @@ func (s *Service) syncGroups(ctx context.Context, provider ServiceProvider, grou func (s *Service) syncUser(ctx context.Context, provider ServiceProvider, user model.User, userResource *ScimUser) (scimSyncAction, *ScimUser, error) { // If user is not allowed for the client, delete it from SCIM provider if userResource != nil && !oidc.IsUserGroupAllowedToAuthorize(user, provider.OidcClient) { - return scimActionDeleted, nil, s.deleteScimResource(ctx, provider, fmt.Sprintf("/Users/%s", url.PathEscape(userResource.ID))) + return scimActionDeleted, nil, s.deleteScimResource(ctx, provider, "/Users/"+url.PathEscape(userResource.ID)) } payload := ScimUser{ @@ -505,11 +505,13 @@ func (s *Service) syncUser(ctx context.Context, provider ServiceProvider, user m if user.LastModified().Before(userResource.GetMeta().LastModified) { return scimActionNone, nil, nil } - path := fmt.Sprintf("/Users/%s", url.PathEscape(userResource.GetID())) + + path := "/Users/" + url.PathEscape(userResource.GetID()) userResource, err := updateScimResource(s, ctx, provider, path, payload) if err != nil { return scimActionNone, nil, err } + return scimActionUpdated, userResource, nil } @@ -525,10 +527,11 @@ func (s *Service) syncUser(ctx context.Context, provider ServiceProvider, user m func (s *Service) syncGroup(ctx context.Context, provider ServiceProvider, group model.UserGroup, groupResource *ScimGroup, userResources []ScimUser) (scimSyncAction, error) { // If group is not allowed for the client, delete it from SCIM provider if groupResource != nil && !groupAllowedForClient(group.ID, provider.OidcClient) { - err := s.deleteScimResource(ctx, provider, fmt.Sprintf("/Groups/%s", url.PathEscape(groupResource.GetID()))) + err := s.deleteScimResource(ctx, provider, "/Groups/"+url.PathEscape(groupResource.GetID())) if err != nil { return scimActionNone, err } + return scimActionDeleted, nil } @@ -560,11 +563,13 @@ func (s *Service) syncGroup(ctx context.Context, provider ServiceProvider, group if group.LastModified().Before(groupResource.GetMeta().LastModified) { return scimActionNone, nil } - path := fmt.Sprintf("/Groups/%s", url.PathEscape(groupResource.GetID())) + + path := "/Groups/" + url.PathEscape(groupResource.GetID()) _, err := updateScimResource(s, ctx, provider, path, groupPayload) if err != nil { return scimActionNone, err } + return scimActionUpdated, nil } @@ -826,7 +831,7 @@ func (s *Service) scimRequest(ctx context.Context, provider ServiceProvider, met } } - return nil, fmt.Errorf("scim request retry attempts exceeded") + return nil, errors.New("scim request retry attempts exceeded") } func scimRetryDelay(retryAfter string, attempt int) time.Duration { diff --git a/backend/internal/scimsync/sync_test.go b/backend/internal/scimsync/sync_test.go index 44870be6..2b5e432a 100644 --- a/backend/internal/scimsync/sync_test.go +++ b/backend/internal/scimsync/sync_test.go @@ -414,7 +414,7 @@ func (m *mockSCIMTransport) RoundTrip(req *http.Request) (*http.Response, error) func (m *mockSCIMTransport) validateRequest(req *http.Request, body []byte) string { if req.URL.Scheme != "https" || req.URL.Host != "scim.example.test" { - return fmt.Sprintf("request used unexpected SCIM endpoint %s", req.URL.String()) + return "request used unexpected SCIM endpoint " + req.URL.String() } if req.Header.Get("Accept") != mockSCIMRequestContentType { return "request did not accept application/scim+json" diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index d6316a49..54ab448a 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -965,7 +965,7 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, clie return apperror.InvalidLogoURL(err) } if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { - return apperror.InvalidLogoURL(fmt.Errorf("URL must use HTTP or HTTPS and include a host")) + return apperror.InvalidLogoURL(errors.New("URL must use HTTP or HTTPS and include a host")) } ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second)