diff --git a/main.go b/main.go index 0282289..4f27a65 100644 --- a/main.go +++ b/main.go @@ -37,15 +37,21 @@ func main() { defer db.Close() // 4. init services - authService := auth.New(db) - shortenerService := shortener.New(db, authService) + authService, err := auth.New(db) + 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 shortenerService.Wire(server) authService.Wire(server) // 6. listen and serve - log.Printf("[server] listening") + log.Printf("[server] listening\n\n") err = http.ListenAndServe(listenTo, server) if err != nil { log.Fatalf("/!\\ cannot listen: %v\n", err) diff --git a/service/auth/auth.go b/service/auth/auth.go index bb36e61..6f7be83 100644 --- a/service/auth/auth.go +++ b/service/auth/auth.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "git.xdrm.io/example/aicra/storage" "git.xdrm.io/go/aicra" "git.xdrm.io/go/aicra/api" ) @@ -17,19 +16,29 @@ import ( // Service manages the url shortener type Service struct { storage *sql.DB + repo *repository } // New returns a bare service -func New(storage *sql.DB) *Service { - log.Printf("[service.auth] created") - return &Service{ - storage: storage, +func New(storage *sql.DB) (*Service, error) { + log.Printf("[auth] creating service") + + 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 func (svc *Service) Wire(server *aicra.Server) { - log.Printf("[service.auth] wired") + log.Printf("[auth] service wired") 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)) // 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()) return } // 4. return data + log.Printf("[auth] new token") res.Data["token"] = token res.SetError(api.ErrorSuccess()) @@ -75,11 +86,15 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc { // fail if invalid header if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") { - res.SetError(api.ErrorPermission()) + res.SetError(api.ErrorToken()) 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 for _, scope := range req.Scope { @@ -88,7 +103,7 @@ func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc { } // success - if scope[0] == string(tokenRole) { + if scope[0] == model.role { handler(req, res) return } diff --git a/service/auth/model.go b/service/auth/model.go new file mode 100644 index 0000000..38b813b --- /dev/null +++ b/service/auth/model.go @@ -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 +} diff --git a/service/shortener/model.go b/service/shortener/model.go index 3d27e4a..9d0a911 100644 --- a/service/shortener/model.go +++ b/service/shortener/model.go @@ -2,30 +2,63 @@ package shortener import ( "database/sql" - "context" - "git.xdrm.io/example/aicra/storage" ) // tinyModel represents an actual tiny url entry in the database. -type tinyModel BadExpr - -BadDecl +type tinyModel struct { + db *sql.DB + tiny string + long string +} type repository struct { db *sql.DB } // newRepo returns an initialized repository. -func newRepo(db *sql.DB) (*model, error) { - log.Printf("[service.shortener] creating repository") +func newRepo(db *sql.DB) (*repository, error) { + _, 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( - tiny varchar(30) PRIMARY, - target varchar(300) NOT NULL, - )`) + return &repository{db}, err +} + +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 { - 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 } diff --git a/service/shortener/shortener.go b/service/shortener/shortener.go index f15bc0f..926dee3 100644 --- a/service/shortener/shortener.go +++ b/service/shortener/shortener.go @@ -6,7 +6,6 @@ import ( "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" ) @@ -19,28 +18,26 @@ type Service struct { } // New returns a bare service -func New(storage *sql.DB, auth *auth.Service) *Service { - log.Printf("[service.shortener] creating") +func New(storage *sql.DB, auth *auth.Service) (*Service, error) { + log.Printf("[shortener] creating service") - service := &Service{ - storage: storage, - authService: auth, - } - - // init repo - repo, err := newRepo(db) + log.Printf("[shortener] creating repo") + repo, err := newRepo(storage) if err != nil { - log.Printf("[service.shortener] cannot create repo") - return service + return nil, err } + log.Printf("[shortener] repo created") - log.Printf("[service.shortener] creating") - return service + log.Printf("[shortener] service created") + return &Service{ + repo: repo, + authService: auth, + }, nil } // Wire to the 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("POST", "/", svc.authService.CheckToken(svc.register)) 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 - longURL := svc.storage.Get(storage.DATA, tinyURL) - if longURL == nil { + model := svc.repo.NewModel(tinyURL, "") + if model.Search() != nil { res.SetError(api.ErrorNoMatchFound()) return } // 3. redirect res.Status = http.StatusPermanentRedirect - res.Headers.Set("Location", string(longURL)) + res.Headers.Set("Location", model.long) res.SetError(api.ErrorSuccess()) } @@ -87,16 +84,19 @@ func (svc *Service) register(req api.Request, res *api.Response) { } // 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") return } // 3. store association - if !svc.storage.Set(storage.DATA, tinyURL, longURL) { + 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()) } @@ -117,16 +117,19 @@ func (svc *Service) update(req api.Request, res *api.Response) { } // 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()) return } // 3. update association - if !svc.storage.Set(storage.DATA, tinyURL, longURL) { + 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()) } @@ -142,16 +145,18 @@ func (svc *Service) delete(req api.Request, res *api.Response) { } // 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()) return } - // 3. update association - if !svc.storage.Del(storage.DATA, tinyURL) { + // 3. delete association + if model.Delete() != nil { res.SetError(api.ErrorFailure()) return } + log.Printf("[shortener] del %q", model.tiny) res.SetError(api.ErrorSuccess()) }