diff --git a/.gitignore b/.gitignore index 2fe74b0..2ba895d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .vscode .realize +debug +go-base + # Binaries for programs and plugins *.exe *.dll diff --git a/README.md b/README.md index 77357ac..7f671c6 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,30 @@ Easily extendible RESTful API boilerplate aiming to follow idiomatic go and best practice. ### Features -* PostgreSQL support including migrations using [go-pg](https://github.com/go-pg/pg) -* Structured logging with [Logrus](https://github.com/sirupsen/logrus) -* Routing with [chi router](https://github.com/go-chi/chi) -* JWT Authentication using [jwt-go](https://github.com/dgrijalva/jwt-go) with passwordless email authentication (could be easily extended to use passwords instead) -* Configuration using [viper](https://github.com/spf13/viper) -* CLI features using [cobra](https://github.com/spf13/cobra) -* [dep](https://github.com/golang/dep) for dependency management +- Configuration using [viper](https://github.com/spf13/viper) +- CLI features using [cobra](https://github.com/spf13/cobra) +- [dep](https://github.com/golang/dep) for dependency management +- PostgreSQL support including migrations using [go-pg](https://github.com/go-pg/pg) +- Structured logging with [Logrus](https://github.com/sirupsen/logrus) +- Routing with [chi router](https://github.com/go-chi/chi) and middleware +- JWT Authentication using [jwt-go](https://github.com/dgrijalva/jwt-go) in combination with passwordless email authentication (could be easily extended to use passwords instead) +- Request data validation using [ozzo-validation](https://github.com/go-ozzo/ozzo-validation) +- HTML emails with [gomail](https://github.com/go-gomail/gomail) +### Start Application +- Clone this repository +- Create a postgres database and set environment variable *DATABASE_URL* accordingly if not using same as default +- Build the application: ```go build``` to create ```go-base``` binary or use ```go run main.go``` instead in the following commands +- Initialize the database migrations table: ```go-base migrate init``` +- Run all migrations found in ./database/migrate with: ```go-base migrate``` +- Run the application: ```go-base serve``` +#### Demo client application +For demonstration of the login and account management features this API also serves a Single Page Application (SPA) as a Progressive Web App (PWA) done with [Quasar Framework](http://quasar-framework.org) which itself is powered by [Vue.js](https://vuejs.org). The client's source code can be found [here](https://github.com/dhax/go-base-client). + +If no valid email smtp settings are provided by environment variables, emails will be print to stdout showing the login token. Use one of the following users for login: +- admin@boot.io (has access to admin panel) +- user@boot.io ### Environment Variables diff --git a/api/admin/accounts.go b/api/admin/accounts.go index 967c3df..32f4ffe 100644 --- a/api/admin/accounts.go +++ b/api/admin/accounts.go @@ -118,7 +118,7 @@ func (rs *AccountResource) list(w http.ResponseWriter, r *http.Request) { func (rs *AccountResource) create(w http.ResponseWriter, r *http.Request) { data := &accountRequest{} if err := render.Bind(r, data); err != nil { - render.Respond(w, r, ErrInvalidRequest(err)) + render.Render(w, r, ErrInvalidRequest(err)) return } diff --git a/api/app/account.go b/api/app/account.go index 8e18988..0a1cdf5 100644 --- a/api/app/account.go +++ b/api/app/account.go @@ -145,12 +145,12 @@ func (d *tokenRequest) Bind(r *http.Request) error { func (rs *AccountResource) updateToken(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "tokenID")) if err != nil { - render.Respond(w, r, ErrBadRequest) + render.Render(w, r, ErrBadRequest) return } data := &tokenRequest{} if err := render.Bind(r, data); err != nil { - render.Respond(w, r, ErrInvalidRequest(err)) + render.Render(w, r, ErrInvalidRequest(err)) return } acc := r.Context().Value(ctxAccount).(*models.Account) @@ -160,7 +160,7 @@ func (rs *AccountResource) updateToken(w http.ResponseWriter, r *http.Request) { ID: t.ID, Identifier: data.Identifier, }); err != nil { - render.Respond(w, r, ErrInvalidRequest(err)) + render.Render(w, r, ErrInvalidRequest(err)) return } } @@ -171,7 +171,7 @@ func (rs *AccountResource) updateToken(w http.ResponseWriter, r *http.Request) { func (rs *AccountResource) deleteToken(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "tokenID")) if err != nil { - render.Respond(w, r, ErrBadRequest) + render.Render(w, r, ErrBadRequest) return } acc := r.Context().Value(ctxAccount).(*models.Account) diff --git a/auth/crypto.go b/auth/crypto.go index c7054e9..a5c1467 100644 --- a/auth/crypto.go +++ b/auth/crypto.go @@ -1,20 +1,20 @@ package auth import ( - "math/rand" - "time" + "crypto/rand" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func randStringBytes(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] + buf := make([]byte, n) + _, err := rand.Read(buf) + if err != nil { + panic(err) } - return string(b) + + for k, v := range buf { + buf[k] = letterBytes[v%byte(len(letterBytes))] + } + return string(buf) } diff --git a/auth/handler.go b/auth/handler.go index b57fd4a..cafc68d 100644 --- a/auth/handler.go +++ b/auth/handler.go @@ -139,7 +139,7 @@ func (rs *Resource) token(w http.ResponseWriter, r *http.Request) { if err := rs.store.SaveRefreshToken(token); err != nil { log(r).Error(err) - render.Respond(w, r, ErrInternalServerError) + render.Render(w, r, ErrInternalServerError) return } @@ -148,7 +148,7 @@ func (rs *Resource) token(w http.ResponseWriter, r *http.Request) { acc.LastLogin = time.Now() if err := rs.store.UpdateAccount(acc); err != nil { log(r).Error(err) - render.Respond(w, r, ErrInternalServerError) + render.Render(w, r, ErrInternalServerError) return } @@ -185,14 +185,14 @@ func (rs *Resource) refresh(w http.ResponseWriter, r *http.Request) { access, refresh := rs.Token.GenTokenPair(acc, token) if err := rs.store.SaveRefreshToken(token); err != nil { log(r).Error(err) - render.Respond(w, r, ErrInternalServerError) + render.Render(w, r, ErrInternalServerError) return } acc.LastLogin = time.Now() if err := rs.store.UpdateAccount(acc); err != nil { log(r).Error(err) - render.Respond(w, r, ErrInternalServerError) + render.Render(w, r, ErrInternalServerError) return }