Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions LEARNING_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- [x] Implement user ownership (users can only CRUD their own todos)
- [x] Add password strength validation
- [x] Ensure OWASP Top 10 security best practices are implemented
- [x] Add CORS middleware and security headers
- [ ] Implement rate limiting on auth endpoints (prevent brute force)

**Why this first:** Almost every real application needs authentication. It touches all layers (API → Service → Database) and teaches security fundamentals.
Expand Down
28 changes: 22 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ package api

import (
"context"
"doit/internal/config"
"doit/pkg/database"
"doit/pkg/logger"
"doit/pkg/retry"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"doit/internal/cache"
"doit/internal/config"
"doit/pkg/database"
"doit/pkg/logger"
"doit/pkg/retry"
)

func Run(ctx context.Context, logger *logger.Logger, cfg *config.Config) error {

// Initialize database with retry logic
dbPool, err := retry.ConnectWithRetry(ctx, retry.DefaultRetryConfig(), func(ctx context.Context) (*database.Pool, error) {
return database.New(ctx, database.Config{
Expand All @@ -42,8 +43,23 @@ func Run(ctx context.Context, logger *logger.Logger, cfg *config.Config) error {
)
defer dbPool.Close()

// Initialize cache with retry logic

opts := &cache.RedisOptions{
Addr: cfg.Redis.Addr,
Password: cfg.Redis.Password,
DB: cfg.Redis.Database,
PoolSize: 10,
}
cache, err := cache.NewRedisCache(opts)
if err != nil {
logger.Error(ctx, "Failed to connect to cache", "error", err)
return fmt.Errorf("failed to connect to cache: %w", err)
}
defer cache.Close()

// Starting the HTTP server with graceful shutdown
srv, err := NewServer(logger, cfg, dbPool)
srv, err := NewServer(logger, cfg, dbPool, cache)
if err != nil {
return fmt.Errorf("failed to create server: %w", err)
}
Expand Down
3 changes: 2 additions & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"doit/api/v1/auth"
"doit/internal/cache"
"doit/internal/config"
"doit/internal/middlewares"
"doit/internal/model"
Expand All @@ -15,7 +16,7 @@ import (
"doit/pkg/logger"
)

func NewServer(logger *logger.Logger, cfg *config.Config, dbPool *database.Pool) (http.Handler, error) {
func NewServer(logger *logger.Logger, cfg *config.Config, dbPool *database.Pool, cache cache.Cache) (http.Handler, error) {
// Helpers
tokenMaker, err := token.NewJWTToken(cfg.JWT.Secret)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ require (
github.com/google/uuid v1.6.0
github.com/islamghany/enfl v1.0.0
github.com/jackc/pgx/v5 v5.7.2
github.com/redis/go-redis/v9 v9.16.0
github.com/stretchr/testify v1.10.0
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.42.0
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
Expand Down Expand Up @@ -34,6 +42,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
47 changes: 47 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cache

import (
"context"
"errors"
"time"
)

// Error Handling
var (
ErrCacheMiss = errors.New("cache miss") // Key not found (distinguish from actual errors)
ErrExpired = errors.New("cache expired") // Key found but expired
ErrConnection = errors.New("cache connection error") // Connection error
ErrSerialization = errors.New("cache serialization error") // Marshal/unmarshal failed
ErrInvalidKey = errors.New("invalid cache key") // Empty or invalid key
)

// Cache defines methods for caching operations.
type Cache interface {
// Basic Operations
Get(ctx context.Context, key string) (any, error)
Set(ctx context.Context, key string, value any) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)

// TTL Operations
SetWithTTL(ctx context.Context, key string, value any, ttl time.Duration) error
GetTTL(ctx context.Context, key string) (time.Duration, error)
Expire(ctx context.Context, key string, ttl time.Duration) error // Set TTL to 0 to remove

// Numbers Operations (useful for counters, rate limiting)
Increment(ctx context.Context, key string) (int64, error)
Decrement(ctx context.Context, key string) (int64, error)
IncrementBy(ctx context.Context, key string, delta int64) (int64, error)

// Utility Operations
Clear(ctx context.Context) error
Close() error
}

// validateKey checks if the key is valid
func validateKey(key string) error {
if key == "" {
return ErrInvalidKey
}
return nil
}
Loading
Loading