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.
This commit is contained in:
Claude 2025-11-05 10:46:59 +00:00
parent 6613d33f10
commit f0eb4bdef5
No known key found for this signature in database
29 changed files with 4100 additions and 104 deletions

View file

@ -0,0 +1,96 @@
package handlers
import (
"net/http"
"github.com/TracksApp/tracks/internal/middleware"
"github.com/TracksApp/tracks/internal/services"
"github.com/gin-gonic/gin"
)
// AuthHandler handles authentication endpoints
type AuthHandler struct {
authService *services.AuthService
}
// NewAuthHandler creates a new AuthHandler
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
return &AuthHandler{
authService: authService,
}
}
// Login handles POST /api/login
func (h *AuthHandler) Login(c *gin.Context) {
var req services.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.authService.Login(req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
// Set cookie
c.SetCookie("tracks_token", resp.Token, 60*60*24*7, "/", "", false, true)
c.JSON(http.StatusOK, resp)
}
// Register handles POST /api/register
func (h *AuthHandler) Register(c *gin.Context) {
var req services.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.authService.Register(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Set cookie
c.SetCookie("tracks_token", resp.Token, 60*60*24*7, "/", "", false, true)
c.JSON(http.StatusCreated, resp)
}
// Logout handles POST /api/logout
func (h *AuthHandler) Logout(c *gin.Context) {
// Clear cookie
c.SetCookie("tracks_token", "", -1, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
// Me handles GET /api/me
func (h *AuthHandler) Me(c *gin.Context) {
user, err := middleware.GetCurrentUser(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
c.JSON(http.StatusOK, user)
}
// RefreshToken handles POST /api/refresh-token
func (h *AuthHandler) RefreshToken(c *gin.Context) {
user, err := middleware.GetCurrentUser(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
token, err := h.authService.RefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}

View file

@ -0,0 +1,230 @@
package handlers
import (
"net/http"
"strconv"
"github.com/TracksApp/tracks/internal/middleware"
"github.com/TracksApp/tracks/internal/models"
"github.com/TracksApp/tracks/internal/services"
"github.com/gin-gonic/gin"
)
// ContextHandler handles context endpoints
type ContextHandler struct {
contextService *services.ContextService
}
// NewContextHandler creates a new ContextHandler
func NewContextHandler(contextService *services.ContextService) *ContextHandler {
return &ContextHandler{
contextService: contextService,
}
}
// ListContexts handles GET /api/contexts
func (h *ContextHandler) ListContexts(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
state := models.ContextState(c.Query("state"))
contexts, err := h.contextService.GetContexts(userID, state)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, contexts)
}
// GetContext handles GET /api/contexts/:id
func (h *ContextHandler) GetContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
context, err := h.contextService.GetContext(userID, uint(contextID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, context)
}
// CreateContext handles POST /api/contexts
func (h *ContextHandler) CreateContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
var req services.CreateContextRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context, err := h.contextService.CreateContext(userID, req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, context)
}
// UpdateContext handles PUT /api/contexts/:id
func (h *ContextHandler) UpdateContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
var req services.UpdateContextRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context, err := h.contextService.UpdateContext(userID, uint(contextID), req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, context)
}
// DeleteContext handles DELETE /api/contexts/:id
func (h *ContextHandler) DeleteContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
if err := h.contextService.DeleteContext(userID, uint(contextID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Context deleted"})
}
// HideContext handles POST /api/contexts/:id/hide
func (h *ContextHandler) HideContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
context, err := h.contextService.HideContext(userID, uint(contextID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, context)
}
// ActivateContext handles POST /api/contexts/:id/activate
func (h *ContextHandler) ActivateContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
context, err := h.contextService.ActivateContext(userID, uint(contextID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, context)
}
// CloseContext handles POST /api/contexts/:id/close
func (h *ContextHandler) CloseContext(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
context, err := h.contextService.CloseContext(userID, uint(contextID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, context)
}
// GetContextStats handles GET /api/contexts/:id/stats
func (h *ContextHandler) GetContextStats(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
contextID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid context ID"})
return
}
stats, err := h.contextService.GetContextStats(userID, uint(contextID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}

View file

@ -0,0 +1,253 @@
package handlers
import (
"net/http"
"strconv"
"github.com/TracksApp/tracks/internal/middleware"
"github.com/TracksApp/tracks/internal/models"
"github.com/TracksApp/tracks/internal/services"
"github.com/gin-gonic/gin"
)
// ProjectHandler handles project endpoints
type ProjectHandler struct {
projectService *services.ProjectService
}
// NewProjectHandler creates a new ProjectHandler
func NewProjectHandler(projectService *services.ProjectService) *ProjectHandler {
return &ProjectHandler{
projectService: projectService,
}
}
// ListProjects handles GET /api/projects
func (h *ProjectHandler) ListProjects(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
state := models.ProjectState(c.Query("state"))
projects, err := h.projectService.GetProjects(userID, state)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, projects)
}
// GetProject handles GET /api/projects/:id
func (h *ProjectHandler) GetProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
project, err := h.projectService.GetProject(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// CreateProject handles POST /api/projects
func (h *ProjectHandler) CreateProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
var req services.CreateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
project, err := h.projectService.CreateProject(userID, req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, project)
}
// UpdateProject handles PUT /api/projects/:id
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
var req services.UpdateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
project, err := h.projectService.UpdateProject(userID, uint(projectID), req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// DeleteProject handles DELETE /api/projects/:id
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
if err := h.projectService.DeleteProject(userID, uint(projectID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Project deleted"})
}
// CompleteProject handles POST /api/projects/:id/complete
func (h *ProjectHandler) CompleteProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
project, err := h.projectService.CompleteProject(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// ActivateProject handles POST /api/projects/:id/activate
func (h *ProjectHandler) ActivateProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
project, err := h.projectService.ActivateProject(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// HideProject handles POST /api/projects/:id/hide
func (h *ProjectHandler) HideProject(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
project, err := h.projectService.HideProject(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// MarkReviewed handles POST /api/projects/:id/review
func (h *ProjectHandler) MarkReviewed(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
project, err := h.projectService.MarkProjectReviewed(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, project)
}
// GetProjectStats handles GET /api/projects/:id/stats
func (h *ProjectHandler) GetProjectStats(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
return
}
stats, err := h.projectService.GetProjectStats(userID, uint(projectID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}

View file

@ -0,0 +1,320 @@
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/TracksApp/tracks/internal/middleware"
"github.com/TracksApp/tracks/internal/models"
"github.com/TracksApp/tracks/internal/services"
"github.com/gin-gonic/gin"
)
// TodoHandler handles todo endpoints
type TodoHandler struct {
todoService *services.TodoService
}
// NewTodoHandler creates a new TodoHandler
func NewTodoHandler(todoService *services.TodoService) *TodoHandler {
return &TodoHandler{
todoService: todoService,
}
}
// ListTodos handles GET /api/todos
func (h *TodoHandler) ListTodos(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
// Parse filters from query parameters
filter := services.ListTodosFilter{
IncludeTags: c.Query("include_tags") == "true",
}
if state := c.Query("state"); state != "" {
filter.State = models.TodoState(state)
}
if contextIDStr := c.Query("context_id"); contextIDStr != "" {
if contextID, err := strconv.ParseUint(contextIDStr, 10, 32); err == nil {
id := uint(contextID)
filter.ContextID = &id
}
}
if projectIDStr := c.Query("project_id"); projectIDStr != "" {
if projectID, err := strconv.ParseUint(projectIDStr, 10, 32); err == nil {
id := uint(projectID)
filter.ProjectID = &id
}
}
if tagName := c.Query("tag"); tagName != "" {
filter.TagName = &tagName
}
if c.Query("starred") == "true" {
starred := true
filter.Starred = &starred
}
if c.Query("overdue") == "true" {
overdue := true
filter.Overdue = &overdue
}
if c.Query("due_today") == "true" {
dueToday := true
filter.DueToday = &dueToday
}
if c.Query("show_from") == "true" {
showFrom := true
filter.ShowFrom = &showFrom
}
todos, err := h.todoService.GetTodos(userID, filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todos)
}
// GetTodo handles GET /api/todos/:id
func (h *TodoHandler) GetTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
todo, err := h.todoService.GetTodo(userID, uint(todoID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todo)
}
// CreateTodo handles POST /api/todos
func (h *TodoHandler) CreateTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
var req services.CreateTodoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo, err := h.todoService.CreateTodo(userID, req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, todo)
}
// UpdateTodo handles PUT /api/todos/:id
func (h *TodoHandler) UpdateTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
var req services.UpdateTodoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo, err := h.todoService.UpdateTodo(userID, uint(todoID), req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todo)
}
// DeleteTodo handles DELETE /api/todos/:id
func (h *TodoHandler) DeleteTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
if err := h.todoService.DeleteTodo(userID, uint(todoID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Todo deleted"})
}
// CompleteTodo handles POST /api/todos/:id/complete
func (h *TodoHandler) CompleteTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
todo, err := h.todoService.CompleteTodo(userID, uint(todoID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todo)
}
// ActivateTodo handles POST /api/todos/:id/activate
func (h *TodoHandler) ActivateTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
todo, err := h.todoService.ActivateTodo(userID, uint(todoID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todo)
}
// DeferTodo handles POST /api/todos/:id/defer
func (h *TodoHandler) DeferTodo(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
todoID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
var req struct {
ShowFrom time.Time `json:"show_from" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo, err := h.todoService.DeferTodo(userID, uint(todoID), req.ShowFrom)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, todo)
}
// AddDependency handles POST /api/todos/:id/dependencies
func (h *TodoHandler) AddDependency(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
predecessorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
var req struct {
SuccessorID uint `json:"successor_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.todoService.AddDependency(userID, uint(predecessorID), req.SuccessorID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "Dependency added"})
}
// RemoveDependency handles DELETE /api/todos/:id/dependencies/:successor_id
func (h *TodoHandler) RemoveDependency(c *gin.Context) {
userID, err := middleware.GetCurrentUserID(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
predecessorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid todo ID"})
return
}
successorID, err := strconv.ParseUint(c.Param("successor_id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid successor ID"})
return
}
if err := h.todoService.RemoveDependency(userID, uint(predecessorID), uint(successorID)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Dependency removed"})
}