Files
todo-vibe-coding/main.go
2026-01-22 22:42:10 +08:00

335 lines
7.5 KiB
Go

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