Compare commits
2 Commits
main
...
feature/sh
| Author | SHA1 | Date | |
|---|---|---|---|
| f4420bd895 | |||
| f0126c656a |
@ -50,12 +50,24 @@ func CreateNewTodo() common.Todo {
|
|||||||
for i, l := range labelSlice {
|
for i, l := range labelSlice {
|
||||||
labelSlice[i] = strings.TrimSpace(l)
|
labelSlice[i] = strings.TrimSpace(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharedInput := common.AskUserString("Shared with (comma separated usernames):\n")
|
||||||
|
sharedSlice := strings.Split(sharedInput, ",")
|
||||||
|
var sharedWith []string
|
||||||
|
for _, s := range sharedSlice {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
sharedWith = append(sharedWith, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return common.Todo{
|
return common.Todo{
|
||||||
Name: common.AskUserString("Name:\n"),
|
Name: common.AskUserString("Name:\n"),
|
||||||
Description: common.AskUserString("Description:\n"),
|
Description: common.AskUserString("Description:\n"),
|
||||||
Status: common.AskUserString(fmt.Sprintf("Status (%s, %s, %s, %s, %s, %s):\n",
|
Status: common.AskUserString(fmt.Sprintf("Status (%s, %s, %s, %s, %s, %s):\n",
|
||||||
common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed)),
|
common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed)),
|
||||||
Labels: labelSlice,
|
Labels: labelSlice,
|
||||||
|
SharedWith: sharedWith,
|
||||||
Owner: cfg.Server.Credentials.Username,
|
Owner: cfg.Server.Credentials.Username,
|
||||||
LastModified: time.Now().UTC(),
|
LastModified: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9
|
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/urfave/cli/v3 v3.8.0
|
github.com/urfave/cli/v3 v3.8.0
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 h1:EGFIRDjHIEt0IEFYeN2NEn/NyVglN6vXB6IRjm1rN0I=
|
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d h1:YpsaXckG7ryvEe7cTwRrGNOB43wLnMlK0vXqYMHdrXQ=
|
||||||
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9/go.mod h1:bHquapurFm/eUTtrl9mGLEdAYc5cOeueHFvqjommp44=
|
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
|||||||
@ -141,6 +141,21 @@ func handleUpdate(scanner *bufio.Scanner, todos []common.Todo, store *common.Bol
|
|||||||
itemToUpdate.Status = newStatus
|
itemToUpdate.Status = newStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("New shared with (comma separated) [%s]: ", strings.Join(itemToUpdate.SharedWith, ", "))
|
||||||
|
scanner.Scan()
|
||||||
|
newSharedWith := strings.TrimSpace(scanner.Text())
|
||||||
|
if newSharedWith != "" {
|
||||||
|
sharedSlice := strings.Split(newSharedWith, ",")
|
||||||
|
var sharedWith []string
|
||||||
|
for _, s := range sharedSlice {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
sharedWith = append(sharedWith, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemToUpdate.SharedWith = sharedWith
|
||||||
|
}
|
||||||
|
|
||||||
err = itemToUpdate.Store(store, cfg.Server.Credentials.Username)
|
err = itemToUpdate.Store(store, cfg.Server.Credentials.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -219,25 +221,38 @@ func (s *BoltStore) ExistsByKey(bucket, key string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoltStore) GetTodoMap(user string) map[string]Todo {
|
func (s *BoltStore) GetTodoMap(user string) map[string]Todo {
|
||||||
storedTodoJsons := s.GetAllFromBucket(user)
|
|
||||||
|
|
||||||
serverTodos := make(map[string]Todo)
|
serverTodos := make(map[string]Todo)
|
||||||
for key, val := range storedTodoJsons {
|
|
||||||
|
s.DB.View(func(tx *bolt.Tx) error {
|
||||||
|
return tx.ForEach(func(name []byte, b *bolt.Bucket) error {
|
||||||
|
bucketName := string(name)
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
var todo Todo
|
var todo Todo
|
||||||
if json.Unmarshal([]byte(val), &todo) == nil {
|
if json.Unmarshal(v, &todo) == nil {
|
||||||
serverTodos[key] = todo
|
if bucketName == user || slices.Contains(todo.SharedWith, user) {
|
||||||
|
serverTodos[string(k)] = todo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return serverTodos
|
return serverTodos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo {
|
func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo {
|
||||||
storedTodoJsons := s.GetAllFromBucket(user)
|
|
||||||
|
|
||||||
var storedTodos []Todo
|
var storedTodos []Todo
|
||||||
for _, val := range storedTodoJsons {
|
|
||||||
|
s.DB.View(func(tx *bolt.Tx) error {
|
||||||
|
return tx.ForEach(func(name []byte, b *bolt.Bucket) error {
|
||||||
|
bucketName := string(name)
|
||||||
|
c := b.Cursor()
|
||||||
|
for _, v := c.First(); v != nil; _, v = c.Next() {
|
||||||
var todo Todo
|
var todo Todo
|
||||||
if json.Unmarshal([]byte(val), &todo) == nil {
|
if json.Unmarshal(v, &todo) == nil {
|
||||||
|
if bucketName == user || slices.Contains(todo.SharedWith, user) {
|
||||||
var include = false
|
var include = false
|
||||||
if includeDeleted {
|
if includeDeleted {
|
||||||
include = true
|
include = true
|
||||||
@ -249,5 +264,10 @@ func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return storedTodos
|
return storedTodos
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,8 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/common
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
@ -18,7 +17,6 @@ require (
|
|||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
@ -14,14 +12,12 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg
|
|||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
@ -42,15 +38,13 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,7 +13,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (todo *Todo) Store(store *BoltStore, user string) error {
|
func (todo *Todo) Store(store *BoltStore, user string) error {
|
||||||
if todo.Owner != user {
|
isOwner := todo.Owner == user
|
||||||
|
isShared := slices.Contains(todo.SharedWith, user)
|
||||||
|
|
||||||
|
if !isOwner && !isShared {
|
||||||
return fmt.Errorf("unauthorized user")
|
return fmt.Errorf("unauthorized user")
|
||||||
}
|
}
|
||||||
if todo.Id == "" {
|
if todo.Id == "" {
|
||||||
@ -23,7 +27,7 @@ func (todo *Todo) Store(store *BoltStore, user string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return store.SaveValueToBucket(user, todo.Id, string(todoJson))
|
return store.SaveValueToBucket(todo.Owner, todo.Id, string(todoJson))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error {
|
func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error {
|
||||||
@ -33,6 +37,7 @@ func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error {
|
|||||||
Description: todoRequest.Description,
|
Description: todoRequest.Description,
|
||||||
Status: todoRequest.Status,
|
Status: todoRequest.Status,
|
||||||
Owner: user,
|
Owner: user,
|
||||||
|
SharedWith: todoRequest.SharedWith,
|
||||||
LastModified: time.Now().UTC(),
|
LastModified: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
todoJson, err := json.Marshal(todo)
|
todoJson, err := json.Marshal(todo)
|
||||||
@ -103,6 +108,9 @@ func (todo Todo) PrintIndexed(index int) {
|
|||||||
uuidStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
uuidStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||||
lastMod := todo.LastModified.Format("2006-01-02 15:04")
|
lastMod := todo.LastModified.Format("2006-01-02 15:04")
|
||||||
metaLine := fmt.Sprintf(" Last Modified: %s • ID: %s", lastMod, todo.Id)
|
metaLine := fmt.Sprintf(" Last Modified: %s • ID: %s", lastMod, todo.Id)
|
||||||
|
if len(todo.SharedWith) > 0 {
|
||||||
|
metaLine = fmt.Sprintf("%s • Shared with: %s", metaLine, strings.Join(todo.SharedWith, ", "))
|
||||||
|
}
|
||||||
fmt.Println(uuidStyle.Render(metaLine))
|
fmt.Println(uuidStyle.Render(metaLine))
|
||||||
|
|
||||||
fmt.Printf(" %s----------------------------%s\n", ColorGray, ColorReset)
|
fmt.Printf(" %s----------------------------%s\n", ColorGray, ColorReset)
|
||||||
@ -114,6 +122,7 @@ func (t Todo) IsEqual(other Todo) bool {
|
|||||||
t.Description == other.Description &&
|
t.Description == other.Description &&
|
||||||
t.Status == other.Status &&
|
t.Status == other.Status &&
|
||||||
t.Owner == other.Owner &&
|
t.Owner == other.Owner &&
|
||||||
|
slices.Equal(t.SharedWith, other.SharedWith) &&
|
||||||
t.Deleted == other.Deleted
|
t.Deleted == other.Deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,5 +131,6 @@ func (t Todo) IsEqualIgnoringStatus(other Todo) bool {
|
|||||||
t.Name == other.Name &&
|
t.Name == other.Name &&
|
||||||
t.Description == other.Description &&
|
t.Description == other.Description &&
|
||||||
t.Owner == other.Owner &&
|
t.Owner == other.Owner &&
|
||||||
|
slices.Equal(t.SharedWith, other.SharedWith) &&
|
||||||
t.Deleted == other.Deleted
|
t.Deleted == other.Deleted
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type Todo struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
SharedWith []string `json:"shared_with,omitzero"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
LastModified time.Time `json:"last_modified"`
|
LastModified time.Time `json:"last_modified"`
|
||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
@ -22,6 +23,7 @@ type StoreTodoRequest struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
SharedWith []string `json:"shared_with,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoList struct {
|
type TodoList struct {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/server
|
|||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9
|
require gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 h1:EGFIRDjHIEt0IEFYeN2NEn/NyVglN6vXB6IRjm1rN0I=
|
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d h1:YpsaXckG7ryvEe7cTwRrGNOB43wLnMlK0vXqYMHdrXQ=
|
||||||
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9/go.mod h1:bHquapurFm/eUTtrl9mGLEdAYc5cOeueHFvqjommp44=
|
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
|
|||||||
134
tests/sharing_test.go
Normal file
134
tests/sharing_test.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||||
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/server/handler"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSharedTodoSync(t *testing.T) {
|
||||||
|
// 1. Setup Environment
|
||||||
|
tempDir, err := os.MkdirTemp("", "todo-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
os.Setenv("JWT_SECRET", "test-secret-123")
|
||||||
|
|
||||||
|
// 2. Initialize Server
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("POST /register", handler.RegisterHandler)
|
||||||
|
mux.HandleFunc("POST /login", handler.LoginHandler)
|
||||||
|
mux.HandleFunc("POST /sync", handler.SyncHandler)
|
||||||
|
mux.HandleFunc("POST /store", handler.StoreHandler)
|
||||||
|
ts := httptest.NewServer(mux)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client := ts.Client()
|
||||||
|
|
||||||
|
// 3. Register two users
|
||||||
|
users := []struct {
|
||||||
|
username, password string
|
||||||
|
token string
|
||||||
|
}{
|
||||||
|
{"owner", "pass1", ""},
|
||||||
|
{"shared", "pass2", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, u := range users {
|
||||||
|
regPayload, _ := json.Marshal(common.Credentials{Username: u.username, Password: u.password})
|
||||||
|
resp, err := client.Post(ts.URL+"/register", "application/json", bytes.NewBuffer(regPayload))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
resp, err = client.Post(ts.URL+"/login", "application/json", bytes.NewBuffer(regPayload))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
users[i].token = resp.Header.Get(common.AuthHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Owner creates a shared todo
|
||||||
|
sharedTodo := common.Todo{
|
||||||
|
Id: uuid.New().String(),
|
||||||
|
Name: "Shared Task",
|
||||||
|
Description: "This is shared",
|
||||||
|
Status: common.NotStarted,
|
||||||
|
Owner: "owner",
|
||||||
|
SharedWith: []string{"shared"},
|
||||||
|
LastModified: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
syncReq := common.TodoList{Todos: []common.Todo{sharedTodo}}
|
||||||
|
syncPayload, _ := json.Marshal(syncReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(syncPayload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set(common.AuthHeader, "Bearer "+users[0].token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// 5. Shared user pulls todos
|
||||||
|
emptySyncReq := common.TodoList{Todos: []common.Todo{}}
|
||||||
|
emptySyncPayload, _ := json.Marshal(emptySyncReq)
|
||||||
|
req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set(common.AuthHeader, "Bearer "+users[1].token)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var syncResp common.SyncResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&syncResp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Shared user should see the todo
|
||||||
|
assert.Len(t, syncResp.SyncedTodos, 1)
|
||||||
|
assert.Equal(t, "Shared Task", syncResp.SyncedTodos[0].Name)
|
||||||
|
assert.Equal(t, "owner", syncResp.SyncedTodos[0].Owner)
|
||||||
|
assert.Contains(t, syncResp.SyncedTodos[0].SharedWith, "shared")
|
||||||
|
|
||||||
|
// 6. Shared user updates the todo status
|
||||||
|
updatedTodo := syncResp.SyncedTodos[0]
|
||||||
|
updatedTodo.Status = common.Done
|
||||||
|
updatedTodo.LastModified = time.Now().UTC()
|
||||||
|
|
||||||
|
updateSyncReq := common.TodoList{Todos: []common.Todo{updatedTodo}}
|
||||||
|
updateSyncPayload, _ := json.Marshal(updateSyncReq)
|
||||||
|
req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(updateSyncPayload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set(common.AuthHeader, "Bearer "+users[1].token)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// 7. Owner pulls todos and checks status
|
||||||
|
req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set(common.AuthHeader, "Bearer "+users[0].token)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&syncResp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Len(t, syncResp.SyncedTodos, 1)
|
||||||
|
assert.Equal(t, common.Done, syncResp.SyncedTodos[0].Status, "Owner should see status update from shared user")
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user