init
This commit is contained in:
334
main.go
Normal file
334
main.go
Normal file
@@ -0,0 +1,334 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user