From 98f3c2aedc44d6e00ba481916d4b993c380c134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Forc=C3=A9n=20Mu=C3=B1oz?= Date: Sat, 9 Nov 2024 19:35:44 +0100 Subject: [PATCH] Init project with vote module --- .gitignore | 3 + db.go | 63 ++++++++++ errors/errors.go | 9 ++ extract.go | 26 ++++ go.mod | 38 ++++++ go.sum | 87 ++++++++++++++ models/models.go | 34 ++++++ models/plans.go | 52 ++++++++ models/polls.go | 44 +++++++ models/users.go | 11 ++ planner.go | 304 +++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 671 insertions(+) create mode 100644 .gitignore create mode 100644 db.go create mode 100644 errors/errors.go create mode 100644 extract.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 models/models.go create mode 100644 models/plans.go create mode 100644 models/polls.go create mode 100644 models/users.go create mode 100644 planner.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01b978c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +ui/node_modules +planner +db.sqlite diff --git a/db.go b/db.go new file mode 100644 index 0000000..e3bef16 --- /dev/null +++ b/db.go @@ -0,0 +1,63 @@ +package main + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "log" + "os" + . "planner/models" +) + +func bootstrapDatabase() *gorm.DB { + fi, err := os.Stat("./db.sqlite") + var db *gorm.DB + if err != nil { + if os.IsNotExist(err) { + db, err := gorm.Open(sqlite.Open("./db.sqlite"), &gorm.Config{}) + if err != nil { + log.Fatal(err) + return nil + } + + db.AutoMigrate(&User{}, &Plan{}, &Poll{}, &Vote{}) + + //var tables = [...]struct { + // key string + // query string + //}{ + // {key: "users", query: "CREATE TABLE users(username STRING PRIMARY KEY, password STRING)"}, + // {key: "plans", query: "CREATE TABLE plans(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING, owner STRING, FOREIGN KEY(owner) REFERENCES users(username))"}, + // {key: "plan_user_relations", query: "CREATE TABLE plan_user_relations(username STRING, plan INTEGER, PRIMARY KEY(username, plan), FOREIGN KEY username REFERENCES user(username), FOREIGN KEY plan REFERENCES plans(id))"}, + // {key: "polls", query: "CREATE TABLE polls(id INTEGER PRIMARY KEY AUTOINCREMENT, plan INTEGER, name STRING, options JSON, FOREIGN KEY plan REFERENCES plans(id))"}, + // {key: "votes", query: "CREATE TABLE votes(id INTEGER, poll INTEGER, user STRING, value JSON, FOREIGN KEY poll REFERENCES polls(id), FOREIGN KEY user REFERENCES user(username))"}, + //} + + //for _, table := range tables { + // _, err = db.Exec(table.query) + // if err != nil { + // log.Fatal("Failed to create " + table.key + " table") + // log.Fatal(err) + // return false + // } + //} + return db + } else { + log.Fatal(err) + return nil + } + } + + if !fi.Mode().IsRegular() { + log.Fatal("File is not regular file") + return nil + } + + db, err = gorm.Open(sqlite.Open("./db.sqlite"), &gorm.Config{}) + + if err != nil { + log.Fatal(err) + return nil + } + + return db +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..0832adf --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import "errors" + +var ( + ErrNotMember = errors.New("User is not member of plan") + ErrNotFound = errors.New("Resource is not found") + ErrInvalidOption = errors.New("Option is not a valid one for this poll") +) diff --git a/extract.go b/extract.go new file mode 100644 index 0000000..d8156ac --- /dev/null +++ b/extract.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "net/http" + "planner/models" +) + +func extract_user(orm *gorm.DB, c *gin.Context) *models.User { + username, _, ok := c.Request.BasicAuth() + if !ok { + c.Status(http.StatusUnauthorized) + return nil + } + u := models.User{ + Username: username, + } + + result := orm.Take(&u) + if result.Error != nil { + c.String(http.StatusNotFound, "Unable to find user "+username) + return nil + } + return &u +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..96690db --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module planner + +go 1.23.0 + +require ( + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.23 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.5.6 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..db68b34 --- /dev/null +++ b/go.sum @@ -0,0 +1,87 @@ +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +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/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +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/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..3736efa --- /dev/null +++ b/models/models.go @@ -0,0 +1,34 @@ +package models + +type User struct { + Username string `gorm:"primaryKey" json:"username"` + Password string `json:"password"` + OwnedPlans []Plan `gorm:"foreignKey:Owner;references:Username" json:"-"` + MemberPlans []Plan `gorm:"many2many:user_plans;" json:"-"` + Votes []Vote `gorm:"foreignKey:UsernameID" json:"-"` +} + +// CREATE TABLE plans(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING, owner STRING, FOREIGN KEY(owner) REFERENCES users(username)) +// CREATE TABLE plan_user_relations(username STRING, plan INTEGER, PRIMARY KEY(username, plan), FOREIGN KEY username REFERENCES user(username), FOREIGN KEY plan REFERENCES plans(id)) +type Plan struct { + ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` + Name string `json:"name"` + Owner string `json:"owner"` + Members []User `gorm:"many2many:user_plans;" json:"-"` + Polls []Poll `gorm:"foreignKey:PlanID;references:ID" json:"-"` +} + +// CREATE TABLE polls(id INTEGER PRIMARY KEY AUTOINCREMENT, plan INTEGER, name STRING, options JSON, FOREIGN KEY plan REFERENCES plans(id)) +type Poll struct { + ID uint `gorm:"primaryKey;autoIncrement:true" json:"id"` + PlanID uint `json:"-"` + Options string `json:"options"` + Votes []Vote `gorm:"foreignKey:PollID;references:ID" json:"-"` +} + +// CREATE TABLE votes(id INTEGER, poll INTEGER, user STRING, value JSON, FOREIGN KEY poll REFERENCES polls(id), FOREIGN KEY user REFERENCES user(username)) +type Vote struct { + PollID uint `gorm:"primaryKey" json:"-"` + UsernameID string `gorm:"primaryKey" json:"username_id"` + Value string `json:"value"` +} diff --git a/models/plans.go b/models/plans.go new file mode 100644 index 0000000..656ff6f --- /dev/null +++ b/models/plans.go @@ -0,0 +1,52 @@ +package models + +import ( + "errors" + "gorm.io/gorm" + . "planner/errors" +) + +func GetPlan(orm *gorm.DB, user User, id uint) (*Plan, error) { + var plan Plan = Plan{ + ID: id, + } + result := orm.Take(&plan) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } else if result.Error != nil { + return nil, result.Error + } + + if plan.Owner == user.Username { + return &plan, nil + } + + isMember, err := plan.IsMember(orm, &user) + + if !isMember || err != nil { + return nil, err + } + + return &plan, nil +} + +func (p *Plan) GetAllUsers(orm *gorm.DB) ([]User, error) { + var users []User + err := orm.Model(p).Association("Members").Find(&users) + return users, err +} + +func (p *Plan) IsMember(orm *gorm.DB, u *User) (bool, error) { + var user User + result := orm. + Table("users u"). + Select("u.*"). + Joins("JOIN user_plans up on u.username=up.user_username AND up.plan_id = ?", p.ID). + Where("u.username = ?", u.Username). + Take(&user).Error + if errors.Is(result, gorm.ErrRecordNotFound) { + return false, ErrNotMember + } + return result == nil, result +} diff --git a/models/polls.go b/models/polls.go new file mode 100644 index 0000000..0484c6c --- /dev/null +++ b/models/polls.go @@ -0,0 +1,44 @@ +package models + +import ( + "fmt" + "strings" + + "gorm.io/gorm" + "planner/errors" +) + +func GetPoll(orm *gorm.DB, user User, id uint) (*Poll, error) { + var poll Poll = Poll{ + ID: id, + } + + if result := orm.Take(&poll); result.Error != nil { + return nil, result.Error + } + + fmt.Printf("%+v\n", poll.PlanID) + + return &poll, nil +} + +func (p *Poll) SetVote(orm *gorm.DB, user User, option string) error { + found := false + options := strings.Split(p.Options, ",") + + for _, opt := range options { + if opt == option { + found = true + break + } + } + if !found { + return errors.ErrInvalidOption + } + + if res := orm.Create(Vote{PollID: p.ID, UsernameID: user.Username, Value: option}); res.Error != nil { + return res.Error + } + + return nil +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..bc9a6dc --- /dev/null +++ b/models/users.go @@ -0,0 +1,11 @@ +package models + +import ( + "gorm.io/gorm" +) + +func (u *User) GetPlans(orm *gorm.DB) ([]Plan, error) { + var plans []Plan + err := orm.Model(u).Association("MemberPlans").Find(&plans) + return plans, err +} diff --git a/planner.go b/planner.go new file mode 100644 index 0000000..5cb7a42 --- /dev/null +++ b/planner.go @@ -0,0 +1,304 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + + . "planner/errors" + "planner/models" + . "planner/models" + + "github.com/gin-gonic/gin" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + fmt.Println("Opening database db.sqlite") + + orm := bootstrapDatabase() + + if orm == nil { + log.Fatal("Failed to init database") + return + } + + db, _ := orm.DB() + defer db.Close() + + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + + r.POST("/login", func(c *gin.Context) { + var q struct { + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` + } + if c.ShouldBind(&q) == nil { + if q.Username == "" { + c.String(http.StatusBadRequest, "Login data is null") + } else { + user := User{ + Username: q.Username, + } + orm.Take(&user) + if user.Password == q.Password { + c.JSON(http.StatusOK, map[string]string{"username": user.Username}) + } else { + c.Status(http.StatusForbidden) + } + } + } else { + c.String(http.StatusBadRequest, "Unable to bind data") + } + }) + + r.GET("/users", func(c *gin.Context) { + var q struct { + Name string `form:"name"` + } + if c.ShouldBind(&q) == nil { + user := User{ + Username: q.Name, + } + orm.Take(&user) + fmt.Println(user) + c.JSON(http.StatusOK, user) + } + }) + + r.POST("/users", func(c *gin.Context) { + var u User + if c.ShouldBind(&u) == nil { + orm.Create(&u) + c.Status(http.StatusCreated) + } else { + fmt.Print("Could not bind model") + } + }) + + r.POST("/plans", func(c *gin.Context) { + username, _, ok := c.Request.BasicAuth() + if !ok { + c.Status(http.StatusUnauthorized) + return + } + u := User{ + Username: username, + } + + if result := orm.Take(&u); result.Error != nil { + c.String(http.StatusNotFound, "Unable to find user "+username) + return + } + + var plan_req struct { + Name string `json:"name"` + } + c.Bind(&plan_req) + var plan Plan = Plan{ + Name: plan_req.Name, + Owner: u.Username, + Members: []User{ + {Username: u.Username}, + }, + } + result := orm.Create(&plan) + + if result.Error != nil { + c.JSON(http.StatusInternalServerError, result.Error) + } else { + c.JSON(http.StatusOK, plan_req) + } + }) + + r.GET("/plans", func(c *gin.Context) { + u := extract_user(orm, c) + if u == nil { + return + } + plans, err := u.GetPlans(orm) + if err == nil { + c.JSON(http.StatusOK, plans) + } else { + c.String(http.StatusInternalServerError, err.Error()) + } + }) + + r.GET("/plans/:id", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var params struct { + Id uint `uri:"id"` + } + bind_result := c.BindUri(¶ms) + if bind_result != nil { + return + } + + plan, err := GetPlan(orm, *user, params.Id) + + if err == nil { + c.JSON(http.StatusOK, plan) + } else if errors.Is(err, ErrNotMember) { + c.Status(http.StatusForbidden) + } else if errors.Is(err, ErrNotFound) { + c.Status(http.StatusNotFound) + } else { + c.String(http.StatusInternalServerError, err.Error()) + } + }) + + r.POST("/plans/:id/polls", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var params struct { + Id uint `uri:"id"` + } + bind_result := c.BindUri(¶ms) + if bind_result != nil { + return + } + + plan, err := GetPlan(orm, *user, params.Id) + + if errors.Is(err, ErrNotFound) { + c.Status(http.StatusNotFound) + return + } else if errors.Is(err, ErrNotMember) { + c.Status(http.StatusForbidden) + return + } else if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + var poll_opts struct { + Options string `json:"options"` + } + bind_result = c.Bind(&poll_opts) + if bind_result != nil { + fmt.Println(bind_result) + return + } + + poll := Poll{ + PlanID: plan.ID, + Options: poll_opts.Options, + } + orm.Create(&poll) + + c.JSON(http.StatusCreated, poll) + }) + + r.GET("/plans/:id/polls", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var params struct { + Id uint `uri:"id"` + } + + bind_result := c.BindUri(¶ms) + if bind_result != nil { + c.Status(http.StatusBadRequest) + return + } + + var polls []Poll + orm.Where("plan_id = ?", params.Id).Find(&polls) + c.JSON(http.StatusOK, polls) + }) + + r.GET("/polls/:poll_id", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var params struct { + PollId uint `uri:"poll_id"` + } + + bind_result := c.BindUri(¶ms) + if bind_result != nil { + fmt.Println(bind_result) + return + } + fmt.Println(params) + + poll, _ := models.GetPoll(orm, *user, params.PollId) + c.JSON(http.StatusOK, poll) + }) + + r.GET("/polls/:poll_id/votes", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var params struct { + PollId uint `uri:"poll_id"` + } + + bind_result := c.BindUri(¶ms) + if bind_result != nil { + fmt.Println(bind_result) + return + } + + var votes []Vote + orm.Where("poll_id = ?", params.PollId).Find(&votes) + c.JSON(http.StatusOK, &votes) + }) + + r.POST("/polls/:poll_id/votes", func(c *gin.Context) { + user := extract_user(orm, c) + if user == nil { + return + } + + var path_params struct { + PollId uint `uri:"poll_id"` + } + + bind_result := c.BindUri(&path_params) + if bind_result != nil { + fmt.Println(bind_result) + return + } + + poll, err := models.GetPoll(orm, *user, path_params.PollId) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + var vote_params struct { + Vote string `json:"vote"` + } + c.Bind(&vote_params) + + if err := poll.SetVote(orm, *user, vote_params.Vote); err != nil { + c.String(http.StatusBadRequest, err.Error()) + } + + c.Status(http.StatusOK) + }) + + r.Run() +}