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
30 changes: 28 additions & 2 deletions .cursor/rules/project-architecture.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ strive-api/

#### Handlers
- **AuthHandlers**: Регистрация, авторизация, обновление токенов
- **UserHandlers**: Управление профилем пользователя и темами
- **HealthHandlers**: Проверка состояния приложения и БД

#### Middleware
Expand All @@ -122,6 +123,7 @@ type User struct {
ID uuid.UUID `json:"id" db:"id"`
Email string `json:"email" db:"email"`
PasswordHash string `json:"-" db:"password_hash"`
Theme string `json:"theme" db:"theme"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
Expand All @@ -131,10 +133,11 @@ type User struct {
- CreateUserRequest
- UpdateUserRequest
- ChangePasswordRequest
- UpdateUserThemeRequest

### 5. Сервисы (internal/services/)

**Файл**: `auth_service.go`
#### AuthService (`auth_service.go`)

Бизнес-логика аутентификации:

Expand All @@ -155,6 +158,24 @@ type AuthService interface {
- JWT токены (access + refresh) с расширенной валидацией
- Валидация токенов с проверкой iss/aud/clock skew
- Специфичные ошибки валидации (ErrTokenExpired, ErrInvalidIssuer, etc.)
- Инициализация темы пользователя по умолчанию при регистрации

#### UserService (`user_service.go`)

Бизнес-логика управления профилем пользователя:

```go
type UserService interface {
GetUserProfile(ctx context.Context, userID uuid.UUID) (*models.User, error)
UpdateUserTheme(ctx context.Context, userID uuid.UUID, theme string) error
}
```

**Функциональность**:
- Получение профиля пользователя с темой
- Обновление темы пользователя (light/dark)
- Валидация темы на уровне сервиса
- Разделение ответственности от AuthService

### 6. Репозитории (internal/repositories/)

Expand All @@ -168,6 +189,7 @@ type UserRepository interface {
GetByID(ctx context.Context, id uuid.UUID) (*models.User, error)
GetByEmail(ctx context.Context, email string) (*models.User, error)
Update(ctx context.Context, user *models.User) error
UpdateTheme(ctx context.Context, userID uuid.UUID, theme string) error
Delete(ctx context.Context, id uuid.UUID) error
}
```
Expand All @@ -176,6 +198,8 @@ type UserRepository interface {
- Подготовленные SQL запросы
- Context для отмены операций
- Обработка ошибок БД
- Операции с темами пользователей
- Рефакторинг с общими методами сканирования

### 7. Валидация (internal/validation/)

Expand All @@ -184,6 +208,7 @@ type UserRepository interface {
Валидация входных данных:
- Email валидация
- Пароль валидация (минимум 8 символов, специальные символы, заглавные/строчные буквы, цифры)
- Валидация темы (только 'light' или 'dark')
- Структурированные ошибки валидации

### 8. Логирование (internal/logger/)
Expand Down Expand Up @@ -223,7 +248,8 @@ GET /swagger/ - Swagger документация
### Защищенные эндпоинты:

```
GET /api/v1/auth/me - Информация о текущем пользователе
GET /api/v1/user/me - Информация о текущем пользователе (включая тему)
PUT /api/v1/user/theme - Обновление темы пользователя
```

## Конфигурация через ENV
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ run-dev:
DB_PASSWORD=password \
DB_NAME=strive \
DB_SSL_MODE=disable \
JWT_SECRET=dev-secret-key-12345 \
JWT_SECRET=dev-secret-key-12345-very-long-for-security \
JWT_ISSUER=strive-api \
JWT_AUDIENCE=strive-app \
JWT_CLOCK_SKEW=2m \
Expand Down
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ A modern workout diary API built with Go, featuring user authentication, JWT tok
## 🚀 Features

- **User Authentication**: JWT-based authentication with access and refresh tokens
- **Password Security**: bcrypt password hashing
- **User Profile Management**: Complete user profile with theme preferences
- **Theme Support**: Light/Dark theme selection for users
- **Password Security**: bcrypt password hashing with strong validation
- **Database Integration**: PostgreSQL with automatic migrations
- **Comprehensive Testing**: 17 unit tests with 73%+ code coverage
- **Clean Architecture**: Separation of concerns with services and repositories
- **Comprehensive Testing**: Unit tests with race detection
- **API Documentation**: OpenAPI/Swagger documentation
- **Containerization**: Docker and Docker Compose support
- **Structured Logging**: JSON/text logging with configurable levels
Expand Down Expand Up @@ -91,10 +94,13 @@ Once the server is running, visit:
- `GET /health` - Health check
- `POST /api/v1/auth/register` - User registration
- `POST /api/v1/auth/login` - User login
- `POST /api/v1/auth/refresh` - Refresh access token
- `POST /api/v1/auth/logout` - User logout

### Protected Endpoints (require JWT token)

- `GET /api/v1/user/profile` - Get user profile
- `GET /api/v1/user/me` - Get user profile (includes theme)
- `PUT /api/v1/user/theme` - Update user theme preference

### Example Usage

Expand All @@ -118,12 +124,20 @@ curl -X POST http://localhost:8080/api/v1/auth/login \
}'
```

**Access protected endpoint:**
**Get user profile:**
```bash
curl -X GET http://localhost:8080/api/v1/user/profile \
curl -X GET http://localhost:8080/api/v1/user/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```

**Update user theme:**
```bash
curl -X PUT http://localhost:8080/api/v1/user/theme \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"theme":"dark"}'
```

## 🧪 Testing

Run the test suite:
Expand Down Expand Up @@ -194,11 +208,19 @@ strive-api/
│ ├── config/ # Configuration management
│ ├── database/ # Database connection and health
│ ├── http/ # HTTP handlers and middleware
│ │ ├── auth_handlers.go # Authentication endpoints
│ │ ├── user_handlers.go # User profile and theme endpoints
│ │ ├── middleware.go # Security and logging middleware
│ │ └── ...
│ ├── logger/ # Structured logging
│ ├── migrate/ # Database migrations
│ ├── models/ # Data models
│ ├── models/ # Data models (User, Theme, etc.)
│ ├── repositories/ # Data access layer
│ └── services/ # Business logic
│ ├── services/ # Business logic
│ │ ├── auth_service.go # Authentication logic
│ │ ├── user_service.go # User profile and theme logic
│ │ └── ...
│ └── validation/ # Input validation
├── docs/ # Generated API documentation
├── migrations/ # Database migration files
├── docker-compose.yml # Docker Compose configuration
Expand Down
38 changes: 24 additions & 14 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ func main() {
runMigrations(cfg, logger)

// Initialize services and handlers
authService := setupServices(db, cfg)
handlers := setupHandlers(authService, logger, db, cfg)
services := setupServices(db, cfg)
handlers := setupHandlers(services, logger, db, cfg)

// Setup routes and middleware
handler := setupRoutes(handlers, logger, authService, cfg)
handler := setupRoutes(handlers, logger, services.Auth, cfg)

// Start server
server := httphandler.NewServer(cfg, handler, logger)
Expand Down Expand Up @@ -100,21 +100,32 @@ func runMigrations(cfg *config.Config, logger *logger.Logger) {
}
}

func setupServices(db *database.Database, cfg *config.Config) services.AuthService {
type Services struct {
Auth services.AuthService
User services.UserService
}

func setupServices(db *database.Database, cfg *config.Config) *Services {
userRepo := repositories.NewUserRepository(db.Pool())
refreshTokenRepo := repositories.NewRefreshTokenRepository(db.Pool())
authService := services.NewAuthService(userRepo, refreshTokenRepo, &cfg.JWT)
return authService
userService := services.NewUserService(userRepo)
return &Services{
Auth: authService,
User: userService,
}
}

type Handlers struct {
Auth *httphandler.AuthHandlers
User *httphandler.UserHandlers
Health *httphandler.DetailedHealthHandler
}

func setupHandlers(authService services.AuthService, logger *logger.Logger, db *database.Database, cfg *config.Config) *Handlers {
func setupHandlers(services *Services, logger *logger.Logger, db *database.Database, cfg *config.Config) *Handlers {
return &Handlers{
Auth: httphandler.NewAuthHandlers(authService, logger, cfg),
Auth: httphandler.NewAuthHandlers(services.Auth, logger, cfg),
User: httphandler.NewUserHandlers(services.User, logger),
Health: httphandler.NewDetailedHealthHandler(logger, db.Pool()),
}
}
Expand Down Expand Up @@ -149,13 +160,12 @@ func setupPublicRoutes(mux *http.ServeMux, handlers *Handlers) {
}

func setupProtectedRoutes(mux *http.ServeMux, authService services.AuthService, logger *logger.Logger, handlers *Handlers) {
protectedMux := http.NewServeMux()

protectedMux.HandleFunc("/me", handlers.Auth.Me)

protectedHandler := httphandler.AuthMiddleware(authService, logger)(protectedMux)

mux.Handle("/api/v1/auth/", http.StripPrefix("/api/v1/auth", protectedHandler))
// User protected routes
userProtectedMux := http.NewServeMux()
userProtectedMux.HandleFunc("/me", handlers.User.Me)
userProtectedMux.HandleFunc("/theme", handlers.User.UpdateTheme)
userProtectedHandler := httphandler.AuthMiddleware(authService, logger)(userProtectedMux)
mux.Handle("/api/v1/user/", http.StripPrefix("/api/v1/user", userProtectedHandler))
}

func applyMiddleware(mux *http.ServeMux, logger *logger.Logger, cfg *config.Config) http.Handler {
Expand Down
Loading