Rewrite Tracks application in Golang
This commit introduces a complete rewrite of the Tracks GTD application
in Go (Golang), providing a modern, performant alternative to the Ruby
on Rails implementation.
## Architecture & Technology Stack
- Language: Go 1.21+
- Web Framework: Gin
- ORM: GORM with SQLite/MySQL/PostgreSQL support
- Authentication: JWT with bcrypt password hashing
- Clean Architecture: Separated models, services, handlers, and middleware
## Implemented Features
### Core Models
- User: Authentication and user management
- Context: GTD contexts (@home, @work, etc.)
- Project: Project grouping and tracking
- Todo: Task management with state machine (active, completed, deferred, pending)
- Tag: Flexible tagging system with polymorphic associations
- Dependency: Todo dependencies with circular dependency detection
- Preference: User preferences and settings
- Note: Project notes
- Attachment: File attachment support (model only)
- RecurringTodo: Recurring task template (model only)
### API Endpoints
**Authentication:**
- POST /api/auth/login - User login
- POST /api/auth/register - User registration
- POST /api/auth/logout - User logout
- GET /api/me - Get current user
**Todos:**
- GET /api/todos - List todos with filtering
- POST /api/todos - Create todo
- GET /api/todos/:id - Get todo details
- PUT /api/todos/:id - Update todo
- DELETE /api/todos/:id - Delete todo
- POST /api/todos/:id/complete - Mark as completed
- POST /api/todos/:id/activate - Mark as active
- POST /api/todos/:id/defer - Defer to future date
- POST /api/todos/:id/dependencies - Add dependency
- DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency
**Projects:**
- GET /api/projects - List projects
- POST /api/projects - Create project
- GET /api/projects/:id - Get project details
- PUT /api/projects/:id - Update project
- DELETE /api/projects/:id - Delete project
- POST /api/projects/:id/complete - Complete project
- POST /api/projects/:id/activate - Activate project
- POST /api/projects/:id/hide - Hide project
- POST /api/projects/:id/review - Mark as reviewed
- GET /api/projects/:id/stats - Get project statistics
**Contexts:**
- GET /api/contexts - List contexts
- POST /api/contexts - Create context
- GET /api/contexts/:id - Get context details
- PUT /api/contexts/:id - Update context
- DELETE /api/contexts/:id - Delete context
- POST /api/contexts/:id/hide - Hide context
- POST /api/contexts/:id/activate - Activate context
- POST /api/contexts/:id/close - Close context
- GET /api/contexts/:id/stats - Get context statistics
### Business Logic
**Todo State Management:**
- Active: Ready to work on
- Completed: Finished tasks
- Deferred: Future actions (show_from date)
- Pending: Blocked by dependencies
**Dependency Management:**
- Create blocking relationships between todos
- Automatic state transitions when blocking todos complete
- Circular dependency detection
- Automatic unblocking when prerequisites complete
**Tag System:**
- Polymorphic tagging for todos and recurring todos
- Automatic tag creation on first use
- Tag cloud support
**Project & Context Tracking:**
- State management (active, hidden, closed/completed)
- Statistics and health indicators
- Review tracking for projects
### Infrastructure
**Configuration:**
- Environment-based configuration
- Support for SQLite, MySQL, and PostgreSQL
- Configurable JWT secrets and token expiry
- Flexible server settings
**Database:**
- GORM for ORM
- Automatic migrations
- Connection pooling
- Multi-database support
**Authentication & Security:**
- JWT-based authentication
- Bcrypt password hashing
- Secure cookie support
- Token refresh mechanism
**Docker Support:**
- Multi-stage Dockerfile for optimized builds
- Docker Compose with PostgreSQL
- Volume mounting for data persistence
- Production-ready configuration
## Project Structure
```
cmd/tracks/ # Application entry point
internal/
config/ # Configuration management
database/ # Database setup and migrations
handlers/ # HTTP request handlers
middleware/ # Authentication middleware
models/ # Database models
services/ # Business logic layer
```
## Documentation
- README_GOLANG.md: Comprehensive documentation
- .env.example: Configuration template
- API documentation included in README
- Code comments for complex logic
## Future Work
The following features from the original Rails app are not yet implemented:
- Recurring todo instantiation logic
- Email integration (Mailgun/CloudMailin)
- Advanced statistics and analytics
- Import/Export functionality (CSV, YAML, XML)
- File upload handling for attachments
- Mobile views
- RSS/Atom feeds
- iCalendar export
## Benefits Over Rails Version
- Performance: Compiled binary, lower resource usage
- Deployment: Single binary, no runtime dependencies
- Type Safety: Compile-time type checking
- Concurrency: Better handling of concurrent requests
- Memory: Lower memory footprint
- Portability: Easy cross-platform compilation
## Testing
The code structure supports testing, though tests are not yet implemented.
Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/TracksApp/tracks/internal/database"
|
|
|
|
|
"github.com/TracksApp/tracks/internal/models"
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AuthService handles authentication logic
|
|
|
|
|
type AuthService struct {
|
|
|
|
|
jwtSecret string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAuthService creates a new AuthService
|
|
|
|
|
func NewAuthService(jwtSecret string) *AuthService {
|
|
|
|
|
return &AuthService{
|
|
|
|
|
jwtSecret: jwtSecret,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LoginRequest represents a login request
|
|
|
|
|
type LoginRequest struct {
|
|
|
|
|
Login string `json:"login" binding:"required"`
|
|
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterRequest represents a registration request
|
|
|
|
|
type RegisterRequest struct {
|
|
|
|
|
Login string `json:"login" binding:"required"`
|
|
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
FirstName string `json:"first_name"`
|
|
|
|
|
LastName string `json:"last_name"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 11:35:36 +00:00
|
|
|
// CreateUserRequest represents an admin user creation request
|
|
|
|
|
type CreateUserRequest struct {
|
|
|
|
|
Login string `json:"login" binding:"required"`
|
|
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
FirstName string `json:"first_name"`
|
|
|
|
|
LastName string `json:"last_name"`
|
|
|
|
|
IsAdmin bool `json:"is_admin"`
|
|
|
|
|
}
|
|
|
|
|
|
Rewrite Tracks application in Golang
This commit introduces a complete rewrite of the Tracks GTD application
in Go (Golang), providing a modern, performant alternative to the Ruby
on Rails implementation.
## Architecture & Technology Stack
- Language: Go 1.21+
- Web Framework: Gin
- ORM: GORM with SQLite/MySQL/PostgreSQL support
- Authentication: JWT with bcrypt password hashing
- Clean Architecture: Separated models, services, handlers, and middleware
## Implemented Features
### Core Models
- User: Authentication and user management
- Context: GTD contexts (@home, @work, etc.)
- Project: Project grouping and tracking
- Todo: Task management with state machine (active, completed, deferred, pending)
- Tag: Flexible tagging system with polymorphic associations
- Dependency: Todo dependencies with circular dependency detection
- Preference: User preferences and settings
- Note: Project notes
- Attachment: File attachment support (model only)
- RecurringTodo: Recurring task template (model only)
### API Endpoints
**Authentication:**
- POST /api/auth/login - User login
- POST /api/auth/register - User registration
- POST /api/auth/logout - User logout
- GET /api/me - Get current user
**Todos:**
- GET /api/todos - List todos with filtering
- POST /api/todos - Create todo
- GET /api/todos/:id - Get todo details
- PUT /api/todos/:id - Update todo
- DELETE /api/todos/:id - Delete todo
- POST /api/todos/:id/complete - Mark as completed
- POST /api/todos/:id/activate - Mark as active
- POST /api/todos/:id/defer - Defer to future date
- POST /api/todos/:id/dependencies - Add dependency
- DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency
**Projects:**
- GET /api/projects - List projects
- POST /api/projects - Create project
- GET /api/projects/:id - Get project details
- PUT /api/projects/:id - Update project
- DELETE /api/projects/:id - Delete project
- POST /api/projects/:id/complete - Complete project
- POST /api/projects/:id/activate - Activate project
- POST /api/projects/:id/hide - Hide project
- POST /api/projects/:id/review - Mark as reviewed
- GET /api/projects/:id/stats - Get project statistics
**Contexts:**
- GET /api/contexts - List contexts
- POST /api/contexts - Create context
- GET /api/contexts/:id - Get context details
- PUT /api/contexts/:id - Update context
- DELETE /api/contexts/:id - Delete context
- POST /api/contexts/:id/hide - Hide context
- POST /api/contexts/:id/activate - Activate context
- POST /api/contexts/:id/close - Close context
- GET /api/contexts/:id/stats - Get context statistics
### Business Logic
**Todo State Management:**
- Active: Ready to work on
- Completed: Finished tasks
- Deferred: Future actions (show_from date)
- Pending: Blocked by dependencies
**Dependency Management:**
- Create blocking relationships between todos
- Automatic state transitions when blocking todos complete
- Circular dependency detection
- Automatic unblocking when prerequisites complete
**Tag System:**
- Polymorphic tagging for todos and recurring todos
- Automatic tag creation on first use
- Tag cloud support
**Project & Context Tracking:**
- State management (active, hidden, closed/completed)
- Statistics and health indicators
- Review tracking for projects
### Infrastructure
**Configuration:**
- Environment-based configuration
- Support for SQLite, MySQL, and PostgreSQL
- Configurable JWT secrets and token expiry
- Flexible server settings
**Database:**
- GORM for ORM
- Automatic migrations
- Connection pooling
- Multi-database support
**Authentication & Security:**
- JWT-based authentication
- Bcrypt password hashing
- Secure cookie support
- Token refresh mechanism
**Docker Support:**
- Multi-stage Dockerfile for optimized builds
- Docker Compose with PostgreSQL
- Volume mounting for data persistence
- Production-ready configuration
## Project Structure
```
cmd/tracks/ # Application entry point
internal/
config/ # Configuration management
database/ # Database setup and migrations
handlers/ # HTTP request handlers
middleware/ # Authentication middleware
models/ # Database models
services/ # Business logic layer
```
## Documentation
- README_GOLANG.md: Comprehensive documentation
- .env.example: Configuration template
- API documentation included in README
- Code comments for complex logic
## Future Work
The following features from the original Rails app are not yet implemented:
- Recurring todo instantiation logic
- Email integration (Mailgun/CloudMailin)
- Advanced statistics and analytics
- Import/Export functionality (CSV, YAML, XML)
- File upload handling for attachments
- Mobile views
- RSS/Atom feeds
- iCalendar export
## Benefits Over Rails Version
- Performance: Compiled binary, lower resource usage
- Deployment: Single binary, no runtime dependencies
- Type Safety: Compile-time type checking
- Concurrency: Better handling of concurrent requests
- Memory: Lower memory footprint
- Portability: Easy cross-platform compilation
## Testing
The code structure supports testing, though tests are not yet implemented.
Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
|
|
|
// AuthResponse represents an authentication response
|
|
|
|
|
type AuthResponse struct {
|
|
|
|
|
Token string `json:"token"`
|
|
|
|
|
User *models.User `json:"user"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login authenticates a user and returns a JWT token
|
|
|
|
|
func (s *AuthService) Login(req LoginRequest) (*AuthResponse, error) {
|
|
|
|
|
var user models.User
|
|
|
|
|
|
|
|
|
|
// Find user by login
|
|
|
|
|
if err := database.DB.Where("login = ?", req.Login).First(&user).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, errors.New("invalid login or password")
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check password
|
|
|
|
|
if !user.CheckPassword(req.Password) {
|
|
|
|
|
return nil, errors.New("invalid login or password")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate token
|
|
|
|
|
token, err := s.GenerateToken(&user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &AuthResponse{
|
|
|
|
|
Token: token,
|
|
|
|
|
User: &user,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register creates a new user account
|
|
|
|
|
func (s *AuthService) Register(req RegisterRequest) (*AuthResponse, error) {
|
|
|
|
|
// Check if user already exists
|
|
|
|
|
var existingUser models.User
|
|
|
|
|
if err := database.DB.Where("login = ?", req.Login).First(&existingUser).Error; err == nil {
|
|
|
|
|
return nil, errors.New("user already exists")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new user
|
|
|
|
|
user := models.User{
|
|
|
|
|
Login: req.Login,
|
|
|
|
|
FirstName: req.FirstName,
|
|
|
|
|
LastName: req.LastName,
|
|
|
|
|
AuthType: models.AuthTypeDatabase,
|
|
|
|
|
Token: uuid.New().String(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set password
|
|
|
|
|
if err := user.SetPassword(req.Password); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save user
|
|
|
|
|
if err := database.DB.Create(&user).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create default preference
|
|
|
|
|
preference := models.Preference{
|
|
|
|
|
UserID: user.ID,
|
|
|
|
|
}
|
|
|
|
|
if err := database.DB.Create(&preference).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate token
|
|
|
|
|
token, err := s.GenerateToken(&user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &AuthResponse{
|
|
|
|
|
Token: token,
|
|
|
|
|
User: &user,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateToken generates a JWT token for a user
|
|
|
|
|
func (s *AuthService) GenerateToken(user *models.User) (string, error) {
|
|
|
|
|
claims := jwt.MapClaims{
|
|
|
|
|
"user_id": user.ID,
|
|
|
|
|
"login": user.Login,
|
|
|
|
|
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days
|
|
|
|
|
"iat": time.Now().Unix(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
|
return token.SignedString([]byte(s.jwtSecret))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RefreshToken refreshes the user's API token
|
|
|
|
|
func (s *AuthService) RefreshToken(userID uint) (string, error) {
|
|
|
|
|
var user models.User
|
|
|
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.Token = uuid.New().String()
|
|
|
|
|
if err := database.DB.Save(&user).Error; err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.Token, nil
|
|
|
|
|
}
|
2025-11-05 11:35:36 +00:00
|
|
|
|
|
|
|
|
// CreateUser creates a new user (admin only)
|
|
|
|
|
func (s *AuthService) CreateUser(req CreateUserRequest) (*models.User, error) {
|
|
|
|
|
// Check if user already exists
|
|
|
|
|
var existingUser models.User
|
|
|
|
|
if err := database.DB.Where("login = ?", req.Login).First(&existingUser).Error; err == nil {
|
|
|
|
|
return nil, errors.New("user already exists")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new user
|
|
|
|
|
user := models.User{
|
|
|
|
|
Login: req.Login,
|
|
|
|
|
FirstName: req.FirstName,
|
|
|
|
|
LastName: req.LastName,
|
|
|
|
|
IsAdmin: req.IsAdmin,
|
|
|
|
|
AuthType: models.AuthTypeDatabase,
|
|
|
|
|
Token: uuid.New().String(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set password
|
|
|
|
|
if err := user.SetPassword(req.Password); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save user
|
|
|
|
|
if err := database.DB.Create(&user).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create default preference
|
|
|
|
|
preference := models.Preference{
|
|
|
|
|
UserID: user.ID,
|
|
|
|
|
}
|
|
|
|
|
if err := database.DB.Create(&preference).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &user, nil
|
|
|
|
|
}
|