package main import ( "context" "embed" "errors" "gitea.seeseepuff.be/seeseemelk/mysqlite" "log" "net" "net/http" "os" "strconv" "github.com/gin-gonic/gin" ) //go:embed migrations/*.sql var migrations embed.FS var db *Db const ( ErrInternalServerError = "Internal Server Error" ErrInvalidUserID = "Invalid user ID" ErrUserNotFound = "User not found" ErrCheckingUserExist = "Error checking user existence: %v" ) // ServerConfig holds configuration for the server. type ServerConfig struct { // The datasource to the SQLite database. // Use ":memory:" for an in-memory database. Datasource string // The port to listen on. // Use an empty string to listen on a random port. Addr string // The port that is actually being listened on. Port int // The channel that gets signaled when the server has started. Started chan bool } func getUsers(c *gin.Context) { users, err := db.GetUsers() if err != nil { log.Printf("Error getting users: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, users) } func getUser(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } user, err := db.GetUser(userId) if errors.Is(err, mysqlite.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } if err != nil { log.Printf("Error getting user: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, user) } func getUserGoals(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } exists, err := db.UserExists(userId) if err != nil { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } goals, err := db.GetUserGoals(userId) if err != nil { log.Printf("Error getting user goals: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, goals) } func createUserGoal(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } // Parse request body var goalRequest CreateGoalRequest if err := c.ShouldBindJSON(&goalRequest); err != nil { log.Printf("Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Validate request if goalRequest.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"}) return } // Create goal in database goalId, err := db.CreateGoal(userId, &goalRequest) if err != nil { log.Printf("Error creating goal: %v", err) if err.Error() == "user does not exist" { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "Could not create goal"}) } return } // Return created goal ID response := CreateGoalResponse{ID: goalId} c.IndentedJSON(http.StatusCreated, response) } func deleteUserGoal(c *gin.Context) { userIdStr := c.Param("userId") goalIdStr := c.Param("goalId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } goalId, err := strconv.Atoi(goalIdStr) if err != nil { log.Printf("Invalid goal ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) return } exists, err := db.UserExists(userId) if err != nil { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } err = db.DeleteGoal(userId, goalId) if err != nil { if err.Error() == "goal not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) } else { log.Printf("Error deleting goal: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) } return } c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) } func createTask(c *gin.Context) { var taskRequest CreateTaskRequest if err := c.ShouldBindJSON(&taskRequest); err != nil { log.Printf("Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } if taskRequest.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Task name cannot be empty"}) return } // If assigned is not nil, check if user exists if taskRequest.Assigned != nil { exists, err := db.UserExists(*taskRequest.Assigned) if err != nil { log.Printf("Error checking user existence: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } } taskId, err := db.CreateTask(&taskRequest) if err != nil { log.Printf("Error creating task: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create task"}) return } response := CreateTaskResponse{ID: taskId} c.IndentedJSON(http.StatusCreated, response) } func getTasks(c *gin.Context) { response, err := db.GetTasks() if err != nil { log.Printf("Error getting tasks: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.JSON(http.StatusOK, &response) } func getTask(c *gin.Context) { taskIdStr := c.Param("taskId") taskId, err := strconv.Atoi(taskIdStr) if err != nil { log.Printf("Invalid task ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := db.GetTask(taskId) if errors.Is(err, mysqlite.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) return } if err != nil { log.Printf("Error getting task: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.JSON(http.StatusOK, &response) } func putTask(c *gin.Context) { taskIdStr := c.Param("taskId") taskId, err := strconv.Atoi(taskIdStr) if err != nil { log.Printf("Invalid task ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } var taskRequest CreateTaskRequest if err := c.ShouldBindJSON(&taskRequest); err != nil { log.Printf("Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } _, err = db.GetTask(taskId) if errors.Is(err, mysqlite.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) return } err = db.UpdateTask(taskId, &taskRequest) if err != nil { log.Printf("Error updating task: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.JSON(http.StatusOK, gin.H{"message": "Task updated successfully"}) } func postAllowance(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf("Invalid user ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) return } var allowanceRequest PostAllowance if err := c.ShouldBindJSON(&allowanceRequest); err != nil { log.Printf("Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } exists, err := db.UserExists(userId) if err != nil { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } err = db.AddAllowance(userId, &allowanceRequest) if err != nil { log.Printf("Error updating allowance: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.JSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"}) } /* * Initialises the database, and then starts the server. If the context gets cancelled, the server is shutdown and the database is closed. */ func start(ctx context.Context, config *ServerConfig) { db = NewDb(config.Datasource) defer db.db.MustClose() router := gin.Default() router.GET("/api/users", getUsers) router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId/goals", getUserGoals) router.POST("/api/user/:userId/goals", createUserGoal) router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal) router.POST("/api/tasks", createTask) router.GET("/api/tasks", getTasks) router.GET("/api/task/:taskId", getTask) router.PUT("/api/task/:taskId", putTask) router.POST("/api/user/:userId/allowance", postAllowance) srv := &http.Server{ Addr: config.Addr, Handler: router.Handler(), } go func() { l, err := net.Listen("tcp", srv.Addr) if err != nil { log.Fatalf("listen: %s\n", err) } config.Port = l.Addr().(*net.TCPAddr).Port log.Printf("Running server on port %s\n", l.Addr().String()) if config.Started != nil { config.Started <- true } if err := http.Serve(l, router.Handler()); err != nil { log.Fatalf("listen: %s\n", err) } }() <-ctx.Done() log.Println("Shutting down") if err := srv.Shutdown(context.Background()); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } } func main() { config := ServerConfig{ Datasource: os.Getenv("DB_PATH"), } start(context.Background(), &config) }