view pkg/controllers/user.go @ 5591:0011f50cf216 surveysperbottleneckid

Removed no longer used alternative api for surveys/ endpoint. As bottlenecks in the summary for SR imports are now identified by their id and no longer by the (not guarantied to be unique!) name, there is no longer the need to request survey data by the name+date tuple (which isn't reliable anyway). So the workaround was now reversed.
author Sascha Wilde <wilde@sha-bang.de>
date Wed, 06 Apr 2022 13:30:29 +0200
parents 79155213c4da
children 6270951dda28
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
//  * Tom Gottfried <tom.gottfried@intevation.de>
//  * Sascha Wilde <sascha.wilde@intevation.de>

package controllers

import (
	"bytes"
	"database/sql"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"text/template"
	"time"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/pkg/auth"
	"gemma.intevation.de/gemma/pkg/log"
	"gemma.intevation.de/gemma/pkg/misc"
	"gemma.intevation.de/gemma/pkg/models"
	"gemma.intevation.de/gemma/pkg/pgxutils"
	"gemma.intevation.de/gemma/pkg/scheduler"

	mw "gemma.intevation.de/gemma/pkg/middleware"
)

const (
	createUserSQL = `INSERT INTO users.list_users
  VALUES ($1, $2, $3, $4, NULL, $5, $6, true)`
	createUserExtentSQL = `INSERT INTO users.list_users
  VALUES ($1, $2, $3, $4,
  ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9, $10, true)`

	updateUserUnprivSQL = `UPDATE users.list_users
  SET (pw, map_extent, email_address)
  = ($2, ST_MakeBox2D(ST_Point($3, $4), ST_Point($5, $6)), $7)
  WHERE username = $1`
	updateUserSQL = `UPDATE users.list_users
  SET (rolname, username, pw, country, map_extent, email_address, report_reciever)
  = ($2, $3, $4, $5, NULL, $6, $7)
  WHERE username = $1`
	updateUserExtentSQL = `UPDATE users.list_users
  SET (rolname, username, pw, country, map_extent, email_address, report_reciever)
  = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10, $11)
  WHERE username = $1`

	deleteUserSQL = `
  WITH del AS (
    DELETE FROM users.list_users
    WHERE username = $1 AND (
      rolname NOT IN ('waterway_admin', 'sys_admin')
      OR NOT EXISTS (SELECT 1 FROM import.imports WHERE username = $1))
    RETURNING *
  ),
  up AS (
    UPDATE users.list_users
    SET (email_address, report_reciever, active) = ('nomail@example.com', false, false)
    WHERE username = $1 AND NOT EXISTS (SELECT * FROM del)
    RETURNING *
  )
  SELECT
    EXISTS (SELECT * FROM del) AS deleted,
    EXISTS (SELECT * FROM up)  AS updated`

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

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

var (
	testSysadminNotifyMailTmpl = template.Must(
		template.New("sysadmin").Parse(`Dear {{ .User }},

this is a test email from the Gemma System Errors notification service.  You
recieved this mail, because a System Administrator triggered the test mail
sending function at {{ .Timestamp }}.

When a critical system error is detected an automated mail will be sent to
{{ .Email }} with details on the error condition.`))

	testWWAdminNotifyMailTmpl = template.Must(
		template.New("waterwayadmin").Parse(`Dear {{ .User }},

this is a test email from the Gemma System Mail notification service.  You
recieved this mail, because a System Administrator triggered the test mail
sending function at {{ .Timestamp }}.

When the status of a data import managed by you changes an automated mail will
be sent to {{ .Email }} with details on the new import status
(including import errors) and details on the corresponding import.`))

	testWWUserNotifyMailTmpl = template.Must(
		template.New("waterwayuser").Parse(`Dear {{ .User }},

this is a test email from the Gemma System Mail notification service.  You
recieved this mail, because a System Administrator triggered the test mail
sending function at {{ .Timestamp }}.`))
)

func deleteUser(req *http.Request) (jr mw.JSONResult, err error) {

	user := mux.Vars(req)["user"]
	if !models.UserName(user).IsValid() {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "error: user invalid",
		}
		return
	}

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

	ctx := req.Context()

	db := mw.JSONConn(req)

	// Remove scheduled tasks.
	ids, err2 := scheduler.ScheduledUserIDs(ctx, db, user)
	if err2 == nil {
		if len(ids) > 0 {
			go func() { scheduler.UnbindByIDs(ids) }()
		}
	} else {
		log.Errorf("%v\n", err2)
	}

	var deleted, updated bool
	if err = db.QueryRowContext(ctx, deleteUserSQL, user).Scan(&deleted, &updated); err != nil {
		return
	}

	var action string

	switch {
	case !deleted && !updated:
		err = mw.JSONError{
			Code:    http.StatusNotFound,
			Message: fmt.Sprintf("Cannot find user %s.", user),
		}
		return
	case deleted:
		action = "deleted"
	case updated:
		action = "deactivated"
	default:
		log.Errorf("Should not happen: user '%s' updated and deleted.\n", user)
		action = "deleted"
	}

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

	log.Infof("User '%s' %s.\n", user, action)

	jr = mw.JSONResult{
		Result: struct {
			Action string `json:"action"`
		}{
			Action: action,
		},
	}
	return
}

func updateUser(req *http.Request) (jr mw.JSONResult, err error) {

	user := models.UserName(mux.Vars(req)["user"])
	if !user.IsValid() {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "error: user invalid",
		}
		return
	}

	newUser := mw.JSONInput(req).(*models.User)
	var res sql.Result

	db := mw.JSONConn(req)

	if s, _ := auth.GetSession(req); s.Roles.Has("sys_admin") {
		if newUser.Extent == nil {
			res, err = db.ExecContext(
				req.Context(),
				updateUserSQL,
				user,
				newUser.Role,
				newUser.User,
				newUser.Password,
				newUser.Country,
				newUser.Email,
				newUser.Reports,
			)
		} else {
			res, err = db.ExecContext(
				req.Context(),
				updateUserExtentSQL,
				user,
				newUser.Role,
				newUser.User,
				newUser.Password,
				newUser.Country,
				newUser.Extent.X1, newUser.Extent.Y1,
				newUser.Extent.X2, newUser.Extent.Y2,
				newUser.Email,
				newUser.Reports,
			)
		}
	} else {
		if newUser.Extent == nil {
			err = mw.JSONError{
				Code:    http.StatusBadRequest,
				Message: "extent is mandatory",
			}
			return
		}
		res, err = db.ExecContext(
			req.Context(),
			updateUserUnprivSQL,
			user,
			newUser.Password,
			newUser.Extent.X1, newUser.Extent.Y1,
			newUser.Extent.X2, newUser.Extent.Y2,
			newUser.Email,
		)
	}

	if err != nil {
		return
	}

	if n, err2 := res.RowsAffected(); err2 == nil && n == 0 {
		err = mw.JSONError{
			Code:    http.StatusNotFound,
			Message: fmt.Sprintf("Cannot find user %s.", user),
		}
		return
	}

	if user != newUser.User {
		// Running in a go routine should not be necessary.
		go func() { auth.Sessions.Logout(string(user)) }()
	}

	log.Infof("User '%s' updated.\n", user)

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

func patchUser(req *http.Request) (jr mw.JSONResult, err error) {

	user := models.UserName(mux.Vars(req)["user"])
	if !user.IsValid() {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "error: user invalid",
		}
		return
	}

	s, ok := auth.GetSession(req)
	if !ok {
		err = mw.JSONError{
			Code:    http.StatusUnauthorized,
			Message: "error: not logged in",
		}
		return
	}

	priv := s.Roles.Has("sys_admin")

	if !priv && s.User != string(user) {
		err = mw.JSONError{
			Code:    http.StatusUnauthorized,
			Message: "error: not allowed to modify someone else",
		}
		return
	}

	var (
		columns   []string
		positions []string
		args      []interface{}
	)

	update := func(column string, value interface{}) {
		columns = append(columns, column)
		positions = append(positions, "$"+strconv.Itoa(len(positions)+1))
		args = append(args, value)
	}

	updateBox := func(column string, extent *models.BoundingBox) {
		columns = append(columns, column)
		pos := len(positions)
		position := fmt.Sprintf("ST_MakeBox2D(ST_Point($%d, $%d), ST_Point($%d, $%d))",
			pos+1, pos+2, pos+3, pos+4)
		positions = append(positions, position)
		args = append(args, extent.X1, extent.Y1, extent.X2, extent.Y2)
	}

	patch := mw.JSONInput(req).(*models.UserPatch)

	if patch.User != nil && priv {
		update("user", *patch.User)
	}
	if patch.Role != nil && priv {
		update("rolname", *patch.Role)
	}
	if patch.Password != nil {
		update("pw", *patch.Password)
	}
	if patch.Email != nil {
		update("email_address", *patch.Email)
	}
	if patch.Country != nil && priv {
		update("country", *patch.Country)
	}
	if patch.Reports != nil && priv {
		update("report_reciever", *patch.Reports)
	}
	if patch.Active != nil && priv {
		update("active", *patch.Active)
	}
	if patch.Extent != nil {
		updateBox("map_extent", patch.Extent)
	}

	var colsS, posS string

	switch len(columns) {
	case 0: // Nothing to do
		jr = mw.JSONResult{
			Code: http.StatusCreated,
			Result: struct {
				Result string `json:"result"`
			}{"success"},
		}
		return
	case 1: // No brackets if there is only one argument.
		colsS = columns[0]
		posS = positions[0]
	default:
		colsS = "(" + strings.Join(columns, ",") + ")"
		posS = "(" + strings.Join(positions, ",") + ")"
	}

	stmt := fmt.Sprintf(
		`UPDATE users.list_users SET %s = %s WHERE username = $%d`,
		colsS,
		posS,
		len(positions)+1)

	args = append(args, user)

	db := mw.JSONConn(req)

	var res sql.Result
	if res, err = db.ExecContext(req.Context(), stmt, args...); err != nil {
		return
	}

	if n, err2 := res.RowsAffected(); err2 == nil && n == 0 {
		err = mw.JSONError{
			Code:    http.StatusNotFound,
			Message: fmt.Sprintf("Cannot find user %s.", user),
		}
		return
	}

	if patch.User != nil && *patch.User != user {
		// Running in a go routine should not be necessary.
		go func() { auth.Sessions.Logout(string(user)) }()
	}

	// Log if the user should be reactivated, even if he is already active.
	var reactivated string
	if patch.Active != nil && *patch.Active && priv {
		reactivated = " (reactivation requested)"
	}

	log.Infof("User '%s' modified%s.\n", user, reactivated)

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

func createUser(req *http.Request) (jr mw.JSONResult, err error) {

	user := mw.JSONInput(req).(*models.User)

	db := mw.JSONConn(req)

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

	if err != nil {
		m, c := pgxutils.ReadableError{Err: err}.MessageAndCode()
		err = mw.JSONError{Code: c, Message: m}
		return
	}

	log.Infof("User '%s' created.\n", user.User)

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

func listUsers(req *http.Request) (jr mw.JSONResult, err error) {

	var rows *sql.Rows

	rows, err = mw.JSONConn(req).QueryContext(req.Context(), listUsersSQL)
	if err != nil {
		return
	}
	defer rows.Close()

	var users []*models.User

	for rows.Next() {
		user := &models.User{Extent: &models.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,
			&user.Reports,
			&user.Active,
		); err != nil {
			return
		}
		users = append(users, user)
	}

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

func listUser(req *http.Request) (jr mw.JSONResult, err error) {

	user := models.UserName(mux.Vars(req)["user"])
	if !user.IsValid() {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "error: user invalid",
		}
		return
	}

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

	err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan(
		&result.Role,
		&result.Country,
		&result.Email,
		&result.Extent.X1, &result.Extent.Y1,
		&result.Extent.X2, &result.Extent.Y2,
		&result.Reports,
	)

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

	jr.Result = result
	return
}

func sendTestMail(req *http.Request) (jr mw.JSONResult, err error) {

	user := models.UserName(mux.Vars(req)["user"])
	if !user.IsValid() {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "error: user invalid",
		}
		return
	}

	userData := &models.User{
		User:   user,
		Extent: &models.BoundingBox{},
	}

	err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan(
		&userData.Role,
		&userData.Country,
		&userData.Email,
		&userData.Extent.X1, &userData.Extent.Y1,
		&userData.Extent.X2, &userData.Extent.Y2,
		&userData.Reports,
	)

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

	var subject string

	var tmplVars = struct {
		User      string
		Timestamp string
		Email     string
	}{
		User:      string(user),
		Timestamp: time.Now().Format("2006-01-02 15:04:05"),
		Email:     string(userData.Email),
	}

	var bodyTmpl *template.Template
	switch userData.Role {
	case "sys_admin":
		subject = "Gemma: Sysadmin Notification TEST"
		bodyTmpl = testSysadminNotifyMailTmpl
	case "waterway_admin":
		subject = "Gemma: Waterway Admin Notification TEST"
		bodyTmpl = testWWAdminNotifyMailTmpl
	default:
		subject = "Gemma: Waterway User Notification TEST"
		bodyTmpl = testWWUserNotifyMailTmpl
	}

	var buf bytes.Buffer
	if err := bodyTmpl.Execute(&buf, &tmplVars); err != nil {
		log.Errorf("%v\n", err)
	}

	err = misc.SendMail(string(userData.Email), subject, buf.String())
	if err != nil {
		return
	}

	jr.Result = &struct {
		Message string `json:"message"`
	}{"Sending test mail."}

	return
}