diff --git a/api.json b/api.json index 6f55a25..d02aa2d 100644 --- a/api.json +++ b/api.json @@ -1,55 +1,144 @@ -{ - "GET": { - "info": "redirects to given tiny url", +[ + + { + "method": "GET", + "path": "/users", "scope": [[]], - "in": { - "URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "string(1,30)" } - }, - "out": {} - }, - - "POST": { - "info": "creates a new tiny url", - "scope": [["admin"]], - "in": { - "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }, - "target": { "info": "url to shorten", "type": "string(5,300)" } - }, - "out": {} - }, - - "PUT": { - "info": "overrides an existing tiny url", - "scope": [["admin"]], - "in": { - "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }, - "target": { "info": "url to shorten", "type": "string(5,300)" } - }, - "out": {} - }, - - "DELETE": { - "info": "removes an existing tiny url", - "scope": [["admin"]], - "in": { - "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" } - }, - "out": {} - }, - - "/": { - "token": { - "POST": { - "info": "creates a 5-minute access token", - "scope": [[]], - "in": { - "URL#0": { "info": "wanted role", "type": "string(3,10)", "name": "role" } - }, - "out": { - "token": { "info": "access token", "type": "string(128,128)" } - } - } + "info": "returns the list of existing users", + "in": {}, + "out": { + "users": { "info": "users list", "type": "any" } } + }, + { + "method": "GET", + "path": "/user/{id}", + "scope": [], + "info": "returns info about an existing user", + "in": { + "{id}": { "info": "the target user id", "name": "user_id", "type": "uint" } + }, + "out": { + "id": { "info": "user id", "type": "uint" }, + "username": { "info": "username", "type": "string(3,30)" }, + "firstname": { "info": "first name", "type": "string(1,30)" }, + "lastname": { "info": "last name", "type": "string(1,30)" }, + "articles": { "info": "user articles", "type": "any" } + } + }, + + { + "method": "POST", + "path": "/user", + "scope": [["admin"]], + "info": "creates a new user", + "in": { + "username": { "info": "username", "type": "string(3,30)" }, + "firstname": { "info": "first name", "type": "string(1,30)" }, + "lastname": { "info": "last name", "type": "string(1,30)" } + }, + "out": { + "id": { "info": "new user's id", "type": "uint" }, + "username": { "info": "new user's username", "type": "string(3,30)" }, + "firstname": { "info": "new user's first name", "type": "string(1,30)" }, + "lastname": { "info": "new user's last name", "type": "string(1,30)" } + } + }, + { + "method": "PUT", + "path": "/user/{id}", + "scope": [["admin"], ["self"]], + "info": "updates an existing user", + "in": { + "{id}": { "info": "the target user id", "type": "uint", "name": "user_id" }, + "username": { "info": "updated username", "type": "?string(3,30)" }, + "firstname": { "info": "updated first name", "type": "?string(1,30)" }, + "lastname": { "info": "updated last name", "type": "?string(1,30)" } + }, + "out": { + "id": { "info": "new user's id", "type": "uint" }, + "username": { "info": "new user's username", "type": "string(3,30)" }, + "firstname": { "info": "new user's first name", "type": "string(1,30)" }, + "lastname": { "info": "new user's last name", "type": "string(1,30)" } + } + }, + + { + "method": "DELETE", + "path": "/user/{id}", + "scope": [["admin"], ["self"]], + "info": "deletes an existing user", + "in": { + "{id}": { "info": "the target user id", "name": "user_id", "type": "uint" } + }, + "out": {} + }, + + + + + { + "method": "GET", + "path": "/user/{id}/articles", + "scope": [[]], + "info": "returns the list of existing articles a user wrote", + "in": { + "{id}": { "info": "author user id", "name": "author_id", "type": "uint" } + }, + "out": { + "articles": { "info": "articles list", "type": "any" } + } + }, + + { + "method": "GET", + "path": "/articles", + "scope": [[]], + "info": "returns the list of existing articles", + "in": {}, + "out": { + "articles": { "info": "articles list", "type": "any" } + } + }, { + "method": "GET", + "path": "/article/{id}", + "scope": [[]], + "info": "returns an existing article", + "in": { + "{id}": { "info": "the target article id", "name": "article_id", "type": "uint" } + }, + "out": { + "id": { "info": "the article id", "type": "uint" }, + "title": { "info": "the article title", "type": "string(5,255)" }, + "body": { "info": "the article body", "type": "string" }, + "author": { "info": "the author user id", "type": "uint" }, + "score": { "info": "absolute vote score", "type": "uint" } + } + }, { + "method": "POST", + "path": "/article", + "scope": [["author"]], + "info": "post a new article", + "in": { + "title": { "info": "the article title", "type": "string(5,255)" }, + "body": { "info": "the article body", "type": "string" } + }, + "out": { + "id": { "info": "the article id", "type": "uint" }, + "title": { "info": "the article title", "type": "string(5,255)" }, + "body": { "info": "the article body", "type": "string" }, + "author": { "info": "the author user id", "type": "uint" }, + "score": { "info": "absolute vote score", "type": "uint" } + } + }, { + "method": "DELETE", + "path": "/article/{id}", + "scope": [["admin"], ["author"]], + "info": "deletes an article", + "in": { + "{id}": { "info": "the target article id", "name": "article_id", "type": "uint" } + }, + "out": { } } -} \ No newline at end of file +] \ No newline at end of file diff --git a/go.mod b/go.mod index 04ce100..b2d1178 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module git.xdrm.io/go/tiny-url-ex go 1.14 -require git.xdrm.io/go/aicra v0.2.0 +require ( + git.xdrm.io/go/aicra v0.3.0 + github.com/jinzhu/gorm v1.9.12 +) + +replace git.xdrm.io/go/aicra => ../aicra diff --git a/go.sum b/go.sum index b25c5b9..170bcad 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,22 @@ -git.xdrm.io/go/aicra v0.2.0 h1:CHXkNA13xH4cnZD/on0i+lcwaAEVnI2QFeFfK9mVpcM= -git.xdrm.io/go/aicra v0.2.0/go.mod h1:ulAzCdKqUN5X4eWQSER70QXSYteSXtybAqupcUuYPdw= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +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.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/main.go b/main.go index 57fed11..0a5ee67 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,15 @@ import ( "log" "net/http" - "git.xdrm.io/go/tiny-url-ex/service/auth" - "git.xdrm.io/go/tiny-url-ex/service/shortener" + "git.xdrm.io/go/aicra/datatype" + "git.xdrm.io/go/aicra/datatype/builtin" + "git.xdrm.io/go/tiny-url-ex/service/article" + "git.xdrm.io/go/tiny-url-ex/service/user" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/sqlite" "git.xdrm.io/go/aicra" - "git.xdrm.io/go/aicra/typecheck/builtin" ) func main() { @@ -16,33 +20,46 @@ func main() { listenTo := ":4242" // 1. build server - log.Printf("[server] building") - server, err := aicra.New("api.json") + log.Printf("[server] create server") + server, err := aicra.New("api.json", buildDataTypes()...) if err != nil { log.Fatalf("/!\\ cannot init server: %v\n", err) } - // 2. add type checkers - server.Checkers.Add(builtin.NewAny()) - server.Checkers.Add(builtin.NewString()) - server.Checkers.Add(builtin.NewFloat64()) + // 2. connect to storage + db, err := gorm.Open("sqlite3", "test.db") + if err != nil { + log.Fatalf("/!\\ cannot open database: %v\n", err) + } + defer db.Close() // 3. init services - authService, err := auth.New() - if err != nil { - log.Fatalf("cannot create auth service: %v", err) - } - shortenerService, err := shortener.New(authService) - if err != nil { - log.Fatalf("cannot create auth service: %v", err) - } + userService := &user.Service{DB: db} + articleService := &article.Service{DB: db} // 4. wire services - shortenerService.Wire(server) - authService.Wire(server) + userService.Wire(server) + articleService.Wire(server) + + // 5. create http server + httpServer, err := server.ToHTTPServer() + if err != nil { + log.Fatalf("cannot get http server: %s", err) + } // 5. listen and serve - log.Printf("[server] listening %s\n\n", listenTo) - log.Fatal(http.ListenAndServe(listenTo, server.HTTP())) + log.Printf("[server] listening to '%s'\n\n", listenTo) + log.Fatal(http.ListenAndServe(listenTo, httpServer)) } + +func buildDataTypes() []datatype.T { + dtypes := make([]datatype.T, 0) + + dtypes = append(dtypes, builtin.AnyDataType{}) + dtypes = append(dtypes, builtin.BoolDataType{}) + dtypes = append(dtypes, builtin.UintDataType{}) + dtypes = append(dtypes, builtin.StringDataType{}) + + return dtypes +} diff --git a/service/article/article.go b/service/article/article.go new file mode 100644 index 0000000..fe664c5 --- /dev/null +++ b/service/article/article.go @@ -0,0 +1,85 @@ +package article + +import ( + "net/http" + + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/tiny-url-ex/service/model" + "github.com/jinzhu/gorm" +) + +// Service to manage users +type Service struct { + DB *gorm.DB +} + +// Wire services to their paths +func (s Service) Wire(server *aicra.Server) { + if !s.DB.HasTable(&model.Article{}) { + s.DB.CreateTable(&model.Article{}) + } + + server.HandleFunc(http.MethodGet, "/articles", s.getAllArticles) + server.HandleFunc(http.MethodGet, "/user/{id}/articles", s.getArticlesByAuthor) + server.HandleFunc(http.MethodGet, "/article/{id}", s.getArticleByID) + server.HandleFunc(http.MethodPost, "/article", s.postArticle) + server.HandleFunc(http.MethodDelete, "/article/{id}", s.deleteArticle) +} + +func (s Service) getArticlesByAuthor(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("author_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "author_id") + return + } + articles := make([]model.Article, 0) + s.DB.Where("author = ?", id).Find(&articles) + res.SetData("articles", articles) + res.SetError(api.ErrorSuccess()) +} + +func (s Service) getAllArticles(req api.Request, res *api.Response) { + articles := make([]model.Article, 0) + s.DB.Find(&articles) + res.SetData("articles", articles) + res.SetError(api.ErrorSuccess()) +} + +func (s Service) getArticleByID(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("article_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "article_id") + return + } + + var article model.Article + if s.DB.First(&article, id).RecordNotFound() { + res.SetError(api.ErrorNoMatchFound()) + return + } + + res.SetData("id", article.ID) + res.SetData("title", article.Title) + res.SetData("body", article.Body) + res.SetData("author", article.Author) + res.SetError(api.ErrorSuccess()) +} + +func (s Service) postArticle(req api.Request, res *api.Response) { + +} + +func (s Service) deleteArticle(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("user_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "user_id") + return + } + + article := model.Article{} + article.ID = id + s.DB.Delete(&article) + + res.SetError(api.ErrorSuccess()) +} diff --git a/service/auth/auth.go b/service/auth/auth.go deleted file mode 100644 index ac3b1b5..0000000 --- a/service/auth/auth.go +++ /dev/null @@ -1,104 +0,0 @@ -package auth - -import ( - "crypto/sha512" - "encoding/hex" - "log" - "net/http" - "strconv" - "strings" - "time" - - "git.xdrm.io/go/aicra" - "git.xdrm.io/go/aicra/api" -) - -// Service manages the url shortener -type Service struct{} - -// New returns a bare service -func New() (*Service, error) { - log.Printf("[auth] creating service") - - log.Printf("[auth] service created") - return &Service{}, nil -} - -// Wire to the aicra server -func (svc *Service) Wire(server *aicra.Server) { - log.Printf("[auth] service wired") - server.HandleFunc(http.MethodPost, "/token", svc.generateToken) -} - -// generateToken generates a token valid for 5 mintes -func (svc *Service) generateToken(req api.Request, res *api.Response) { - - // 1. extract input - role, err := req.Param.GetString("role") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "role", err.Error()) - return - } - - // 2. generate token - hasher := sha512.New() - defer hasher.Reset() - hasher.Write([]byte(strconv.FormatInt(time.Now().Unix(), 5))) - token := hex.EncodeToString(hasher.Sum(nil)) - - // 3. store token - model := NewModel(token, role, 5*time.Minute) - if model.Create() != nil { - res.SetError(api.ErrorFailure()) - return - } - - // 4. return data - log.Printf("[auth] new token") - res.Data["token"] = token - res.SetError(api.ErrorSuccess()) - -} - -// CheckToken returns whether a token is valid -func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc { - return func(req api.Request, res *api.Response) { - - // success if no scope [['admin']] - if req.Scope == nil || len(req.Scope) < 1 { - handler(req, res) - return - } - - headerToken := req.Request.Header.Get("Authorization") - - // fail if invalid header - if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") { - res.SetError(api.ErrorToken()) - return - } - - model := NewModel(headerToken, "", 0) - if model.Search() != nil { - res.SetError(api.ErrorToken()) - return - } - - // fail if the role of the token does not match any scope - for _, scope := range req.Scope { - if scope == nil || len(scope) != 1 { - continue - } - - // success - if scope[0] == model.role { - handler(req, res) - return - } - } - - // failure - res.SetError(api.ErrorPermission()) - return - } -} diff --git a/service/auth/model.go b/service/auth/model.go deleted file mode 100644 index 3a093b9..0000000 --- a/service/auth/model.go +++ /dev/null @@ -1,71 +0,0 @@ -package auth - -import ( - "errors" - "time" -) - -// Model represents an actual tiny url entry in the database. -type Model struct { - token string - role string - expires int64 -} - -// storage contains entries -var storage []*Model = make([]*Model, 0) - -// NewModel builds a token model from arguments -func NewModel(token, role string, duration time.Duration) *Model { - return &Model{ - token: token, - role: role, - expires: time.Now().Add(duration).Unix(), - } -} - -// Search for model in storage -func (mod *Model) Search() error { - for _, row := range storage { - - // silently delete if expired - if row.expires < time.Now().Unix() { - row.Delete() - continue - } - - if row.token == mod.token { - mod.token = row.token - mod.role = row.role - mod.expires = row.expires - return nil - } - } - - return errors.New("not found") -} - -// Create the model in storage -func (mod *Model) Create() error { - // fail if already exists - for _, row := range storage { - if row.token == mod.token { - return errors.New("already exists") - } - } - - storage = append(storage, mod) - return nil -} - -// Delete the model from storage -func (mod *Model) Delete() error { - for i, row := range storage { - if row.token == mod.token { - storage = append(storage[:i], storage[i+1:]...) - return nil - } - } - - return errors.New("not found") -} diff --git a/service/model/article.go b/service/model/article.go new file mode 100644 index 0000000..c16d840 --- /dev/null +++ b/service/model/article.go @@ -0,0 +1,9 @@ +package model + +// Article representation of an article in storage +type Article struct { + ID uint `json:"article_id" gorm:"primary_key"` + Author uint `json:"author"` + Title string `json:"title"` + Body string `json:"body"` +} diff --git a/service/model/user.go b/service/model/user.go new file mode 100644 index 0000000..b17574b --- /dev/null +++ b/service/model/user.go @@ -0,0 +1,10 @@ +package model + +// User representation of a user in storage +type User struct { + ID uint `json:"user_id" gorm:"primary_key"` + Username string `json:"username"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Articles []Article `gorm:"foreignKey:Author"` +} diff --git a/service/shortener/model.go b/service/shortener/model.go deleted file mode 100644 index 6d76cf0..0000000 --- a/service/shortener/model.go +++ /dev/null @@ -1,71 +0,0 @@ -package shortener - -import ( - "errors" -) - -// Model represents an actual tiny url entry in the database. -type Model struct { - tiny string - long string -} - -// storage contains entries -var storage []*Model = make([]*Model, 0) - -// NewModel builds a tiny model from arguments -func NewModel(tiny string, long string) *Model { - return &Model{ - tiny: tiny, - long: long, - } -} - -// Search for model in storage -func (mod *Model) Search() error { - for _, row := range storage { - if row.tiny == mod.tiny { - mod.tiny = row.tiny - mod.long = row.long - return nil - } - } - - return errors.New("not found") -} - -// Create the model in storage -func (mod *Model) Create() error { - // fail if already exists - if mod.Search() == nil { - return errors.New("already exists") - } - - storage = append(storage, mod) - return nil -} - -// Update the model in storage -func (mod *Model) Update() error { - for _, row := range storage { - if row.tiny == mod.tiny { - row.tiny = mod.tiny - row.long = mod.long - return nil - } - } - - return errors.New("not found") -} - -// Delete the model from storage -func (mod *Model) Delete() error { - for i, row := range storage { - if row.tiny == mod.tiny && row.long == mod.long { - storage = append(storage[:i], storage[i+1:]...) - return nil - } - } - - return errors.New("not found") -} diff --git a/service/shortener/shortener.go b/service/shortener/shortener.go deleted file mode 100644 index 24d1e0a..0000000 --- a/service/shortener/shortener.go +++ /dev/null @@ -1,151 +0,0 @@ -package shortener - -import ( - "log" - "net/http" - - "git.xdrm.io/go/aicra" - "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/tiny-url-ex/service/auth" -) - -// Service manages the url shortener -type Service struct { - authService *auth.Service -} - -// New returns a bare service -func New(auth *auth.Service) (*Service, error) { - log.Printf("[shortener] creating service") - - log.Printf("[shortener] service created") - return &Service{ - authService: auth, - }, nil -} - -// Wire to the aicra server -func (svc *Service) Wire(server *aicra.Server) { - log.Printf("[shortener] service wired") - server.HandleFunc(http.MethodGet, "/", svc.redirect) - server.HandleFunc(http.MethodPost, "/", svc.authService.CheckToken(svc.register)) - server.HandleFunc(http.MethodPut, "/", svc.authService.CheckToken(svc.update)) - server.HandleFunc(http.MethodDelete, "/", svc.authService.CheckToken(svc.delete)) -} - -// redirect from a tiny url to the long url -func (svc *Service) redirect(req api.Request, res *api.Response) { - - // 1. extract input - tinyURL, err := req.Param.GetString("url") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "url", err.Error()) - return - } - - // 2. check in db if exists - model := NewModel(tinyURL, "") - if model.Search() != nil { - res.SetError(api.ErrorNoMatchFound()) - return - } - - // 3. redirect - res.Status = http.StatusPermanentRedirect - res.Headers.Set("Location", model.long) - res.SetError(api.ErrorSuccess()) - -} - -// register registers a new tiny url to a long one -func (svc *Service) register(req api.Request, res *api.Response) { - - // 1. extract arguments - longURL, err := req.Param.GetString("target") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "target", err.Error()) - return - } - tinyURL, err := req.Param.GetString("url") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "url", err.Error()) - return - } - - // 2. fail if already used - model := NewModel(tinyURL, "") - if model.Search() == nil { - res.SetError(api.ErrorAlreadyExists(), "url") - return - } - - // 3. store association - model.long = longURL - if model.Create() != nil { - res.SetError(api.ErrorFailure()) - return - } - log.Printf("[shortener] new %q -> %q", model.tiny, model.long) - - res.SetError(api.ErrorSuccess()) -} - -// update updates an existing tiny url to a new long one -func (svc *Service) update(req api.Request, res *api.Response) { - - // 1. extract arguments - longURL, err := req.Param.GetString("target") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "target", err.Error()) - return - } - tinyURL, err := req.Param.GetString("url") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "url", err.Error()) - return - } - - // 2. fail if not already existing - model := NewModel(tinyURL, "") - if model.Search() != nil { - res.SetError(api.ErrorNoMatchFound()) - return - } - - // 3. update association - model.long = longURL - if model.Update() != nil { - res.SetError(api.ErrorFailure()) - return - } - log.Printf("[shortener] upd %q -> %q", model.tiny, model.long) - - res.SetError(api.ErrorSuccess()) -} - -// delete removes a new tiny url -func (svc *Service) delete(req api.Request, res *api.Response) { - - // 1. extract arguments - tinyURL, err := req.Param.GetString("url") - if err != nil { - res.SetError(api.ErrorInvalidParam(), "url", err.Error()) - return - } - - // 2. fail if not already existing - model := NewModel(tinyURL, "") - if model.Search() != nil { - res.SetError(api.ErrorNoMatchFound()) - return - } - - // 3. delete association - if model.Delete() != nil { - res.SetError(api.ErrorFailure()) - return - } - log.Printf("[shortener] del %q", model.tiny) - - res.SetError(api.ErrorSuccess()) -} diff --git a/service/user/user.go b/service/user/user.go new file mode 100644 index 0000000..6b5563d --- /dev/null +++ b/service/user/user.go @@ -0,0 +1,133 @@ +package user + +import ( + "net/http" + + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/tiny-url-ex/service/model" + "github.com/jinzhu/gorm" +) + +// Service to manage users +type Service struct { + DB *gorm.DB +} + +// Wire services to their paths +func (s Service) Wire(server *aicra.Server) { + if !s.DB.HasTable(&model.User{}) { + s.DB.CreateTable(&model.User{}) + } + + server.HandleFunc(http.MethodGet, "/users", s.getAllUsers) + server.HandleFunc(http.MethodGet, "/user/{id}", s.getUserByID) + server.HandleFunc(http.MethodPost, "/user", s.createUser) + server.HandleFunc(http.MethodPut, "/user/{id}", s.updateUser) + server.HandleFunc(http.MethodDelete, "/user/{id}", s.deleteUser) +} + +func (s Service) getAllUsers(req api.Request, res *api.Response) { + users := make([]model.User, 0) + s.DB.Find(&users) + res.SetData("users", users) + res.SetError(api.ErrorSuccess()) +} +func (s Service) getUserByID(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("user_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "user_id") + return + } + + var user model.User + if s.DB.First(&user, id).RecordNotFound() { + res.SetError(api.ErrorNoMatchFound()) + return + } + + res.SetData("id", user.ID) + res.SetData("username", user.Username) + res.SetData("firstname", user.Firstname) + res.SetData("lastname", user.Lastname) + res.SetData("articles", user.Articles) + res.SetError(api.ErrorSuccess()) +} + +func (s Service) createUser(req api.Request, res *api.Response) { + var user model.User + var err error + user.Username, err = req.Param.GetString("username") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "username") + return + } + user.Firstname, err = req.Param.GetString("firstname") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "firstname") + return + } + user.Lastname, err = req.Param.GetString("lastname") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "lastname") + return + } + + s.DB.Create(&user) + if s.DB.Last(&user).RecordNotFound() { + res.SetError(api.ErrorNoMatchFound()) + return + } + + res.SetData("id", user.ID) + res.SetData("username", user.Username) + res.SetData("firstname", user.Firstname) + res.SetData("lastname", user.Lastname) + res.SetData("articles", user.Articles) + res.SetError(api.ErrorSuccess()) +} + +func (s Service) updateUser(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("user_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "user_id") + return + } + + var user model.User + if s.DB.First(&user, id).RecordNotFound() { + res.SetError(api.ErrorNoMatchFound()) + return + } + + // override with updated values + if updated, err := req.Param.GetString("username"); err == nil { + user.Username = updated + } + if updated, err := req.Param.GetString("firstname"); err == nil { + user.Firstname = updated + } + if updated, err := req.Param.GetString("lastname"); err == nil { + user.Lastname = updated + } + + // update + if s.DB.Save(&user).RowsAffected < 1 { + res.SetError(api.ErrorFailure()) + return + } + res.SetError(api.ErrorSuccess()) +} + +func (s Service) deleteUser(req api.Request, res *api.Response) { + id, err := req.Param.GetUint("user_id") + if err != nil { + res.SetError(api.ErrorInvalidParam(), "user_id") + return + } + + user := model.User{} + user.ID = id + s.DB.Delete(&user) + res.SetError(api.ErrorSuccess()) +}