Compare commits
2 Commits
7c3cfe4aa0
...
fca88ccf82
Author | SHA1 | Date |
---|---|---|
Adrien Marquès | fca88ccf82 | |
Adrien Marquès | fd0a920533 |
|
@ -53,4 +53,4 @@ WORKDIR /app/
|
|||
USER appuser:appuser
|
||||
|
||||
EXPOSE 4242/tcp
|
||||
CMD ["/app/binary"]
|
||||
ENTRYPOINT ["/app/binary"]
|
191
api.json
191
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": { }
|
||||
}
|
||||
|
||||
}
|
||||
]
|
7
go.mod
7
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
|
||||
|
|
24
go.sum
24
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=
|
||||
|
|
59
main.go
59
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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
Loading…
Reference in New Issue