tracks/internal/handlers/todo_handler.go

321 lines
8 KiB
Go
Raw Normal View History

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 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"})
}