2024-04-03 02:43:57 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
2024-05-07 13:38:31 +08:00
|
|
|
"regexp"
|
2024-04-03 02:43:57 +08:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2024-05-26 02:33:41 +08:00
|
|
|
"github.com/knadh/listmonk/internal/auth"
|
2024-07-16 01:34:00 +08:00
|
|
|
"github.com/knadh/listmonk/internal/core"
|
2024-05-07 13:38:31 +08:00
|
|
|
"github.com/knadh/listmonk/internal/utils"
|
2024-04-03 02:43:57 +08:00
|
|
|
"github.com/knadh/listmonk/models"
|
|
|
|
"github.com/labstack/echo/v4"
|
2024-05-07 13:38:31 +08:00
|
|
|
"gopkg.in/volatiletech/null.v6"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
reUsername = regexp.MustCompile("^[a-zA-Z0-9_\\-\\.]+$")
|
2024-04-03 02:43:57 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
// handleGetUsers retrieves users.
|
|
|
|
func handleGetUsers(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
userID, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
)
|
|
|
|
|
|
|
|
// Fetch one.
|
|
|
|
single := false
|
|
|
|
if userID > 0 {
|
|
|
|
single = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if single {
|
2024-05-23 14:24:10 +08:00
|
|
|
out, err := app.core.GetUser(userID, "", "")
|
2024-04-03 02:43:57 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-05-23 14:24:10 +08:00
|
|
|
|
|
|
|
out.Password = null.String{}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all users.
|
|
|
|
out, err := app.core.GetUsers()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-05-23 14:24:10 +08:00
|
|
|
for n := range out {
|
|
|
|
out[n].Password = null.String{}
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCreateUser handles user creation.
|
|
|
|
func handleCreateUser(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
u = models.User{}
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := c.Bind(&u); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Username = strings.TrimSpace(u.Username)
|
|
|
|
u.Name = strings.TrimSpace(u.Name)
|
2024-05-07 13:38:31 +08:00
|
|
|
email := strings.TrimSpace(u.Email.String)
|
2024-04-03 02:43:57 +08:00
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
// Validate fields.
|
2024-05-23 14:24:10 +08:00
|
|
|
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
2024-04-03 02:43:57 +08:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
|
|
}
|
2024-05-07 13:38:31 +08:00
|
|
|
if !reUsername.MatchString(u.Username) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
|
|
}
|
|
|
|
if u.Type != models.UserTypeAPI {
|
|
|
|
if !utils.ValidateEmail(email) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
|
|
|
}
|
|
|
|
if u.PasswordLogin {
|
|
|
|
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
|
|
}
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
2024-05-07 13:38:31 +08:00
|
|
|
|
|
|
|
u.Email = null.String{String: email, Valid: true}
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Name == "" {
|
|
|
|
u.Name = u.Username
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
// Create the user in the database.
|
2024-07-16 01:34:00 +08:00
|
|
|
user, err := app.core.CreateUser(u)
|
2024-04-03 02:43:57 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-07-16 01:34:00 +08:00
|
|
|
if user.Type != models.UserTypeAPI {
|
|
|
|
user.Password = null.String{}
|
2024-06-16 16:20:04 +08:00
|
|
|
}
|
2024-04-03 02:43:57 +08:00
|
|
|
|
2024-07-16 01:34:00 +08:00
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
2024-10-26 22:17:01 +08:00
|
|
|
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
2024-07-16 01:34:00 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{user})
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// handleUpdateUser handles user modification.
|
|
|
|
func handleUpdateUser(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
)
|
|
|
|
|
|
|
|
if id < 1 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Incoming params.
|
|
|
|
var u models.User
|
|
|
|
if err := c.Bind(&u); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate.
|
|
|
|
u.Username = strings.TrimSpace(u.Username)
|
|
|
|
u.Name = strings.TrimSpace(u.Name)
|
2024-05-07 13:38:31 +08:00
|
|
|
email := strings.TrimSpace(u.Email.String)
|
2024-04-03 02:43:57 +08:00
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
// Validate fields.
|
2024-05-23 14:24:10 +08:00
|
|
|
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
2024-04-03 02:43:57 +08:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
|
|
}
|
2024-05-07 13:38:31 +08:00
|
|
|
if !reUsername.MatchString(u.Username) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
|
|
}
|
2024-04-03 02:43:57 +08:00
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
if u.Type != models.UserTypeAPI {
|
|
|
|
if !utils.ValidateEmail(email) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
|
|
|
}
|
|
|
|
if u.PasswordLogin && u.Password.String != "" {
|
2024-04-03 02:43:57 +08:00
|
|
|
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
|
|
}
|
|
|
|
|
2024-05-07 13:38:31 +08:00
|
|
|
if u.Password.String != "" {
|
|
|
|
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Get the existing user for password validation.
|
2024-05-23 14:24:10 +08:00
|
|
|
user, err := app.core.GetUser(id, "", "")
|
2024-05-07 13:38:31 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If password login is enabled, but there's no password in the DB and there's no incoming
|
|
|
|
// password, throw an error.
|
|
|
|
if !user.HasPassword {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
|
|
}
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
|
|
|
}
|
2024-05-07 13:38:31 +08:00
|
|
|
|
|
|
|
u.Email = null.String{String: email, Valid: true}
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Name == "" {
|
|
|
|
u.Name = u.Username
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
|
|
|
|
2024-07-16 01:34:00 +08:00
|
|
|
// Update the user in the DB.
|
|
|
|
user, err := app.core.UpdateUser(id, u)
|
2024-04-03 02:43:57 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-07-16 01:34:00 +08:00
|
|
|
// Clear the pasword before sending outside.
|
|
|
|
user.Password = null.String{}
|
|
|
|
|
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
2024-10-26 22:17:01 +08:00
|
|
|
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
2024-07-16 01:34:00 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{user})
|
2024-04-03 02:43:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// handleDeleteUsers handles user deletion, either a single one (ID in the URI), or a list.
|
|
|
|
func handleDeleteUsers(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
|
|
|
ids []int
|
|
|
|
)
|
|
|
|
|
|
|
|
if id < 1 && len(ids) == 0 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if id > 0 {
|
|
|
|
ids = append(ids, int(id))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := app.core.DeleteUsers(ids); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-07-16 01:34:00 +08:00
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
2024-10-26 22:17:01 +08:00
|
|
|
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
2024-07-16 01:34:00 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-04-03 02:43:57 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
2024-05-26 02:33:41 +08:00
|
|
|
|
|
|
|
// handleGetUserProfile fetches the uesr profile for the currently logged in user.
|
|
|
|
func handleGetUserProfile(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
user = c.Get(auth.UserKey).(models.User)
|
|
|
|
)
|
|
|
|
user.Password.String = ""
|
|
|
|
user.Password.Valid = false
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{user})
|
|
|
|
}
|
2024-05-31 02:07:20 +08:00
|
|
|
|
|
|
|
// handleUpdateUserProfile update's the current user's profile.
|
|
|
|
func handleUpdateUserProfile(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
user = c.Get(auth.UserKey).(models.User)
|
|
|
|
)
|
|
|
|
|
|
|
|
u := models.User{}
|
|
|
|
if err := c.Bind(&u); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
u.PasswordLogin = user.PasswordLogin
|
|
|
|
u.Name = strings.TrimSpace(u.Name)
|
|
|
|
email := strings.TrimSpace(u.Email.String)
|
|
|
|
|
|
|
|
// Validate fields.
|
2024-07-09 03:12:29 +08:00
|
|
|
if user.PasswordLogin {
|
|
|
|
if !utils.ValidateEmail(email) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
|
|
|
}
|
|
|
|
u.Email = null.String{String: email, Valid: true}
|
2024-05-31 02:07:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if u.PasswordLogin && u.Password.String != "" {
|
|
|
|
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-02 20:13:56 +08:00
|
|
|
out, err := app.core.UpdateUserProfile(user.ID, u)
|
2024-05-31 02:07:20 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
out.Password = null.String{}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
2024-07-16 01:34:00 +08:00
|
|
|
|
2024-10-26 22:17:01 +08:00
|
|
|
// cacheUsers fetches (API) users and caches them in the auth module.
|
|
|
|
// It also returns a bool indicating whether there are any actual users in the DB at all,
|
|
|
|
// which if there aren't, the first time user setup needs to be run.
|
|
|
|
func cacheUsers(co *core.Core, a *auth.Auth) (bool, error) {
|
2024-07-16 01:34:00 +08:00
|
|
|
allUsers, err := co.GetUsers()
|
|
|
|
if err != nil {
|
2024-10-26 22:17:01 +08:00
|
|
|
return false, err
|
2024-07-16 01:34:00 +08:00
|
|
|
}
|
|
|
|
|
2024-10-26 22:17:01 +08:00
|
|
|
hasUser := false
|
2024-07-16 01:34:00 +08:00
|
|
|
apiUsers := make([]models.User, 0, len(allUsers))
|
|
|
|
for _, u := range allUsers {
|
|
|
|
if u.Type == models.UserTypeAPI && u.Status == models.UserStatusEnabled {
|
|
|
|
apiUsers = append(apiUsers, u)
|
|
|
|
}
|
2024-10-26 22:17:01 +08:00
|
|
|
|
|
|
|
if u.Type == models.UserTypeUser {
|
|
|
|
hasUser = true
|
|
|
|
}
|
2024-07-16 01:34:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
a.CacheAPIUsers(apiUsers)
|
2024-10-26 22:17:01 +08:00
|
|
|
return hasUser, nil
|
2024-07-16 01:34:00 +08:00
|
|
|
}
|