package main import ( "net/http" "strconv" "sync" "time" "github.com/gin-gonic/gin" ) type Task struct { ID int64 `json:"id"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` DueAt string `json:"due_at"` Priority int `json:"priority"` Tags []string `json:"tags"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type taskStore struct { mu sync.Mutex nextID int64 items map[int64]Task } type User struct { ID int64 `json:"id"` Email string `json:"email"` Password string `json:"-"` } type authStore struct { mu sync.Mutex nextID int64 users map[string]User sessions map[string]int64 } func newTaskStore() *taskStore { return &taskStore{ nextID: 1, items: make(map[int64]Task), } } func newAuthStore() *authStore { return &authStore{ nextID: 1, users: make(map[string]User), sessions: make(map[string]int64), } } func (s *authStore) register(email, password string) (User, bool) { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.users[email]; exists { return User{}, false } user := User{ ID: s.nextID, Email: email, Password: password, } s.nextID++ s.users[email] = user return user, true } func (s *authStore) login(email, password string) (string, bool) { s.mu.Lock() defer s.mu.Unlock() user, ok := s.users[email] if !ok || user.Password != password { return "", false } token := strconv.FormatInt(time.Now().UnixNano(), 36) + "-" + strconv.FormatInt(user.ID, 10) s.sessions[token] = user.ID return token, true } func (s *authStore) validate(token string) bool { s.mu.Lock() defer s.mu.Unlock() _, ok := s.sessions[token] return ok } func (s *taskStore) list() []Task { s.mu.Lock() defer s.mu.Unlock() result := make([]Task, 0, len(s.items)) for _, t := range s.items { result = append(result, t) } return result } func (s *taskStore) get(id int64) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.items[id] return t, ok } func (s *taskStore) create(input Task) Task { s.mu.Lock() defer s.mu.Unlock() input.ID = s.nextID s.nextID++ now := time.Now().UTC() input.CreatedAt = now input.UpdatedAt = now if input.Status == "" { input.Status = "todo" } s.items[input.ID] = input return input } func (s *taskStore) update(id int64, input Task) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() existing, ok := s.items[id] if !ok { return Task{}, false } if input.Title != "" { existing.Title = input.Title } if input.Description != "" { existing.Description = input.Description } if input.Status != "" { existing.Status = input.Status } if input.DueAt != "" { existing.DueAt = input.DueAt } if input.Priority != 0 { existing.Priority = input.Priority } if input.Tags != nil { existing.Tags = input.Tags } existing.UpdatedAt = time.Now().UTC() s.items[id] = existing return existing, true } func (s *taskStore) delete(id int64) bool { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.items[id]; !ok { return false } delete(s.items, id) return true } func main() { store := newTaskStore() authStore := newAuthStore() gin.SetMode(gin.DebugMode) router := gin.Default() router.RedirectTrailingSlash = false router.RedirectFixedPath = false router.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } c.Next() }) router.GET("/api/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) router.GET("/", func(c *gin.Context) { c.File("test/web/index.html") }) router.GET("/styles.css", func(c *gin.Context) { c.File("test/web/styles.css") }) router.GET("/app.js", func(c *gin.Context) { c.File("test/web/app.js") }) auth := router.Group("/api/v1/auth") { auth.POST("/register", func(c *gin.Context) { var input struct { Email string `json:"email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if input.Email == "" || input.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "email and password required"}) return } user, ok := authStore.register(input.Email, input.Password) if !ok { c.JSON(http.StatusConflict, gin.H{"error": "user already exists"}) return } c.JSON(http.StatusCreated, gin.H{"id": user.ID, "email": user.Email}) }) auth.POST("/login", func(c *gin.Context) { var input struct { Email string `json:"email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if input.Email == "" || input.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "email and password required"}) return } token, ok := authStore.login(input.Email, input.Password) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } c.JSON(http.StatusOK, gin.H{"token": token}) }) } api := router.Group("/api/v1") api.Use(func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"}) return } token := authHeader if len(authHeader) > 7 && authHeader[:7] == "Bearer " { token = authHeader[7:] } if token == "" || !authStore.validate(token) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } c.Next() }) tasks := api.Group("/tasks") { tasks.GET("", func(c *gin.Context) { c.JSON(http.StatusOK, store.list()) }) tasks.POST("", func(c *gin.Context) { var input Task if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } created := store.create(input) c.JSON(http.StatusCreated, created) }) tasks.GET(":id", func(c *gin.Context) { id, err := parseID(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } task, ok := store.get(id) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, task) }) tasks.PUT(":id", func(c *gin.Context) { id, err := parseID(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var input Task if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updated, ok := store.update(id, input) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, updated) }) tasks.DELETE(":id", func(c *gin.Context) { id, err := parseID(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if !store.delete(id) { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.Status(http.StatusNoContent) }) } if err := router.Run(":8080"); err != nil { panic(err) } } func parseID(value string) (int64, error) { return strconv.ParseInt(value, 10, 64) }