Compare commits
2 Commits
5a20e76df2
...
ba8a7e8690
Author | SHA1 | Date | |
---|---|---|---|
ba8a7e8690 | |||
1e463fec55 |
Binary file not shown.
@ -2,10 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gavv/httpexpect/v2"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gavv/httpexpect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -15,6 +16,7 @@ const (
|
|||||||
func startServer(t *testing.T) *httpexpect.Expect {
|
func startServer(t *testing.T) *httpexpect.Expect {
|
||||||
config := ServerConfig{
|
config := ServerConfig{
|
||||||
Datasource: ":memory:",
|
Datasource: ":memory:",
|
||||||
|
//Datasource: "test.db",
|
||||||
Addr: ":0",
|
Addr: ":0",
|
||||||
Started: make(chan bool),
|
Started: make(chan bool),
|
||||||
}
|
}
|
||||||
@ -284,6 +286,54 @@ func TestCreateTask(t *testing.T) {
|
|||||||
responseWithUser.Value("id").Number().IsEqual(2)
|
responseWithUser.Value("id").Number().IsEqual(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateScheduleTask(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// Create a new task without assigned user
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"name": "Test Task",
|
||||||
|
"reward": 100,
|
||||||
|
"schedule": "0 */5 * * * *",
|
||||||
|
}
|
||||||
|
|
||||||
|
response := e.POST("/tasks").
|
||||||
|
WithJSON(requestBody).
|
||||||
|
Expect().
|
||||||
|
Status(201). // Expect Created status
|
||||||
|
JSON().Object()
|
||||||
|
|
||||||
|
requestBody["schedule"] = "every 5 seconds"
|
||||||
|
e.POST("/tasks").WithJSON(requestBody).Expect().Status(400)
|
||||||
|
|
||||||
|
// Verify the response has an ID
|
||||||
|
response.ContainsKey("id")
|
||||||
|
response.Value("id").Number().IsEqual(1)
|
||||||
|
|
||||||
|
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
|
||||||
|
|
||||||
|
// Get task
|
||||||
|
result := e.GET("/task/1").Expect().Status(200).JSON().Object()
|
||||||
|
result.Value("id").IsEqual(1)
|
||||||
|
result.Value("name").IsEqual("Test Task")
|
||||||
|
result.Value("schedule").IsEqual("0 */5 * * * *")
|
||||||
|
result.Value("reward").IsEqual(100)
|
||||||
|
result.Value("assigned").IsNull()
|
||||||
|
|
||||||
|
// Complete the task
|
||||||
|
e.POST("/task/1/complete").Expect().Status(200)
|
||||||
|
|
||||||
|
// Set expires date to 1 second in the past
|
||||||
|
db.db.Query("update tasks set next_run = ? where id = 1").Bind(time.Now().Add(10 * -time.Minute).Unix()).MustExec()
|
||||||
|
|
||||||
|
// Verify a new task is created
|
||||||
|
newTask := e.GET("/task/2").Expect().Status(200).JSON().Object()
|
||||||
|
newTask.Value("id").IsEqual(2)
|
||||||
|
newTask.Value("name").IsEqual("Test Task")
|
||||||
|
newTask.Value("schedule").IsEqual("0 */5 * * * *")
|
||||||
|
newTask.Value("reward").IsEqual(100)
|
||||||
|
newTask.Value("assigned").IsNull()
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteTask(t *testing.T) {
|
func TestDeleteTask(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
@ -914,3 +964,88 @@ func createTestAllowance(e *httpexpect.Expect, name string, target float64, weig
|
|||||||
func createTestTask(e *httpexpect.Expect) int {
|
func createTestTask(e *httpexpect.Expect) int {
|
||||||
return createTestTaskWithAmount(e, 100)
|
return createTestTaskWithAmount(e, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transfer tests
|
||||||
|
func TestTransferSuccessful(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// Create two allowances for user 1
|
||||||
|
createTestAllowance(e, "From Allowance", 100, 1)
|
||||||
|
createTestAllowance(e, "To Allowance", 100, 1)
|
||||||
|
|
||||||
|
// Add 30 to allowance 1
|
||||||
|
req := map[string]interface{}{"amount": 30, "description": "funds"}
|
||||||
|
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||||
|
|
||||||
|
// Transfer 10 from 1 to 2
|
||||||
|
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||||
|
e.POST("/transfer").WithJSON(transfer).Expect().Status(200).JSON().Object().Value("message").IsEqual("Transfer successful")
|
||||||
|
|
||||||
|
// Verify balances
|
||||||
|
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
|
allowances.Value(1).Object().Value("progress").Number().InDelta(20.0, 0.01)
|
||||||
|
allowances.Value(2).Object().Value("progress").Number().InDelta(10.0, 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferCapsAtTarget(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// Create two allowances
|
||||||
|
createTestAllowance(e, "From Allowance", 100, 1)
|
||||||
|
createTestAllowance(e, "To Allowance", 5, 1)
|
||||||
|
|
||||||
|
// Add 10 to allowance 1
|
||||||
|
req := map[string]interface{}{"amount": 10, "description": "funds"}
|
||||||
|
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||||
|
|
||||||
|
// Transfer 10 from 1 to 2, but to only needs 5
|
||||||
|
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||||
|
e.POST("/transfer").WithJSON(transfer).Expect().Status(200)
|
||||||
|
|
||||||
|
// Verify capped transfer
|
||||||
|
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
|
allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) // from had 10, transferred 5 -> left 5
|
||||||
|
allowances.Value(2).Object().Value("progress").Number().InDelta(5.0, 0.01) // to reached target
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferDifferentUsersFails(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// Create allowance for user 1 and user 2
|
||||||
|
createTestAllowance(e, "User1 Allowance", 100, 1)
|
||||||
|
// create for user 2
|
||||||
|
e.POST("/user/2/allowance").WithJSON(CreateAllowanceRequest{Name: "User2 Allowance", Target: 100, Weight: 1}).Expect().Status(201)
|
||||||
|
|
||||||
|
// Add to user1 allowance
|
||||||
|
req := map[string]interface{}{"amount": 10, "description": "funds"}
|
||||||
|
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
|
||||||
|
|
||||||
|
// Attempt transfer between different users
|
||||||
|
transfer := map[string]interface{}{"from": 1, "to": 1 /* wrong id to simulate different user's id? */}
|
||||||
|
// To ensure different user, fetch the allowance id for user2 (it's 1 for user2 in its own context but global id will be 2)
|
||||||
|
// Create above for user2 produced global id 2, so use that
|
||||||
|
transfer = map[string]interface{}{"from": 1, "to": 2, "amount": 5}
|
||||||
|
e.POST("/transfer").WithJSON(transfer).Expect().Status(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferInsufficientFunds(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// Create two allowances
|
||||||
|
createTestAllowance(e, "From Allowance", 100, 1)
|
||||||
|
createTestAllowance(e, "To Allowance", 100, 1)
|
||||||
|
|
||||||
|
// Ensure from has 0 balance
|
||||||
|
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
|
||||||
|
resp := e.POST("/transfer").WithJSON(transfer).Expect().Status(400).JSON().Object()
|
||||||
|
// Error text should mention insufficient funds
|
||||||
|
resp.Value("error").String().Contains("insufficient")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferNotFound(t *testing.T) {
|
||||||
|
e := startServer(t)
|
||||||
|
|
||||||
|
// No allowances exist yet (only user rows). Attempt transfer with non-existent IDs
|
||||||
|
transfer := map[string]interface{}{"from": 999, "to": 1000, "amount": 1}
|
||||||
|
e.POST("/transfer").WithJSON(transfer).Expect().Status(404)
|
||||||
|
}
|
||||||
|
176
backend/db.go
176
backend/db.go
@ -7,6 +7,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/adhocore/gronx"
|
||||||
|
|
||||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -313,10 +315,20 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
var nextRun *int64
|
||||||
|
if task.Schedule != nil {
|
||||||
|
nextRunTime, err := gronx.NextTick(*task.Schedule, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to calculate next run: %w", err)
|
||||||
|
}
|
||||||
|
nextRunTimeAsInt := nextRunTime.Unix()
|
||||||
|
nextRun = &nextRunTimeAsInt
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the new task
|
// Insert the new task
|
||||||
reward := int(math.Round(task.Reward * 100.0))
|
reward := int(math.Round(task.Reward * 100.0))
|
||||||
err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
|
err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) values (?, ?, ?, ?, ?)").
|
||||||
Bind(task.Name, reward, task.Assigned).
|
Bind(task.Name, reward, task.Assigned, task.Schedule, nextRun).
|
||||||
Exec()
|
Exec()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -340,13 +352,17 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *Db) GetTasks() ([]Task, error) {
|
func (db *Db) GetTasks() ([]Task, error) {
|
||||||
tasks := make([]Task, 0)
|
err := db.UpdateScheduledTasks()
|
||||||
var err error
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update scheduled tasks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
|
tasks := make([]Task, 0)
|
||||||
|
|
||||||
|
for row := range db.db.Query("select id, name, reward, assigned, schedule from tasks where completed is null").Range(&err) {
|
||||||
task := Task{}
|
task := Task{}
|
||||||
var reward int64
|
var reward int64
|
||||||
err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned)
|
err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule)
|
||||||
task.Reward = float64(reward) / 100.0
|
task.Reward = float64(reward) / 100.0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -362,16 +378,78 @@ func (db *Db) GetTasks() ([]Task, error) {
|
|||||||
func (db *Db) GetTask(id int) (Task, error) {
|
func (db *Db) GetTask(id int) (Task, error) {
|
||||||
task := Task{}
|
task := Task{}
|
||||||
|
|
||||||
var reward int64
|
err := db.UpdateScheduledTasks()
|
||||||
err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
|
|
||||||
Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned)
|
|
||||||
task.Reward = float64(reward) / 100.0
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Task{}, err
|
return Task{}, fmt.Errorf("failed to update scheduled tasks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reward int64
|
||||||
|
err = db.db.Query("select id, name, reward, assigned, schedule from tasks where id = ? and completed is null").
|
||||||
|
Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule)
|
||||||
|
if err != nil {
|
||||||
|
return task, err
|
||||||
|
}
|
||||||
|
task.Reward = float64(reward) / 100.0
|
||||||
return task, nil
|
return task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Db) UpdateScheduledTasks() error {
|
||||||
|
type ScheduledTask struct {
|
||||||
|
ID int
|
||||||
|
Schedule string
|
||||||
|
Expires int64
|
||||||
|
}
|
||||||
|
tasks := make([]ScheduledTask, 0)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for row := range db.db.Query("select id, schedule, next_run from tasks where schedule is not null").Range(&err) {
|
||||||
|
task := ScheduledTask{}
|
||||||
|
err := row.Scan(&task.ID, &task.Schedule, &task.Expires)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if time.Now().Unix() >= task.Expires {
|
||||||
|
tasks = append(tasks, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch scheduled tasks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
nextRun, err := gronx.NextTickAfter(task.Schedule, time.Now(), false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to calculate next run for task %d: %w", task.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) select name, reward, assigned, schedule, ? from tasks where id = ?").
|
||||||
|
Bind(nextRun.Unix(), task.ID).
|
||||||
|
Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Query("update tasks set schedule = null where id = ?").Bind(task.ID).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Query("select last_insert_rowid()").MustScanSingle(&task.ID)
|
||||||
|
log.Printf("Task %d scheduled for %s", task.ID, nextRun)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Db) DeleteTask(id int) error {
|
func (db *Db) DeleteTask(id int) error {
|
||||||
tx, err := db.db.Begin()
|
tx, err := db.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -453,7 +531,10 @@ func (db *Db) CompleteTask(taskId int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove the task
|
// Remove the task
|
||||||
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
|
err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
@ -631,3 +712,74 @@ func (db *Db) AddAllowanceAmount(userId int, allowanceId int, request AddAllowan
|
|||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransferAllowance transfers amount from one allowance goal to another.
|
||||||
|
// Both allowance ids must exist and belong to the same user. The transfer
|
||||||
|
// will not move more than the 'to' goal still needs (target - balance).
|
||||||
|
func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error {
|
||||||
|
if fromId == toId {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
amountCents := int(math.Round(amount * 100.0))
|
||||||
|
if amountCents <= 0 {
|
||||||
|
return fmt.Errorf("amount must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
// Fetch from allowance (user_id, balance)
|
||||||
|
var fromUserId int
|
||||||
|
var fromBalance int
|
||||||
|
err = tx.Query("select user_id, balance from allowances where id = ?").Bind(fromId).ScanSingle(&fromUserId, &fromBalance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch to allowance (user_id, target, balance)
|
||||||
|
var toUserId int
|
||||||
|
var toTarget int
|
||||||
|
var toBalance int
|
||||||
|
err = tx.Query("select user_id, target, balance from allowances where id = ?").Bind(toId).ScanSingle(&toUserId, &toTarget, &toBalance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure same owner
|
||||||
|
if fromUserId != toUserId {
|
||||||
|
return fmt.Errorf("allowances do not belong to the same user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how much the 'to' goal still needs
|
||||||
|
remainingTo := toTarget - toBalance
|
||||||
|
if remainingTo <= 0 {
|
||||||
|
// Nothing to transfer
|
||||||
|
return fmt.Errorf("target already reached")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit transfer to what 'to' still needs
|
||||||
|
transfer := amountCents
|
||||||
|
if transfer > remainingTo {
|
||||||
|
transfer = remainingTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure 'from' has enough balance
|
||||||
|
if fromBalance < transfer {
|
||||||
|
return fmt.Errorf("insufficient funds in source allowance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform updates
|
||||||
|
err = tx.Query("update allowances set balance = balance - ? where id = ? and user_id = ?").Bind(transfer, fromId, fromUserId).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").Bind(transfer, toId, toUserId).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
@ -29,7 +29,8 @@ type Task struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Reward float64 `json:"reward"`
|
Reward float64 `json:"reward"`
|
||||||
Assigned *int `json:"assigned"` // Pointer to allow null
|
Assigned *int `json:"assigned"`
|
||||||
|
Schedule *string `json:"schedule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Allowance struct {
|
type Allowance struct {
|
||||||
@ -68,6 +69,7 @@ type CreateTaskRequest struct {
|
|||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Reward float64 `json:"reward"`
|
Reward float64 `json:"reward"`
|
||||||
Assigned *int `json:"assigned"`
|
Assigned *int `json:"assigned"`
|
||||||
|
Schedule *string `json:"schedule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateTaskResponse struct {
|
type CreateTaskResponse struct {
|
||||||
@ -78,3 +80,10 @@ type AddAllowanceAmountRequest struct {
|
|||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransferRequest represents a request to transfer amount between two goals.
|
||||||
|
type TransferRequest struct {
|
||||||
|
From int `json:"from"`
|
||||||
|
To int `json:"to"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
@ -6,11 +6,12 @@ require (
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
||||||
github.com/gavv/httpexpect/v2 v2.17.0
|
github.com/gavv/httpexpect/v2 v2.17.0
|
||||||
github.com/gin-contrib/cors v1.7.5
|
github.com/gin-contrib/cors v1.7.5
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
||||||
|
github.com/adhocore/gronx v1.19.6 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
@ -49,7 +50,7 @@ require (
|
|||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/sergi/go-diff v1.3.1 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.14 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.62.0 // indirect
|
github.com/valyala/fasthttp v1.62.0 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
@ -68,10 +69,10 @@ require (
|
|||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.65.7 // indirect
|
modernc.org/libc v1.65.8 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.37.0 // indirect
|
modernc.org/sqlite v1.37.1 // indirect
|
||||||
moul.io/http2curl/v2 v2.3.0 // indirect
|
moul.io/http2curl/v2 v2.3.0 // indirect
|
||||||
zombiezen.com/go/sqlite v1.4.0 // indirect
|
zombiezen.com/go/sqlite v1.4.2 // indirect
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,8 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
||||||
|
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
|
||||||
|
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
@ -34,6 +36,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@ -127,6 +131,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
|
||||||
|
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
||||||
@ -216,6 +222,8 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
|
modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
|
||||||
|
modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@ -226,6 +234,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
|
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||||
|
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
@ -235,3 +245,5 @@ moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHc
|
|||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
|
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
|
||||||
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
|
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
|
||||||
|
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
|
||||||
|
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=
|
||||||
|
@ -4,13 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||||
|
"github.com/adhocore/gronx"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -436,6 +439,14 @@ func createTask(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if taskRequest.Schedule != nil {
|
||||||
|
valid := gronx.IsValid(*taskRequest.Schedule)
|
||||||
|
if !valid {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid cron schedule: %s", *taskRequest.Schedule)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If assigned is not nil, check if user exists
|
// If assigned is not nil, check if user exists
|
||||||
if taskRequest.Assigned != nil {
|
if taskRequest.Assigned != nil {
|
||||||
exists, err := db.UserExists(*taskRequest.Assigned)
|
exists, err := db.UserExists(*taskRequest.Assigned)
|
||||||
@ -513,6 +524,11 @@ func putTask(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting task: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = db.UpdateTask(taskId, &taskRequest)
|
err = db.UpdateTask(taskId, &taskRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -638,10 +654,68 @@ func getHistory(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusOK, history)
|
c.IndentedJSON(http.StatusOK, history)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// TransferRequest handler: accepts JSON with from, to, amount and transfers between allowances
|
||||||
Initialises the database, and then starts the server.
|
func transferHandler(c *gin.Context) {
|
||||||
If the context gets cancelled, the server is shutdown and the database is closed.
|
var req TransferRequest
|
||||||
*/
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("Error parsing transfer request: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.From == 0 || req.To == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Amount <= 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Amount must be positive"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both allowances exist and fetch owners
|
||||||
|
var fromUserId int
|
||||||
|
err := db.db.Query("select user_id from allowances where id = ?").Bind(req.From).ScanSingle(&fromUserId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Source allowance not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Error checking source allowance: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var toUserId int
|
||||||
|
err = db.db.Query("select user_id from allowances where id = ?").Bind(req.To).ScanSingle(&toUserId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Destination allowance not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Error checking destination allowance: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromUserId != toUserId {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowances do not belong to the same user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform transfer
|
||||||
|
err = db.TransferAllowance(req.From, req.To, req.Amount)
|
||||||
|
if err != nil {
|
||||||
|
// Map common errors to 400
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Transfer successful"})
|
||||||
|
}
|
||||||
|
|
||||||
func start(ctx context.Context, config *ServerConfig) {
|
func start(ctx context.Context, config *ServerConfig) {
|
||||||
db = NewDb(config.Datasource)
|
db = NewDb(config.Datasource)
|
||||||
defer db.db.MustClose()
|
defer db.db.MustClose()
|
||||||
@ -673,6 +747,8 @@ func start(ctx context.Context, config *ServerConfig) {
|
|||||||
router.PUT("/api/task/:taskId", putTask)
|
router.PUT("/api/task/:taskId", putTask)
|
||||||
router.DELETE("/api/task/:taskId", deleteTask)
|
router.DELETE("/api/task/:taskId", deleteTask)
|
||||||
router.POST("/api/task/:taskId/complete", completeTask)
|
router.POST("/api/task/:taskId/complete", completeTask)
|
||||||
|
// transfer endpoint
|
||||||
|
router.POST("/api/transfer", transferHandler)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: config.Addr,
|
Addr: config.Addr,
|
||||||
|
3
backend/migrations/5_add_schedules.sql
Normal file
3
backend/migrations/5_add_schedules.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
alter table tasks add column schedule text;
|
||||||
|
alter table tasks add column completed date;
|
||||||
|
alter table tasks add column next_run date;
|
@ -71,10 +71,18 @@ func renderCreateTask(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.CreateTask(&CreateTaskRequest{
|
request := &CreateTaskRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
Reward: reward,
|
Reward: reward,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
schedule := c.PostForm("schedule")
|
||||||
|
if schedule != "" {
|
||||||
|
request.Schedule = &schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.CreateTask(request)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderError(c, http.StatusInternalServerError, err)
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -81,6 +81,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Assigned</th>
|
<th>Assigned</th>
|
||||||
<th>Reward</th>
|
<th>Reward</th>
|
||||||
|
<th>Schedule</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -96,6 +97,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{.Reward}}</td>
|
<td>{{.Reward}}</td>
|
||||||
|
<td>{{.Schedule}}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/completeTask?task={{.ID}}">Mark as completed</a>
|
<a href="/completeTask?task={{.ID}}">Mark as completed</a>
|
||||||
</td>
|
</td>
|
||||||
@ -105,6 +107,7 @@
|
|||||||
<td><label><input type="text" name="name" placeholder="Name"></label></td>
|
<td><label><input type="text" name="name" placeholder="Name"></label></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
|
<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
|
||||||
|
<td><label><input type="text" name="schedule" placeholder="Schedule"></label></td>
|
||||||
<td><input type="submit" value="Create"></td>
|
<td><input type="submit" value="Create"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -422,7 +422,10 @@ components:
|
|||||||
description: The task name
|
description: The task name
|
||||||
reward:
|
reward:
|
||||||
type: integer
|
type: integer
|
||||||
description: The task reward, in cents
|
description: The task reward
|
||||||
|
schedule:
|
||||||
|
type: string
|
||||||
|
description: The schedule of the task, in cron format
|
||||||
assigned:
|
assigned:
|
||||||
type: integer
|
type: integer
|
||||||
description: The user ID of the user assigned to the task
|
description: The user ID of the user assigned to the task
|
||||||
|
Loading…
x
Reference in New Issue
Block a user