Compare commits
No commits in common. "fca88ccf828b07f3f243d18406b9adafaa282be0" and "7c3cfe4aa0d28cbf38f142f2750d28febbcaaaa8" have entirely different histories.
fca88ccf82
...
7c3cfe4aa0
|
@ -53,4 +53,4 @@ WORKDIR /app/
|
||||||
USER appuser:appuser
|
USER appuser:appuser
|
||||||
|
|
||||||
EXPOSE 4242/tcp
|
EXPOSE 4242/tcp
|
||||||
ENTRYPOINT ["/app/binary"]
|
CMD ["/app/binary"]
|
163
api.json
163
api.json
|
@ -1,144 +1,55 @@
|
||||||
[
|
{
|
||||||
|
"GET": {
|
||||||
{
|
"info": "redirects to given tiny url",
|
||||||
"method": "GET",
|
|
||||||
"path": "/users",
|
|
||||||
"scope": [[]],
|
"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": {
|
"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": {
|
|
||||||
"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": {}
|
"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": {}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
"/": {
|
||||||
"method": "GET",
|
"token": {
|
||||||
"path": "/user/{id}/articles",
|
"POST": {
|
||||||
|
"info": "creates a 5-minute access token",
|
||||||
"scope": [[]],
|
"scope": [[]],
|
||||||
"info": "returns the list of existing articles a user wrote",
|
|
||||||
"in": {
|
"in": {
|
||||||
"{id}": { "info": "author user id", "name": "author_id", "type": "uint" }
|
"URL#0": { "info": "wanted role", "type": "string(3,10)", "name": "role" }
|
||||||
},
|
},
|
||||||
"out": {
|
"out": {
|
||||||
"articles": { "info": "articles list", "type": "any" }
|
"token": { "info": "access token", "type": "string(128,128)" }
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"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,9 +2,4 @@ module git.xdrm.io/go/tiny-url-ex
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require (
|
require git.xdrm.io/go/aicra v0.2.0
|
||||||
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,22 +1,2 @@
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
git.xdrm.io/go/aicra v0.2.0 h1:CHXkNA13xH4cnZD/on0i+lcwaAEVnI2QFeFfK9mVpcM=
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
git.xdrm.io/go/aicra v0.2.0/go.mod h1:ulAzCdKqUN5X4eWQSER70QXSYteSXtybAqupcUuYPdw=
|
||||||
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,15 +4,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/tiny-url-ex/service/auth"
|
||||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
"git.xdrm.io/go/tiny-url-ex/service/shortener"
|
||||||
"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"
|
||||||
|
"git.xdrm.io/go/aicra/typecheck/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -20,46 +16,33 @@ func main() {
|
||||||
listenTo := ":4242"
|
listenTo := ":4242"
|
||||||
|
|
||||||
// 1. build server
|
// 1. build server
|
||||||
log.Printf("[server] create server")
|
log.Printf("[server] building")
|
||||||
server, err := aicra.New("api.json", buildDataTypes()...)
|
server, err := aicra.New("api.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("/!\\ cannot init server: %v\n", err)
|
log.Fatalf("/!\\ cannot init server: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. connect to storage
|
// 2. add type checkers
|
||||||
db, err := gorm.Open("sqlite3", "test.db")
|
server.Checkers.Add(builtin.NewAny())
|
||||||
if err != nil {
|
server.Checkers.Add(builtin.NewString())
|
||||||
log.Fatalf("/!\\ cannot open database: %v\n", err)
|
server.Checkers.Add(builtin.NewFloat64())
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 3. init services
|
// 3. init services
|
||||||
userService := &user.Service{DB: db}
|
authService, err := auth.New()
|
||||||
articleService := &article.Service{DB: db}
|
|
||||||
|
|
||||||
// 4. wire services
|
|
||||||
userService.Wire(server)
|
|
||||||
articleService.Wire(server)
|
|
||||||
|
|
||||||
// 5. create http server
|
|
||||||
httpServer, err := server.ToHTTPServer()
|
|
||||||
if err != nil {
|
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
|
// 5. listen and serve
|
||||||
log.Printf("[server] listening to '%s'\n\n", listenTo)
|
log.Printf("[server] listening %s\n\n", listenTo)
|
||||||
log.Fatal(http.ListenAndServe(listenTo, httpServer))
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())
|
|
||||||
}
|
|
Loading…
Reference in New Issue