313 lines
7.9 KiB
Go
313 lines
7.9 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)
|
|
}
|
|
|
|
/*
|
|
*
|
|
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)
|
|
|
|
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)
|
|
}
|