Add token unwrapping, admin concept and user creation endpoint
This commit is contained in:
parent
7ebb14e431
commit
3c7c32423e
12 changed files with 141 additions and 16 deletions
|
@ -26,7 +26,8 @@ Default configuration can be found in the
|
||||||
- [X] Decide on a visual framework to work with (maybe react-material?)
|
- [X] Decide on a visual framework to work with (maybe react-material?)
|
||||||
- [ ] Add webapp linter
|
- [ ] Add webapp linter
|
||||||
- [X] Add token and login endpoint
|
- [X] Add token and login endpoint
|
||||||
- [ ] Add token reading wrapper at the API level
|
- [X] Add token reading wrapper at the API level
|
||||||
|
- [X] Add the concept of admin users
|
||||||
- [ ] Add games management endpoints and webapp screens
|
- [ ] Add games management endpoints and webapp screens
|
||||||
- [ ] Add chat messages in game
|
- [ ] Add chat messages in game
|
||||||
- [ ] Add websocket messages for new chat post
|
- [ ] Add websocket messages for new chat post
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ctrlz.es/mgdelacroix/craban/model"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const userContextKey = "user"
|
||||||
|
|
||||||
func (a *API) Login(w http.ResponseWriter, r *http.Request) {
|
func (a *API) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
body := ParseBody(r)
|
body := ParseBody(r)
|
||||||
username := body.String("username")
|
username := body.String("username")
|
||||||
|
@ -22,3 +34,61 @@ func (a *API) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
w.Write([]byte(token))
|
w.Write([]byte(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *API) getUserFromToken(tokenStr string) (*model.User, error) {
|
||||||
|
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// Validate the alg is what you expect:
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(*a.App.Config.Secret), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
userIDClaim, ok := claims["userID"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("userID claim is not set")
|
||||||
|
}
|
||||||
|
userID, err := strconv.Atoi(userIDClaim)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.App.GetUserByID(userID)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Malformed claims")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) Secured(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tokenStr := r.Header.Get("Authorization")
|
||||||
|
if tokenStr == "" {
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(tokenStr, "Bearer ") {
|
||||||
|
tokenStr = tokenStr[7:]
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.getUserFromToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Str("token", tokenStr).Msg("cannot get user from token")
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user
|
||||||
|
ctx := context.WithValue(r.Context(), userContextKey, user)
|
||||||
|
fn(w, r.Clone(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserFromRequest(r *http.Request) (*model.User, bool) {
|
||||||
|
user, ok := r.Context().Value(userContextKey).(*model.User)
|
||||||
|
return user, ok
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,46 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.ctrlz.es/mgdelacroix/craban/model"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *API) CreateUser(w http.ResponseWriter, r *http.Request) {
|
func (a *API) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, _ := UserFromRequest(r)
|
||||||
|
if !user.Admin {
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := ParseBody(r)
|
||||||
|
username := body.String("username")
|
||||||
|
password := body.String("password")
|
||||||
|
name := body.String("name")
|
||||||
|
mail := body.String("mail")
|
||||||
|
admin := body.Bool("admin")
|
||||||
|
|
||||||
|
newUser := &model.User{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Name: name,
|
||||||
|
Mail: mail,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := newUser.IsValid(); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("%s: %s", http.StatusText(http.StatusBadRequest), err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUser, err := a.App.CreateUser(username, password, name, mail, admin)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("user couldn't be created")
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON(w, createdUser, 201)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,13 @@ func (b *Body) Int(name string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Body) Bool(name string) bool {
|
||||||
|
if res, ok := (*b)[name].(bool); ok {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func JSON(w http.ResponseWriter, data interface{}, statusCode int) {
|
func JSON(w http.ResponseWriter, data interface{}, statusCode int) {
|
||||||
b, err := json.Marshal(data)
|
b, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
config *model.Config
|
Config *model.Config
|
||||||
store *store.Store
|
Store *store.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(config *model.Config, store *store.Store) *App {
|
func NewApp(config *model.Config, store *store.Store) *App {
|
||||||
return &App{
|
return &App{
|
||||||
config: config,
|
Config: config,
|
||||||
store: store,
|
Store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) Login(username, password string) (string, error) {
|
func (a *App) Login(username, password string) (string, error) {
|
||||||
user, err := a.store.User().GetByUsername(username)
|
user, err := a.Store.User().GetByUsername(username)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return "", nil
|
return "", nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -19,7 +19,7 @@ func (a *App) Login(username, password string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := utils.GenerateToken(user.ID, *a.config.Secret)
|
token, err := utils.GenerateToken(user.ID, *a.Config.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cannot generate token: %w", err)
|
return "", fmt.Errorf("cannot generate token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"git.ctrlz.es/mgdelacroix/craban/utils"
|
"git.ctrlz.es/mgdelacroix/craban/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) CreateUser(username, password, name, mail string) (*model.User, error) {
|
func (a *App) CreateUser(username, password, name, mail string, admin bool) (*model.User, error) {
|
||||||
hashedPassword, err := utils.Encrypt(password)
|
hashedPassword, err := utils.Encrypt(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create user: %w", err)
|
return nil, fmt.Errorf("cannot create user: %w", err)
|
||||||
|
@ -18,19 +18,24 @@ func (a *App) CreateUser(username, password, name, mail string) (*model.User, er
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
Name: name,
|
Name: name,
|
||||||
Mail: mail,
|
Mail: mail,
|
||||||
|
Admin: admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newUser.IsValid(); err != nil {
|
if err := newUser.IsValid(); err != nil {
|
||||||
return nil, fmt.Errorf("invalid user for creation: %w", err)
|
return nil, fmt.Errorf("invalid user for creation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.store.User().Create(newUser)
|
return a.Store.User().Create(newUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ListUsers() ([]*model.User, error) {
|
func (a *App) ListUsers() ([]*model.User, error) {
|
||||||
return a.store.User().List()
|
return a.Store.User().List()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) DeleteUserByUsername(username string) error {
|
func (a *App) DeleteUserByUsername(username string) error {
|
||||||
return a.store.User().DeleteByUsername(username)
|
return a.Store.User().DeleteByUsername(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetUserByID(userID int) (*model.User, error) {
|
||||||
|
return a.Store.User().GetByID(userID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ func CreateUserCmd() *cobra.Command {
|
||||||
cmd.MarkFlagRequired("name")
|
cmd.MarkFlagRequired("name")
|
||||||
cmd.Flags().StringP("mail", "m", "", "the mail of the new user")
|
cmd.Flags().StringP("mail", "m", "", "the mail of the new user")
|
||||||
cmd.MarkFlagRequired("mail")
|
cmd.MarkFlagRequired("mail")
|
||||||
|
cmd.Flags().BoolP("admin", "a", false, "sets the new user as an admin")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -67,6 +68,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) {
|
||||||
password, _ := cmd.Flags().GetString("password")
|
password, _ := cmd.Flags().GetString("password")
|
||||||
name, _ := cmd.Flags().GetString("name")
|
name, _ := cmd.Flags().GetString("name")
|
||||||
mail, _ := cmd.Flags().GetString("mail")
|
mail, _ := cmd.Flags().GetString("mail")
|
||||||
|
admin, _ := cmd.Flags().GetBool("admin")
|
||||||
|
|
||||||
config, _ := cmd.Flags().GetString("config")
|
config, _ := cmd.Flags().GetString("config")
|
||||||
srv, err := server.NewServerWithConfigPath(config)
|
srv, err := server.NewServerWithConfigPath(config)
|
||||||
|
@ -76,7 +78,7 @@ func createUserCmdF(cmd *cobra.Command, _ []string) {
|
||||||
}
|
}
|
||||||
defer srv.Store.Close()
|
defer srv.Store.Close()
|
||||||
|
|
||||||
user, err := srv.App.CreateUser(username, password, name, mail)
|
user, err := srv.App.CreateUser(username, password, name, mail, admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("user couldn't be created")
|
log.Error().Err(err).Msg("user couldn't be created")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -10,6 +10,7 @@ type User struct {
|
||||||
Mail string `json:"mail"`
|
Mail string `json:"mail"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) IsValid() error {
|
func (u *User) IsValid() error {
|
||||||
|
|
|
@ -57,7 +57,8 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
mail VARCHAR(255) UNIQUE NOT NULL,
|
mail VARCHAR(255) UNIQUE NOT NULL,
|
||||||
username VARCHAR(255) UNIQUE NOT NULL,
|
username VARCHAR(255) UNIQUE NOT NULL,
|
||||||
password VARCHAR(255) NOT NULL
|
password VARCHAR(255) NOT NULL,
|
||||||
|
admin BOOLEAN NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS game (
|
CREATE TABLE IF NOT EXISTS game (
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"git.ctrlz.es/mgdelacroix/craban/model"
|
"git.ctrlz.es/mgdelacroix/craban/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var userColumns = []string{"id", "name", "mail", "username", "password"}
|
var userColumns = []string{"id", "name", "mail", "username", "password", "admin"}
|
||||||
|
|
||||||
type UserStore struct {
|
type UserStore struct {
|
||||||
Conn *sql.DB
|
Conn *sql.DB
|
||||||
|
@ -34,6 +34,7 @@ func (us *UserStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
|
||||||
&user.Mail,
|
&user.Mail,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.Password,
|
&user.Password,
|
||||||
|
&user.Admin,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -94,7 +95,7 @@ func (us *UserStore) GetByUsername(username string) (*model.User, error) {
|
||||||
func (us *UserStore) Create(user *model.User) (*model.User, error) {
|
func (us *UserStore) Create(user *model.User) (*model.User, error) {
|
||||||
query := us.Q().Insert("users").
|
query := us.Q().Insert("users").
|
||||||
Columns(userColumns[1:]...).
|
Columns(userColumns[1:]...).
|
||||||
Values(user.Name, user.Mail, user.Username, user.Password)
|
Values(user.Name, user.Mail, user.Username, user.Password, user.Admin)
|
||||||
|
|
||||||
res, err := query.Exec()
|
res, err := query.Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (w *WebServer) RegisterRoutes(api *api.API) {
|
||||||
apiRouter := r.PathPrefix("/api").Subrouter()
|
apiRouter := r.PathPrefix("/api").Subrouter()
|
||||||
|
|
||||||
apiRouter.HandleFunc("/login", api.Login).Methods("POST")
|
apiRouter.HandleFunc("/login", api.Login).Methods("POST")
|
||||||
apiRouter.HandleFunc("/user", api.CreateUser).Methods("POST")
|
apiRouter.HandleFunc("/user", api.Secured(api.CreateUser)).Methods("POST")
|
||||||
|
|
||||||
staticFSSub, _ := fs.Sub(staticFS, "static")
|
staticFSSub, _ := fs.Sub(staticFS, "static")
|
||||||
staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))}
|
staticFSHandler := StaticFSHandler{http.FileServer(http.FS(staticFSSub))}
|
||||||
|
|
Loading…
Reference in a new issue