Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
830945b
feat(linter): add bodyclose linter and fix test resource cleanup
mishankov Oct 25, 2025
b141fe8
feat(linter): add additional linters and improve error handling
mishankov Oct 25, 2025
620ac9b
refactor(auth): centralize error variables and use them in service
mishankov Oct 25, 2025
eb55d2c
feat(linter): add exhaustive, fatcontext and forcetypeassert linters
mishankov Oct 25, 2025
2699ee3
.
mishankov Oct 25, 2025
3a91388
feat(linter): add gocheckcompilerdirectives and gochecknoglobals linters
mishankov Oct 25, 2025
9d1a9ce
feat(linter): add additional linters and improve documentation
mishankov Oct 25, 2025
06219af
feat(linter): add godox and gosec linters with security improvements
mishankov Oct 25, 2025
dc9b6c1
refactor(cli): remove redundant filepath.Clean call
mishankov Oct 25, 2025
07de241
feat(linter): add govet, iface, and intrange linters with configuration
mishankov Oct 25, 2025
e38282e
feat(linter): add makezero and mirror linters
mishankov Oct 25, 2025
0397429
feat(linter): add additional linters and improve error handling
mishankov Oct 25, 2025
7222076
feat(linter): add noctx linter to configuration
mishankov Oct 25, 2025
43a712c
fix(test): prevent nil pointer dereference in http client test
mishankov Oct 25, 2025
908e5bd
fix(auth): prevent nil pointer panic in authentication middleware
mishankov Oct 25, 2025
ac8ef3c
fix(auth): return proper error in mock user service
mishankov Oct 26, 2025
d3bf4fb
test: enable parallel test execution and add missing t.Parallel()
mishankov Oct 26, 2025
f3a6b79
fix(test): add nolint directive for bodyclose in http client test
mishankov Oct 26, 2025
750eb2f
ci: add perfsprint linter to golangci configuration
mishankov Oct 26, 2025
af6e47f
ci: add prealloc linter to golangci configuration
mishankov Oct 26, 2025
0a07907
style: align struct tags in auth and session models
mishankov Oct 26, 2025
96827f0
ci: add additional linters and enforce HTTP method constants in tests
mishankov Oct 26, 2025
b8259d2
feat(linter): add additional linters and improve error handling
mishankov Oct 26, 2025
745fa42
ci: enable gofmt formatter in golangci configuration
mishankov Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
version: "2"

linters:
# consider to add: cyclop
# find sane settings for revive
default: none
enable:
- bodyclose
- containedctx
- dupl
- err113
- errcheck
- exhaustive
- fatcontext
- forcetypeassert
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- goconst
- gocritic
- godoclint
- godox
- gosec
- govet
- iface
- intrange
- makezero
- mirror
- musttag
- nestif
- nilerr
- nilnesserr
- nilnil
- noctx
- paralleltest
- perfsprint
- prealloc
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- wastedassign
- wrapcheck

settings:
iface:
enable:
- identical # Identifies interfaces in the same package that have identical method sets.
- unused # Identifies interfaces that are not used anywhere in the same package where the interface is defined.
- opaque # Identifies functions that return interfaces, but the actual returned value is always a single concrete implementation.

tagliatelle:
case:
rules:
json: camel
exclusions:
rules:
- path: _test\.go
linters:
- err113
- errcheck
- gosec
- nilnil

formatters:
enable:
- gofmt
7 changes: 6 additions & 1 deletion application/healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"net/http"

"github.com/mishankov/platforma/log"
)

type healther interface {
Expand All @@ -24,5 +26,8 @@ func (h *HealthCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

json.NewEncoder(w).Encode(health)
err := json.NewEncoder(w).Encode(health)
if err != nil {
log.ErrorContext(r.Context(), "failed to decode response to json", "error", err)
}
}
17 changes: 13 additions & 4 deletions auth/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ package auth

import "errors"

var ErrInvalidUsername = errors.New("invalid username")
var ErrInvalidPassword = errors.New("invalid password")
var ErrWrongUserOrPassword = errors.New("wrong user or password")
var ErrCurrentPasswordIncorrect = errors.New("current password is incorrect")
var (
ErrUserNotFound = errors.New("user not found")
ErrWrongUserOrPassword = errors.New("wrong user or password")

ErrInvalidUsername = errors.New("invalid username")
ErrShortUsername = errors.New("short username")
ErrLongUsername = errors.New("long username")

ErrInvalidPassword = errors.New("invalid password")
ErrShortPassword = errors.New("short password")
ErrLongPassword = errors.New("long password")
ErrCurrentPasswordIncorrect = errors.New("current password is incorrect")
)
7 changes: 6 additions & 1 deletion auth/handler_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package auth
import (
"encoding/json"
"net/http"

"github.com/mishankov/platforma/log"
)

type GetHandler struct {
Expand Down Expand Up @@ -48,5 +50,8 @@ func (h *GetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Username: user.Username,
}

json.NewEncoder(w).Encode(resp)
err = json.NewEncoder(w).Encode(resp)
if err != nil {
log.ErrorContext(ctx, "failed to decode response to json", "error", err)
}
}
20 changes: 12 additions & 8 deletions auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"context"
"errors"
"net/http"

"github.com/mishankov/platforma/log"
Expand Down Expand Up @@ -29,20 +30,23 @@ func (m *AuthenticationMiddleware) Wrap(next http.Handler) http.Handler {
}

user, err := m.userService.GetFromSession(r.Context(), cookie.Value)
if err != nil {
http.Error(w, "failed to get user", http.StatusInternalServerError)
if errors.Is(err, ErrUserNotFound) {
w.WriteHeader(http.StatusUnauthorized)
return
}

if user == nil {
w.WriteHeader(http.StatusUnauthorized)
if err != nil {
http.Error(w, "failed to get user", http.StatusInternalServerError)
return
}

ctxWithUserId := context.WithValue(r.Context(), log.UserIdKey, user.ID)
ctxWithUser := context.WithValue(ctxWithUserId, UserContextKey, user)
requestWithUser := r.WithContext(ctxWithUser)
newRequest := r
if user != nil {
ctxWithUserId := context.WithValue(r.Context(), log.UserIdKey, user.ID)
ctxWithUser := context.WithValue(ctxWithUserId, UserContextKey, user)
newRequest = r.WithContext(ctxWithUser)
}

next.ServeHTTP(w, requestWithUser)
next.ServeHTTP(w, newRequest)
})
}
23 changes: 17 additions & 6 deletions auth/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
)

func TestAuthenticationMiddleware_ValidSession(t *testing.T) {
t.Parallel()

userSvc := &mockUserService{
users: map[string]*auth.User{
"valid-session-id": {ID: "user-id", Username: "testuser"},
Expand All @@ -23,7 +25,7 @@ func TestAuthenticationMiddleware_ValidSession(t *testing.T) {
w.WriteHeader(http.StatusOK)
}))

req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "session", Value: "valid-session-id"})
w := httptest.NewRecorder()

Expand All @@ -35,6 +37,8 @@ func TestAuthenticationMiddleware_ValidSession(t *testing.T) {
}

func TestAuthenticationMiddleware_NoSessionCookie(t *testing.T) {
t.Parallel()

userSvc := &mockUserService{
cookieName: "session",
}
Expand All @@ -44,7 +48,7 @@ func TestAuthenticationMiddleware_NoSessionCookie(t *testing.T) {
t.Fatal("handler should not be called when authentication fails")
}))

req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()

handler.ServeHTTP(w, req)
Expand All @@ -55,6 +59,8 @@ func TestAuthenticationMiddleware_NoSessionCookie(t *testing.T) {
}

func TestAuthenticationMiddleware_InvalidSession(t *testing.T) {
t.Parallel()

userSvc := &mockUserService{
users: map[string]*auth.User{},
cookieName: "session",
Expand All @@ -65,7 +71,7 @@ func TestAuthenticationMiddleware_InvalidSession(t *testing.T) {
t.Fatal("handler should not be called when authentication fails")
}))

req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "session", Value: "invalid-session-id"})
w := httptest.NewRecorder()

Expand All @@ -77,6 +83,8 @@ func TestAuthenticationMiddleware_InvalidSession(t *testing.T) {
}

func TestAuthenticationMiddleware_UserServiceError(t *testing.T) {
t.Parallel()

userSvc := &mockUserService{
error: errors.New("database error"),
cookieName: "session",
Expand All @@ -87,7 +95,7 @@ func TestAuthenticationMiddleware_UserServiceError(t *testing.T) {
t.Fatal("handler should not be called when authentication fails")
}))

req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "session", Value: "session-id"})
w := httptest.NewRecorder()

Expand All @@ -99,6 +107,8 @@ func TestAuthenticationMiddleware_UserServiceError(t *testing.T) {
}

func TestAuthenticationMiddleware_UserNotFound(t *testing.T) {
t.Parallel()

userSvc := &mockUserService{
users: map[string]*auth.User{},
cookieName: "session",
Expand All @@ -109,7 +119,7 @@ func TestAuthenticationMiddleware_UserNotFound(t *testing.T) {
t.Fatal("handler should not be called when authentication fails")
}))

req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "session", Value: "session-id"})
w := httptest.NewRecorder()

Expand All @@ -130,10 +140,11 @@ func (m *mockUserService) GetFromSession(ctx context.Context, sessionId string)
if m.error != nil {
return nil, m.error
}

if user, ok := m.users[sessionId]; ok {
return user, nil
}
return nil, nil
return nil, auth.ErrUserNotFound
}

func (m *mockUserService) CookieName() string {
Expand Down
14 changes: 7 additions & 7 deletions auth/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ const (
)

type User struct {
ID string `json:"id" db:"id"`
Username string `json:"username" db:"username"`
Password string `json:"password" db:"password"`
Salt string `json:"salt" db:"salt"`
Created time.Time `json:"created" db:"created"`
Updated time.Time `json:"updated" db:"updated"`
Status Status `json:"status" db:"status"`
ID string `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password"`
Salt string `db:"salt" json:"salt"`
Created time.Time `db:"created" json:"created"`
Updated time.Time `db:"updated" json:"updated"`
Status Status `db:"status" json:"status"`
}
15 changes: 11 additions & 4 deletions auth/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"context"
"database/sql"
"fmt"

"github.com/mishankov/platforma/database"
)
Expand Down Expand Up @@ -46,7 +47,7 @@ func (r *Repository) Get(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get user by id: %w", err)
}
return &user, nil
}
Expand All @@ -55,7 +56,7 @@ func (r *Repository) GetByUsername(ctx context.Context, username string) (*User,
var user User
err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE username = $1", username)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get user by username: %w", err)
}
return &user, nil
}
Expand All @@ -66,7 +67,10 @@ func (r *Repository) Create(ctx context.Context, user *User) error {
VALUES (:id, :username, :password, :salt, :created, :updated, :status)
`
_, err := r.db.NamedExecContext(ctx, query, user)
return err
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}

func (r *Repository) UpdatePassword(ctx context.Context, id, password, salt string) error {
Expand All @@ -76,5 +80,8 @@ func (r *Repository) UpdatePassword(ctx context.Context, id, password, salt stri
WHERE id = $3
`
_, err := r.db.ExecContext(ctx, query, password, salt, id)
return err
if err != nil {
return fmt.Errorf("failed to update password: %w", err)
}
return nil
}
Loading