Compare commits

..

No commits in common. "fca88ccf828b07f3f243d18406b9adafaa282be0" and "7c3cfe4aa0d28cbf38f142f2750d28febbcaaaa8" have entirely different histories.

13 changed files with 460 additions and 431 deletions

View File

@ -53,4 +53,4 @@ WORKDIR /app/
USER appuser:appuser
EXPOSE 4242/tcp
ENTRYPOINT ["/app/binary"]
CMD ["/app/binary"]

165
api.json
View File

@ -1,144 +1,55 @@
[
{
"method": "GET",
"path": "/users",
{
"GET": {
"info": "redirects to given tiny url",
"scope": [[]],
"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" }
"URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "string(1,30)" }
},
"out": {}
},
{
"method": "GET",
"path": "/user/{id}/articles",
"scope": [[]],
"info": "returns the list of existing articles a user wrote",
"POST": {
"info": "creates a new tiny url",
"scope": [["admin"]],
"in": {
"{id}": { "info": "author user id", "name": "author_id", "type": "uint" }
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" },
"target": { "info": "url to shorten", "type": "string(5,300)" }
},
"out": {
"articles": { "info": "articles list", "type": "any" }
}
"out": {}
},
{
"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",
"PUT": {
"info": "overrides an existing tiny url",
"scope": [["admin"]],
"in": {
"{id}": { "info": "the target article id", "name": "article_id", "type": "uint" }
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" },
"target": { "info": "url to shorten", "type": "string(5,300)" }
},
"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",
"out": {}
},
"DELETE": {
"info": "removes an existing tiny url",
"scope": [["admin"]],
"in": {
"title": { "info": "the article title", "type": "string(5,255)" },
"body": { "info": "the article body", "type": "string" }
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }
},
"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" }
"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)" }
}
}
}
}, {
"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": { }
}
]
}

7
go.mod
View File

@ -2,9 +2,4 @@ module git.xdrm.io/go/tiny-url-ex
go 1.14
require (
git.xdrm.io/go/aicra v0.3.0
github.com/jinzhu/gorm v1.9.12
)
replace git.xdrm.io/go/aicra => ../aicra
require git.xdrm.io/go/aicra v0.2.0

24
go.sum
View File

@ -1,22 +1,2 @@
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=
git.xdrm.io/go/aicra v0.2.0 h1:CHXkNA13xH4cnZD/on0i+lcwaAEVnI2QFeFfK9mVpcM=
git.xdrm.io/go/aicra v0.2.0/go.mod h1:ulAzCdKqUN5X4eWQSER70QXSYteSXtybAqupcUuYPdw=

59
main.go
View File

@ -4,15 +4,11 @@ import (
"log"
"net/http"
"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/tiny-url-ex/service/auth"
"git.xdrm.io/go/tiny-url-ex/service/shortener"
"git.xdrm.io/go/aicra"
"git.xdrm.io/go/aicra/typecheck/builtin"
)
func main() {
@ -20,46 +16,33 @@ func main() {
listenTo := ":4242"
// 1. build server
log.Printf("[server] create server")
server, err := aicra.New("api.json", buildDataTypes()...)
log.Printf("[server] building")
server, err := aicra.New("api.json")
if err != nil {
log.Fatalf("/!\\ cannot init server: %v\n", err)
}
// 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()
// 2. add type checkers
server.Checkers.Add(builtin.NewAny())
server.Checkers.Add(builtin.NewString())
server.Checkers.Add(builtin.NewFloat64())
// 3. init services
userService := &user.Service{DB: db}
articleService := &article.Service{DB: db}
// 4. wire services
userService.Wire(server)
articleService.Wire(server)
// 5. create http server
httpServer, err := server.ToHTTPServer()
authService, err := auth.New()
if err != nil {
log.Fatalf("cannot get http server: %s", err)
log.Fatalf("cannot create auth service: %v", err)
}
shortenerService, err := shortener.New(authService)
if err != nil {
log.Fatalf("cannot create auth service: %v", err)
}
// 4. wire services
shortenerService.Wire(server)
authService.Wire(server)
// 5. listen and serve
log.Printf("[server] listening to '%s'\n\n", listenTo)
log.Fatal(http.ListenAndServe(listenTo, httpServer))
log.Printf("[server] listening %s\n\n", listenTo)
log.Fatal(http.ListenAndServe(listenTo, server.HTTP()))
}
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
}

View File

@ -1,85 +0,0 @@
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())
}

104
service/auth/auth.go Normal file
View File

@ -0,0 +1,104 @@
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
}
}

71
service/auth/model.go Normal file
View File

@ -0,0 +1,71 @@
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")
}

View File

@ -1,9 +0,0 @@
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"`
}

View File

@ -1,10 +0,0 @@
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"`
}

View File

@ -0,0 +1,71 @@
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")
}

View File

@ -0,0 +1,151 @@
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())
}

View File

@ -1,133 +0,0 @@
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())
}