implement postgres repositories
This commit is contained in:
parent
80f372952c
commit
cde7fa4798
12
main.go
12
main.go
|
@ -37,15 +37,21 @@ func main() {
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// 4. init services
|
// 4. init services
|
||||||
authService := auth.New(db)
|
authService, err := auth.New(db)
|
||||||
shortenerService := shortener.New(db, authService)
|
if err != nil {
|
||||||
|
log.Fatalf("cannot create auth service: %v", err)
|
||||||
|
}
|
||||||
|
shortenerService, err := shortener.New(db, authService)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot create auth service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 5. wire services
|
// 5. wire services
|
||||||
shortenerService.Wire(server)
|
shortenerService.Wire(server)
|
||||||
authService.Wire(server)
|
authService.Wire(server)
|
||||||
|
|
||||||
// 6. listen and serve
|
// 6. listen and serve
|
||||||
log.Printf("[server] listening")
|
log.Printf("[server] listening\n\n")
|
||||||
err = http.ListenAndServe(listenTo, server)
|
err = http.ListenAndServe(listenTo, server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("/!\\ cannot listen: %v\n", err)
|
log.Fatalf("/!\\ cannot listen: %v\n", err)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.xdrm.io/example/aicra/storage"
|
|
||||||
"git.xdrm.io/go/aicra"
|
"git.xdrm.io/go/aicra"
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/api"
|
||||||
)
|
)
|
||||||
|
@ -17,19 +16,29 @@ import (
|
||||||
// Service manages the url shortener
|
// Service manages the url shortener
|
||||||
type Service struct {
|
type Service struct {
|
||||||
storage *sql.DB
|
storage *sql.DB
|
||||||
|
repo *repository
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a bare service
|
// New returns a bare service
|
||||||
func New(storage *sql.DB) *Service {
|
func New(storage *sql.DB) (*Service, error) {
|
||||||
log.Printf("[service.auth] created")
|
log.Printf("[auth] creating service")
|
||||||
return &Service{
|
|
||||||
storage: storage,
|
log.Printf("[auth] creating repo")
|
||||||
|
repo, err := newRepo(storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Printf("[auth] repo created")
|
||||||
|
|
||||||
|
log.Printf("[auth] service created")
|
||||||
|
return &Service{
|
||||||
|
repo: repo,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire to the aicra server
|
// Wire to the aicra server
|
||||||
func (svc *Service) Wire(server *aicra.Server) {
|
func (svc *Service) Wire(server *aicra.Server) {
|
||||||
log.Printf("[service.auth] wired")
|
log.Printf("[auth] service wired")
|
||||||
server.HandleFunc("POST", "/token", svc.generateToken)
|
server.HandleFunc("POST", "/token", svc.generateToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,12 +59,14 @@ func (svc *Service) generateToken(req api.Request, res *api.Response) {
|
||||||
token := hex.EncodeToString(hasher.Sum(nil))
|
token := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
|
||||||
// 3. store token
|
// 3. store token
|
||||||
if !svc.storage.Set(storage.TOKEN, token, role, 5*time.Minute) {
|
model := svc.repo.NewModel(token, role, 5*time.Minute)
|
||||||
|
if model.Create() != nil {
|
||||||
res.SetError(api.ErrorFailure())
|
res.SetError(api.ErrorFailure())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. return data
|
// 4. return data
|
||||||
|
log.Printf("[auth] new token")
|
||||||
res.Data["token"] = token
|
res.Data["token"] = token
|
||||||
res.SetError(api.ErrorSuccess())
|
res.SetError(api.ErrorSuccess())
|
||||||
|
|
||||||
|
@ -75,11 +86,15 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc {
|
||||||
|
|
||||||
// fail if invalid header
|
// fail if invalid header
|
||||||
if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") {
|
if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") {
|
||||||
res.SetError(api.ErrorPermission())
|
res.SetError(api.ErrorToken())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenRole := svc.storage.Get(storage.TOKEN, headerToken)
|
model := svc.repo.NewModel(headerToken, "", 0)
|
||||||
|
if model.Search() != nil {
|
||||||
|
res.SetError(api.ErrorToken())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// fail if the role of the token does not match any scope
|
// fail if the role of the token does not match any scope
|
||||||
for _, scope := range req.Scope {
|
for _, scope := range req.Scope {
|
||||||
|
@ -88,7 +103,7 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// success
|
// success
|
||||||
if scope[0] == string(tokenRole) {
|
if scope[0] == model.role {
|
||||||
handler(req, res)
|
handler(req, res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tokenModel represents an actual tiny url entry in the database.
|
||||||
|
type tokenModel struct {
|
||||||
|
db *sql.DB
|
||||||
|
token string
|
||||||
|
role string
|
||||||
|
expires int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// newRepo returns an initialized repository.
|
||||||
|
func newRepo(db *sql.DB) (*repository, error) {
|
||||||
|
_, err := db.Exec(`CREATE TABLE if not exists token(
|
||||||
|
token varchar(128) PRIMARY KEY,
|
||||||
|
role varchar(300) NOT NULL,
|
||||||
|
expires INT NOT NULL
|
||||||
|
);`)
|
||||||
|
|
||||||
|
return &repository{db}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *repository) NewModel(token, role string, duration time.Duration) *tokenModel {
|
||||||
|
return &tokenModel{
|
||||||
|
db: repo.db,
|
||||||
|
token: token,
|
||||||
|
role: role,
|
||||||
|
expires: time.Now().Add(duration).Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tokenModel) Search() error {
|
||||||
|
row := mod.db.QueryRow(`SELECT token, role, expires FROM token WHERE token = $1 LIMIT 1;`, mod.token)
|
||||||
|
|
||||||
|
receiver := &tokenModel{}
|
||||||
|
err := row.Scan(&receiver.token, &receiver.role, &receiver.expires)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete if expired
|
||||||
|
if receiver.expires < time.Now().Unix() {
|
||||||
|
receiver.db = mod.db
|
||||||
|
receiver.Delete()
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.token = receiver.token
|
||||||
|
mod.role = receiver.role
|
||||||
|
mod.expires = receiver.expires
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tokenModel) Create() error {
|
||||||
|
_, err := mod.db.Exec(`INSERT INTO token (token, role, expires) VALUES ($1, $2, $3);`, mod.token, mod.role, mod.expires)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tokenModel) Delete() error {
|
||||||
|
_, err := mod.db.Exec(`DELETE FROM token WHERE token = $1;`, mod.token)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -2,30 +2,63 @@ package shortener
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"context"
|
|
||||||
"git.xdrm.io/example/aicra/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// tinyModel represents an actual tiny url entry in the database.
|
// tinyModel represents an actual tiny url entry in the database.
|
||||||
type tinyModel BadExpr
|
type tinyModel struct {
|
||||||
|
db *sql.DB
|
||||||
BadDecl
|
tiny string
|
||||||
|
long string
|
||||||
|
}
|
||||||
|
|
||||||
type repository struct {
|
type repository struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRepo returns an initialized repository.
|
// newRepo returns an initialized repository.
|
||||||
func newRepo(db *sql.DB) (*model, error) {
|
func newRepo(db *sql.DB) (*repository, error) {
|
||||||
log.Printf("[service.shortener] creating repository")
|
_, err := db.Exec(`CREATE TABLE if not exists tiny(
|
||||||
|
tiny varchar(30) PRIMARY KEY,
|
||||||
|
long varchar(300) NOT NULL
|
||||||
|
);`)
|
||||||
|
|
||||||
res, err := db.Exec(ctx, `CREATE TABLE if not exist tiny(
|
return &repository{db}, err
|
||||||
tiny varchar(30) PRIMARY,
|
}
|
||||||
target varchar(300) NOT NULL,
|
|
||||||
)`)
|
func (repo *repository) NewModel(tiny string, long string) *tinyModel {
|
||||||
|
return &tinyModel{
|
||||||
|
db: repo.db,
|
||||||
|
tiny: tiny,
|
||||||
|
long: long,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tinyModel) Search() error {
|
||||||
|
row := mod.db.QueryRow(`SELECT tiny,long FROM tiny WHERE tiny = $1 LIMIT 1;`, mod.tiny)
|
||||||
|
|
||||||
|
receiver := &tinyModel{}
|
||||||
|
err := row.Scan(&receiver.tiny, &receiver.long)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &repository{ctx, db}
|
mod.tiny = receiver.tiny
|
||||||
|
mod.long = receiver.long
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tinyModel) Create() error {
|
||||||
|
_, err := mod.db.Exec(`INSERT INTO tiny(tiny,long) VALUES($1,$2);`, mod.tiny, mod.long)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tinyModel) Update() error {
|
||||||
|
_, err := mod.db.Exec(`UPDATE tiny SET long = $1 WHERE tiny = $2;`, mod.long, mod.tiny)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *tinyModel) Delete() error {
|
||||||
|
_, err := mod.db.Exec(`DELETE FROM tiny WHERE tiny = $1;`, mod.tiny)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/example/aicra/service/auth"
|
"git.xdrm.io/example/aicra/service/auth"
|
||||||
"git.xdrm.io/example/aicra/storage"
|
|
||||||
"git.xdrm.io/go/aicra"
|
"git.xdrm.io/go/aicra"
|
||||||
"git.xdrm.io/go/aicra/api"
|
"git.xdrm.io/go/aicra/api"
|
||||||
)
|
)
|
||||||
|
@ -19,28 +18,26 @@ type Service struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a bare service
|
// New returns a bare service
|
||||||
func New(storage *sql.DB, auth *auth.Service) *Service {
|
func New(storage *sql.DB, auth *auth.Service) (*Service, error) {
|
||||||
log.Printf("[service.shortener] creating")
|
log.Printf("[shortener] creating service")
|
||||||
|
|
||||||
service := &Service{
|
log.Printf("[shortener] creating repo")
|
||||||
storage: storage,
|
repo, err := newRepo(storage)
|
||||||
authService: auth,
|
|
||||||
}
|
|
||||||
|
|
||||||
// init repo
|
|
||||||
repo, err := newRepo(db)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[service.shortener] cannot create repo")
|
return nil, err
|
||||||
return service
|
|
||||||
}
|
}
|
||||||
|
log.Printf("[shortener] repo created")
|
||||||
|
|
||||||
log.Printf("[service.shortener] creating")
|
log.Printf("[shortener] service created")
|
||||||
return service
|
return &Service{
|
||||||
|
repo: repo,
|
||||||
|
authService: auth,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire to the aicra server
|
// Wire to the aicra server
|
||||||
func (svc *Service) Wire(server *aicra.Server) {
|
func (svc *Service) Wire(server *aicra.Server) {
|
||||||
log.Printf("[service.shortener] wired")
|
log.Printf("[shortener] service wired")
|
||||||
server.HandleFunc("GET", "/", svc.redirect)
|
server.HandleFunc("GET", "/", svc.redirect)
|
||||||
server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register))
|
server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register))
|
||||||
server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update))
|
server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update))
|
||||||
|
@ -58,15 +55,15 @@ func (svc *Service) redirect(req api.Request, res *api.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. check in db if exists
|
// 2. check in db if exists
|
||||||
longURL := svc.storage.Get(storage.DATA, tinyURL)
|
model := svc.repo.NewModel(tinyURL, "")
|
||||||
if longURL == nil {
|
if model.Search() != nil {
|
||||||
res.SetError(api.ErrorNoMatchFound())
|
res.SetError(api.ErrorNoMatchFound())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. redirect
|
// 3. redirect
|
||||||
res.Status = http.StatusPermanentRedirect
|
res.Status = http.StatusPermanentRedirect
|
||||||
res.Headers.Set("Location", string(longURL))
|
res.Headers.Set("Location", model.long)
|
||||||
res.SetError(api.ErrorSuccess())
|
res.SetError(api.ErrorSuccess())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -87,16 +84,19 @@ func (svc *Service) register(req api.Request, res *api.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. fail if already used
|
// 2. fail if already used
|
||||||
if svc.storage.Get(storage.DATA, tinyURL) != nil {
|
model := svc.repo.NewModel(tinyURL, "")
|
||||||
|
if model.Search() == nil {
|
||||||
res.SetError(api.ErrorAlreadyExists(), "url")
|
res.SetError(api.ErrorAlreadyExists(), "url")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. store association
|
// 3. store association
|
||||||
if !svc.storage.Set(storage.DATA, tinyURL, longURL) {
|
model.long = longURL
|
||||||
|
if model.Create() != nil {
|
||||||
res.SetError(api.ErrorFailure())
|
res.SetError(api.ErrorFailure())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("[shortener] new %q -> %q", model.tiny, model.long)
|
||||||
|
|
||||||
res.SetError(api.ErrorSuccess())
|
res.SetError(api.ErrorSuccess())
|
||||||
}
|
}
|
||||||
|
@ -117,16 +117,19 @@ func (svc *Service) update(req api.Request, res *api.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. fail if not already existing
|
// 2. fail if not already existing
|
||||||
if svc.storage.Get(storage.DATA, tinyURL) == nil {
|
model := svc.repo.NewModel(tinyURL, "")
|
||||||
|
if model.Search() != nil {
|
||||||
res.SetError(api.ErrorNoMatchFound())
|
res.SetError(api.ErrorNoMatchFound())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. update association
|
// 3. update association
|
||||||
if !svc.storage.Set(storage.DATA, tinyURL, longURL) {
|
model.long = longURL
|
||||||
|
if model.Update() != nil {
|
||||||
res.SetError(api.ErrorFailure())
|
res.SetError(api.ErrorFailure())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("[shortener] upd %q -> %q", model.tiny, model.long)
|
||||||
|
|
||||||
res.SetError(api.ErrorSuccess())
|
res.SetError(api.ErrorSuccess())
|
||||||
}
|
}
|
||||||
|
@ -142,16 +145,18 @@ func (svc *Service) delete(req api.Request, res *api.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. fail if not already existing
|
// 2. fail if not already existing
|
||||||
if svc.storage.Get(storage.DATA, tinyURL) == nil {
|
model := svc.repo.NewModel(tinyURL, "")
|
||||||
|
if model.Search() != nil {
|
||||||
res.SetError(api.ErrorNoMatchFound())
|
res.SetError(api.ErrorNoMatchFound())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. update association
|
// 3. delete association
|
||||||
if !svc.storage.Del(storage.DATA, tinyURL) {
|
if model.Delete() != nil {
|
||||||
res.SetError(api.ErrorFailure())
|
res.SetError(api.ErrorFailure())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("[shortener] del %q", model.tiny)
|
||||||
|
|
||||||
res.SetError(api.ErrorSuccess())
|
res.SetError(api.ErrorSuccess())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue