new repo from git.xdrm.io/go/aicra@v0.2.0
This commit is contained in:
parent
fc64e500f0
commit
1fb2a210a1
18
aicra.json
18
aicra.json
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"root": ".",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8080,
|
||||
|
||||
"driver": "plugin",
|
||||
|
||||
"types": {
|
||||
"default": true,
|
||||
"folder": "type"
|
||||
},
|
||||
"controllers": {
|
||||
"folder": "controller.plugin"
|
||||
},
|
||||
"middlewares": {
|
||||
"folder": "middleware.plugin"
|
||||
}
|
||||
}
|
18
api.json
18
api.json
|
@ -3,7 +3,7 @@
|
|||
"info": "redirects to given tiny url",
|
||||
"scope": [[]],
|
||||
"in": {
|
||||
"URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "varchar(1,30)" }
|
||||
"URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "string(1,30)" }
|
||||
},
|
||||
"out": {}
|
||||
},
|
||||
|
@ -12,8 +12,8 @@
|
|||
"info": "creates a new tiny url",
|
||||
"scope": [["admin"]],
|
||||
"in": {
|
||||
"URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" },
|
||||
"target": { "info": "url to shorten", "type": "varchar(5,300)" }
|
||||
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" },
|
||||
"target": { "info": "url to shorten", "type": "string(5,300)" }
|
||||
},
|
||||
"out": {}
|
||||
},
|
||||
|
@ -22,8 +22,8 @@
|
|||
"info": "overrides an existing tiny url",
|
||||
"scope": [["admin"]],
|
||||
"in": {
|
||||
"URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" },
|
||||
"target": { "info": "url to shorten", "type": "varchar(5,300)" }
|
||||
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" },
|
||||
"target": { "info": "url to shorten", "type": "string(5,300)" }
|
||||
},
|
||||
"out": {}
|
||||
},
|
||||
|
@ -32,7 +32,7 @@
|
|||
"info": "removes an existing tiny url",
|
||||
"scope": [["admin"]],
|
||||
"in": {
|
||||
"URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" }
|
||||
"URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }
|
||||
},
|
||||
"out": {}
|
||||
},
|
||||
|
@ -40,13 +40,13 @@
|
|||
"/": {
|
||||
"token": {
|
||||
"POST": {
|
||||
"info": "creates a 1-minute access token",
|
||||
"info": "creates a 5-minute access token",
|
||||
"scope": [[]],
|
||||
"in": {
|
||||
"URL#0": { "info": "wanted role", "type": "varchar(3,10)", "name": "role" }
|
||||
"URL#0": { "info": "wanted role", "type": "string(3,10)", "name": "role" }
|
||||
},
|
||||
"out": {
|
||||
"token": { "info": "access token", "type": "varchar(128,128)" }
|
||||
"token": { "info": "access token", "type": "string(128,128)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.xdrm.io/example/aicra/db"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/aicra/driver"
|
||||
e "git.xdrm.io/go/aicra/err"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
type RootController int
|
||||
|
||||
func Export() driver.Controller { return new(RootController) }
|
||||
|
||||
// Redirects to an url from a key
|
||||
func (rctl RootController) Get(d api.Arguments) api.Response {
|
||||
|
||||
r := api.NewResponse()
|
||||
|
||||
/* (1) Init redis connection */
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (2) Extract api input */
|
||||
key, err := d.GetString("url")
|
||||
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("url")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (3) Check if match for this key */
|
||||
val := cli.Get(db.DATA, key)
|
||||
if val == nil {
|
||||
r.Err = e.NoMatchFound
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (4) Redirect to value */
|
||||
r.Set("_REDIRECT_", string(val))
|
||||
r.Err = e.Success
|
||||
return *r
|
||||
}
|
||||
|
||||
// Stores a new tinyurl/fullurl combination
|
||||
func (rctl RootController) Post(d api.Arguments) api.Response {
|
||||
|
||||
r := api.NewResponse()
|
||||
/* (1) Init redis connection */
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (2) Extract api input */
|
||||
target, err := d.GetString("target")
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("target")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
url, err := d.GetString("url")
|
||||
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("url")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (3) Check if key already used */
|
||||
if cli.Get(db.DATA, url) != nil {
|
||||
r.Err = e.AlreadyExists
|
||||
r.Err.Put("url")
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (4) Store */
|
||||
if !cli.Set(db.DATA, url, target) {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
r.Err = e.Success
|
||||
return *r
|
||||
}
|
||||
|
||||
// Overrides a existing tinyurl with new target
|
||||
func (rctl RootController) Put(d api.Arguments) api.Response {
|
||||
|
||||
r := api.NewResponse()
|
||||
|
||||
/* (1) Init redis connection */
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (2) Extract api input */
|
||||
target, err := d.GetString("target")
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("target")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
url, err := d.GetString("url")
|
||||
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("url")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (3) Check if key already used */
|
||||
if cli.Get(db.DATA, url) == nil {
|
||||
r.Err = e.NoMatchFound
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (4) Update */
|
||||
if !cli.Set(db.DATA, url, target) {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
r.Err = e.Success
|
||||
return *r
|
||||
}
|
||||
|
||||
// Deletes an existing tinyurl
|
||||
func (rctl RootController) Delete(d api.Arguments) api.Response {
|
||||
|
||||
r := api.NewResponse()
|
||||
|
||||
/* (1) Init redis connection */
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (2) Extract api input */
|
||||
url, err := d.GetString("url")
|
||||
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("url")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (3) Check if key already used */
|
||||
if cli.Get(db.DATA, url) == nil {
|
||||
r.Err = e.NoMatchFound
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (4) Delete */
|
||||
if !cli.Del(db.DATA, url) {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
r.Err = e.Success
|
||||
return *r
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"git.xdrm.io/example/aicra/db"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/aicra/driver"
|
||||
e "git.xdrm.io/go/aicra/err"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
type TokenController int
|
||||
|
||||
func Export() driver.Controller { return new(TokenController) }
|
||||
|
||||
// Builds an access token from credentials
|
||||
func (tctl TokenController) Post(d api.Arguments) api.Response {
|
||||
|
||||
r := api.NewResponse()
|
||||
|
||||
/* (1) Init redis connection */
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (2) Extract api input */
|
||||
role, err := d.GetString("role")
|
||||
if err != nil {
|
||||
r.Err = e.InvalidParam
|
||||
r.Err.Put("url")
|
||||
r.Err.Put(err.Error())
|
||||
return *r
|
||||
}
|
||||
|
||||
/* (3) Generate token */
|
||||
hasher := sha512.New()
|
||||
defer hasher.Reset()
|
||||
hasher.Write([]byte(strconv.FormatInt(time.Now().Unix(), 5)))
|
||||
token := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
/* (4) Store */
|
||||
if !cli.Set(db.TOKEN, token, role, time.Minute) {
|
||||
r.Err = e.Failure
|
||||
return *r
|
||||
}
|
||||
|
||||
r.Set("token", token)
|
||||
r.Err = e.Success
|
||||
return *r
|
||||
}
|
||||
|
||||
func (tctl TokenController) Get(d api.Arguments) api.Response { return *api.NewResponse() }
|
||||
func (tctl TokenController) Put(d api.Arguments) api.Response { return *api.NewResponse() }
|
||||
func (tctl TokenController) Delete(d api.Arguments) api.Response { return *api.NewResponse() }
|
81
db/db.go
81
db/db.go
|
@ -1,81 +0,0 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-redis/redis"
|
||||
"time"
|
||||
)
|
||||
|
||||
const NONCE = "go-tiny-url"
|
||||
|
||||
type Domain string
|
||||
|
||||
const (
|
||||
DATA Domain = "data"
|
||||
TOKEN Domain = "token"
|
||||
)
|
||||
|
||||
type db redis.Client
|
||||
|
||||
// Returns a connected client to dataset
|
||||
func Connect() *db {
|
||||
|
||||
cli := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
if _, err := cli.Ping().Result(); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (*db)(cli)
|
||||
}
|
||||
|
||||
// returns value from key (nil if nothing)
|
||||
func (c *db) Get(dom Domain, key string) []byte {
|
||||
|
||||
// 1. Try to get
|
||||
if val, err := (*redis.Client)(c).Get(fmt.Sprintf("%s:%s:%s", NONCE, dom, key)).Result(); err != nil {
|
||||
// 2. nil if nothing found
|
||||
return nil
|
||||
} else {
|
||||
// 3. return if found
|
||||
return []byte(val)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// stores a value for a key (success state in return)
|
||||
func (c *db) Set(dom Domain, key string, value string, exp ...time.Duration) bool {
|
||||
|
||||
var expiration time.Duration = 0
|
||||
if len(exp) > 0 {
|
||||
expiration = exp[0]
|
||||
}
|
||||
|
||||
// 1. Try to set
|
||||
if (*redis.Client)(c).Set(fmt.Sprintf("%s:%s:%s", NONCE, dom, key), value, expiration).Err() != nil {
|
||||
// 2. failure
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. success
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// deletes the value for a key (success state in return)
|
||||
func (c *db) Del(dom Domain, key string) bool {
|
||||
|
||||
// 1. Try to set
|
||||
if (*redis.Client)(c).Del(fmt.Sprintf("%s:%s:%s", NONCE, dom, key)).Err() != nil {
|
||||
// 2. failure
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. success
|
||||
return true
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
module git.xdrm.io/example/aicra
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
git.xdrm.io/go/aicra v0.2.0
|
||||
github.com/go-redis/redis v6.15.2+incompatible
|
||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||
github.com/onsi/gomega v1.5.0 // indirect
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
git.xdrm.io/go/aicra v0.2.0 h1:JrX773PfTaI4jYkeihpdv0U1caLGSsAKpBh21W6W0Ik=
|
||||
git.xdrm.io/go/aicra v0.2.0/go.mod h1:ulAzCdKqUN5X4eWQSER70QXSYteSXtybAqupcUuYPdw=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
|
||||
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
38
main.go
38
main.go
|
@ -1,24 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.xdrm.io/go/aicra"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/example/aicra/service/auth"
|
||||
"git.xdrm.io/example/aicra/service/shortener"
|
||||
|
||||
"git.xdrm.io/example/aicra/storage"
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/typecheck/builtin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
listenTo := "127.0.0.1:4242"
|
||||
|
||||
// 1. build server
|
||||
log.Printf("[server] building")
|
||||
server, err := aicra.New("api.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("/!\\ cannot init server: %v\n", err)
|
||||
}
|
||||
|
||||
log.Printf("[Server up] %s\n", listenTo)
|
||||
// 2. add type checkers
|
||||
server.Checkers.Add(builtin.NewAny())
|
||||
server.Checkers.Add(builtin.NewString())
|
||||
server.Checkers.Add(builtin.NewFloat64())
|
||||
|
||||
// 3. storage connect
|
||||
log.Printf("[storage] connecting")
|
||||
storageClient, err := storage.New()
|
||||
if err != nil {
|
||||
log.Fatalf("/!\\ cannot connect %v", err)
|
||||
}
|
||||
defer storageClient.Close()
|
||||
|
||||
// 4. init services
|
||||
authService := auth.New(storageClient)
|
||||
shortenerService := shortener.New(storageClient, authService)
|
||||
|
||||
// 5. wire services
|
||||
shortenerService.Wire(server)
|
||||
authService.Wire(server)
|
||||
|
||||
// 6. listen and serve
|
||||
log.Printf("[server] listening")
|
||||
err = http.ListenAndServe(listenTo, server)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("/!\\ cannot listen: %v\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.xdrm.io/example/aicra/db"
|
||||
"git.xdrm.io/go/aicra/driver"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
type AuthMiddleware int
|
||||
|
||||
func Export() driver.Middleware { return new(AuthMiddleware) }
|
||||
|
||||
// Authentication middleware
|
||||
func (amw AuthMiddleware) Inspect(req http.Request, scope *[]string) {
|
||||
|
||||
// 1. get authorization header
|
||||
token := req.Header.Get("Authorization")
|
||||
|
||||
// fail if no header
|
||||
if len(token) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. fail on invalid token format
|
||||
if len(token) != 128 || strings.ContainsAny(token, "$-_") {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. get role for this token
|
||||
cli := db.Connect()
|
||||
if cli == nil {
|
||||
return
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
role := cli.Get(db.TOKEN, token)
|
||||
if role == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// add role to scope
|
||||
*scope = append(*scope, string(role))
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.xdrm.io/example/aicra/storage"
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
)
|
||||
|
||||
// Service manages the url shortener
|
||||
type Service struct {
|
||||
storage *storage.Client
|
||||
}
|
||||
|
||||
// New returns a bare service
|
||||
func New(storage *storage.Client) *Service {
|
||||
log.Printf("[service.auth] created")
|
||||
return &Service{
|
||||
storage: storage,
|
||||
}
|
||||
}
|
||||
|
||||
// Wire to the aicra server
|
||||
func (svc *Service) Wire(server *aicra.Server) {
|
||||
log.Printf("[service.auth] wired")
|
||||
server.HandleFunc("POST", "/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.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("role")
|
||||
res.Err.Put(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
|
||||
if !svc.storage.Set(storage.TOKEN, token, role, 5*time.Minute) {
|
||||
res.Err = api.ErrorFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// 4. return data
|
||||
res.Data["token"] = token
|
||||
res.Err = 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.Err = api.ErrorPermission()
|
||||
return
|
||||
}
|
||||
|
||||
tokenRole := svc.storage.Get(storage.TOKEN, headerToken)
|
||||
|
||||
// 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] == string(tokenRole) {
|
||||
handler(req, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// failure
|
||||
res.Err = api.ErrorPermission()
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package shortener
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/example/aicra/service/auth"
|
||||
"git.xdrm.io/example/aicra/storage"
|
||||
"git.xdrm.io/go/aicra"
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
)
|
||||
|
||||
// Service manages the url shortener
|
||||
type Service struct {
|
||||
storage *storage.Client
|
||||
authService *auth.Service
|
||||
}
|
||||
|
||||
// New returns a bare service
|
||||
func New(storage *storage.Client, auth *auth.Service) *Service {
|
||||
log.Printf("[service.shortener] created")
|
||||
return &Service{
|
||||
storage: storage,
|
||||
authService: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// Wire to the aicra server
|
||||
func (svc *Service) Wire(server *aicra.Server) {
|
||||
log.Printf("[service.shortener] wired")
|
||||
server.HandleFunc("GET", "/", svc.redirect)
|
||||
server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register))
|
||||
server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update))
|
||||
server.HandleFunc("DELETE", "/", 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.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("url")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. check in db if exists
|
||||
longURL := svc.storage.Get(storage.DATA, tinyURL)
|
||||
if longURL == nil {
|
||||
res.Err = api.ErrorNoMatchFound()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. redirect
|
||||
res.Status = http.StatusPermanentRedirect
|
||||
res.Headers.Set("Location", string(longURL))
|
||||
res.Err = 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.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("target")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
tinyURL, err := req.Param.GetString("url")
|
||||
if err != nil {
|
||||
res.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("url")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. fail if already used
|
||||
if svc.storage.Get(storage.DATA, tinyURL) != nil {
|
||||
res.Err = api.ErrorAlreadyExists()
|
||||
res.Err.Put("url")
|
||||
return
|
||||
}
|
||||
|
||||
// 3. store association
|
||||
if !svc.storage.Set(storage.DATA, tinyURL, longURL) {
|
||||
res.Err = api.ErrorFailure()
|
||||
return
|
||||
}
|
||||
|
||||
res.Err = 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.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("target")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
tinyURL, err := req.Param.GetString("url")
|
||||
if err != nil {
|
||||
res.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("url")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. fail if not already existing
|
||||
if svc.storage.Get(storage.DATA, tinyURL) == nil {
|
||||
res.Err = api.ErrorNoMatchFound()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. update association
|
||||
if !svc.storage.Set(storage.DATA, tinyURL, longURL) {
|
||||
res.Err = api.ErrorFailure()
|
||||
return
|
||||
}
|
||||
|
||||
res.Err = 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.Err = api.ErrorInvalidParam()
|
||||
res.Err.Put("url")
|
||||
res.Err.Put(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. fail if not already existing
|
||||
if svc.storage.Get(storage.DATA, tinyURL) == nil {
|
||||
res.Err = api.ErrorNoMatchFound()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. update association
|
||||
if !svc.storage.Del(storage.DATA, tinyURL) {
|
||||
res.Err = api.ErrorFailure()
|
||||
return
|
||||
}
|
||||
|
||||
res.Err = api.ErrorSuccess()
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
const nonce = "go-tiny-url"
|
||||
|
||||
const (
|
||||
// DATA domain used to store actual data
|
||||
DATA string = "data"
|
||||
// TOKEN domain used to store tokens
|
||||
TOKEN string = "token"
|
||||
)
|
||||
|
||||
// Client is a wrapper around the redis client
|
||||
type Client struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// New returns new client
|
||||
func New() (*Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
if _, err := client.Ping().Result(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (c *Client) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
// Get returns a value from key or NIL
|
||||
func (c *Client) Get(dom, key string) []byte {
|
||||
|
||||
redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key)
|
||||
|
||||
val, err := c.client.Get(redisKey).Result()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []byte(val)
|
||||
}
|
||||
|
||||
// Set stores a value for a key (success state in return)
|
||||
func (c *Client) Set(dom, key string, value string, exp ...time.Duration) bool {
|
||||
redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key)
|
||||
|
||||
var expiration time.Duration
|
||||
if len(exp) > 0 {
|
||||
expiration = exp[0]
|
||||
}
|
||||
|
||||
return c.client.Set(redisKey, value, expiration).Err() == nil
|
||||
}
|
||||
|
||||
// Del deletes the value for a key (success state in return)
|
||||
func (c *Client) Del(dom, key string) bool {
|
||||
redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key)
|
||||
|
||||
return c.client.Del(redisKey).Err() == nil
|
||||
}
|
Loading…
Reference in New Issue