view controllers/user.go @ 257:dfc2b035e055

Slimming down the signature of the JSONHandler type to not take the http.ResponseWriter. Idea of this handler is to simply transform JSON to JSON. The input is already parsed. The output is generated from JSONResult. So there is no need to pass the ResponseWriter to the handler function.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 27 Jul 2018 13:03:56 +0200
parents de6fdb316b8f
children ab9859981ee3
line wrap: on
line source

package controllers

import (
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"strings"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/auth"
)

type (
	Email   string
	Country string
	Role    string

	BoundingBox struct {
		X1 float64 `json:"x1"`
		Y1 float64 `json:"y1"`
		X2 float64 `json:"x2"`
		Y2 float64 `json:"y2"`
	}

	User struct {
		User     string       `json:"user"`
		Role     Role         `json:"role"`
		Password string       `json:"password,omitempty"`
		Email    Email        `json:"email"`
		Country  Country      `json:"country"`
		Extent   *BoundingBox `json:"extent"`
	}
)

const (
	createUserSQL       = `SELECT sys_admin.create_user($1, $2, $3, $4, NULL, $5)`
	createUserExtentSQL = `SELECT sys_admin.create_user($1, $2, $3, $4,
  ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)`

	updateUserSQL       = `SELECT sys_admin.update_user($1, $2, $3, $4, $5, NULL, $6)`
	updateUserExtentSQL = `SELECT sys_admin.update_user($1, $2, $3, $4, $5,
  ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10)`

	deleteUserSQL = `SELECT sys_admin.delete_user($1)`

	listUsersSQL = `SELECT
  rolname,
  username,
  country,
  email_address,
  ST_XMin(map_extent), ST_YMin(map_extent),
  ST_XMax(map_extent), ST_YMax(map_extent)
FROM sys_admin.list_users`

	listUserSQL = `SELECT
  rolname,
  country,
  email_address,
  ST_XMin(map_extent), ST_YMin(map_extent),
  ST_XMax(map_extent), ST_YMax(map_extent)
FROM sys_admin.list_users
WHERE username = $1`
)

var errNoString = errors.New("Not a string")

var (
	// https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
	emailRe = regexp.MustCompile(
		`(?:[a-z0-9!#$%&'*+/=?^_` + "`" +
			`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` + "`" +
			`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]` +
			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")` +
			`@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?` +
			`|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}` +
			`(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]` +
			`:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]` +
			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])`)
	errNoEmailAddress = errors.New("Not a valid email address")
)

func (e *Email) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return err
	}
	if !emailRe.MatchString(s) {
		return errNoEmailAddress
	}
	*e = Email(s)
	return nil
}

func (e Email) Value() (driver.Value, error) {
	return string(e), nil
}

func (e *Email) Scan(src interface{}) (err error) {
	if s, ok := src.(string); ok {
		*e = Email(s)
	} else {
		err = errNoString
	}
	return
}

var (
	validCountries = []string{
		"AT", "BG", "DE", "HU", "HR",
		"MD", "RO", "RS", "SK", "UA",
	}
	errNoValidCountry = errors.New("Not a valid country")
)

func (c *Country) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return err
	}
	s = strings.ToUpper(s)
	for _, v := range validCountries {
		if v == s {
			*c = Country(v)
			return nil
		}
	}
	return errNoValidCountry
}

func (c Country) Value() (driver.Value, error) {
	return string(c), nil
}

func (c *Country) Scan(src interface{}) (err error) {
	if s, ok := src.(string); ok {
		*c = Country(s)
	} else {
		err = errNoString
	}
	return
}

var (
	validRoles = []string{
		"waterway_user",
		"waterway_admin",
		"sys_admin",
	}
	errNoValidRole = errors.New("Not a valid role")
)

func (r Role) Value() (driver.Value, error) {
	return string(r), nil
}

func (r *Role) Scan(src interface{}) (err error) {
	if s, ok := src.(string); ok {
		*r = Role(s)
	} else {
		err = errNoString
	}
	return
}

func (r *Role) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return err
	}
	s = strings.ToLower(s)
	for _, v := range validRoles {
		if v == s {
			*r = Role(v)
			return nil
		}
	}
	return errNoValidRole
}

func deleteUser(
	input interface{}, req *http.Request,
	db *sql.DB,
) (jr JSONResult, err error) {

	user := mux.Vars(req)["user"]
	if user == "" {
		err = JSONError{http.StatusBadRequest, "error: user empty"}
		return
	}

	session, _ := auth.GetSession(req)
	if session.User == user {
		err = JSONError{http.StatusBadRequest, "error: cannot delete yourself"}
		return
	}

	if _, err = db.Exec(deleteUserSQL, user); err != nil {
		return
	}

	// Running in a go routine should not be necessary.
	go func() { auth.ConnPool.Logout(user) }()

	jr = JSONResult{Code: http.StatusNoContent}
	return
}

func updateUser(
	input interface{}, req *http.Request,
	db *sql.DB,
) (jr JSONResult, err error) {

	user := mux.Vars(req)["user"]
	if user == "" {
		err = JSONError{http.StatusBadRequest, "error: user empty"}
		return
	}

	newUser := input.(*User)

	if newUser.Extent == nil {
		_, err = db.Exec(
			updateUserSQL,
			user,
			newUser.Role,
			newUser.User,
			newUser.Password,
			newUser.Country,
			newUser.Email,
		)
	} else {
		_, err = db.Exec(
			updateUserExtentSQL,
			user,
			newUser.Role,
			newUser.User,
			newUser.Password,
			newUser.Country,
			newUser.Extent.X1, newUser.Extent.Y1,
			newUser.Extent.X2, newUser.Extent.Y2,
			newUser.Email,
		)
	}

	if err != nil {
		return
	}

	jr = JSONResult{
		Code: http.StatusCreated,
		Result: struct {
			Result string `json:"result"`
		}{
			Result: "success",
		},
	}
	return
}

func createUser(
	input interface{}, req *http.Request,
	db *sql.DB,
) (jr JSONResult, err error) {

	user := input.(*User)

	if user.Extent == nil {
		_, err = db.Exec(
			createUserSQL,
			user.Role,
			user.User,
			user.Password,
			user.Country,
			user.Email,
		)
	} else {
		_, err = db.Exec(
			createUserExtentSQL,
			user.Role,
			user.User,
			user.Password,
			user.Country,
			user.Extent.X1, user.Extent.Y1,
			user.Extent.X2, user.Extent.Y2,
			user.Email,
		)
	}

	if err != nil {
		return
	}

	jr = JSONResult{
		Code: http.StatusCreated,
		Result: struct {
			Result string `json:"result"`
		}{
			Result: "success",
		},
	}
	return
}

func listUsers(
	input interface{}, req *http.Request,
	db *sql.DB,
) (jr JSONResult, err error) {

	var rows *sql.Rows

	rows, err = db.Query(listUsersSQL)
	if err != nil {
		return
	}
	defer rows.Close()

	var users []*User

	for rows.Next() {
		user := &User{Extent: &BoundingBox{}}
		if err = rows.Scan(
			&user.Role,
			&user.User,
			&user.Country,
			&user.Email,
			&user.Extent.X1, &user.Extent.Y1,
			&user.Extent.X2, &user.Extent.Y2,
		); err != nil {
			return
		}
		users = append(users, user)
	}

	jr = JSONResult{
		Result: struct {
			Users []*User `json:"users"`
		}{
			Users: users,
		},
	}
	return
}

func listUser(
	input interface{}, req *http.Request,
	db *sql.DB,
) (jr JSONResult, err error) {

	user := mux.Vars(req)["user"]
	if user == "" {
		err = JSONError{http.StatusBadRequest, "error: user empty"}
		return
	}

	result := &User{
		User:   user,
		Extent: &BoundingBox{},
	}

	err = db.QueryRow(listUserSQL, user).Scan(
		&result.Role,
		&result.Country,
		&result.Email,
		&result.Extent.X1, &result.Extent.Y1,
		&result.Extent.X2, &result.Extent.Y2,
	)

	switch {
	case err == sql.ErrNoRows:
		err = JSONError{
			Code:    http.StatusNotFound,
			Message: fmt.Sprintf("Cannot find user %s.", user),
		}
		return
	case err != nil:
		return
	}

	jr.Result = result
	return
}