2025-05-13 13:52:10 +02:00

372 lines
9.6 KiB
Go

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 err != nil {
log.Printf("Error getting user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
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
}
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)
}