mirror of
https://github.com/knadh/listmonk.git
synced 2025-10-06 13:26:17 +08:00
This commit splits roles into two, user roles and list roles, both of which are attached separately to a user. List roles are collection of lists each with read|write permissions, while user roles now have all permissions except for per-list ones. This allows for easier management of roles, eliminating the need to clone and create new roles just to adjust specific list permissions.
288 lines
7.2 KiB
Go
288 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/knadh/listmonk/internal/auth"
|
|
"github.com/knadh/listmonk/internal/core"
|
|
"github.com/knadh/listmonk/internal/utils"
|
|
"github.com/knadh/listmonk/models"
|
|
"github.com/labstack/echo/v4"
|
|
"gopkg.in/volatiletech/null.v6"
|
|
)
|
|
|
|
var (
|
|
reUsername = regexp.MustCompile("^[a-zA-Z0-9_\\-\\.]+$")
|
|
)
|
|
|
|
// 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 {
|
|
out, err := app.core.GetUser(userID, "", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
out.Password = null.String{}
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
}
|
|
|
|
// Get all users.
|
|
out, err := app.core.GetUsers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for n := range out {
|
|
out[n].Password = null.String{}
|
|
}
|
|
|
|
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)
|
|
email := strings.TrimSpace(u.Email.String)
|
|
|
|
// Validate fields.
|
|
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
}
|
|
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"))
|
|
}
|
|
}
|
|
|
|
u.Email = null.String{String: email, Valid: true}
|
|
}
|
|
|
|
if u.Name == "" {
|
|
u.Name = u.Username
|
|
}
|
|
|
|
// Create the user in the database.
|
|
user, err := app.core.CreateUser(u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user.Type != models.UserTypeAPI {
|
|
user.Password = null.String{}
|
|
}
|
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
|
if err := cacheAPIUsers(app.core, app.auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{user})
|
|
}
|
|
|
|
// 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)
|
|
email := strings.TrimSpace(u.Email.String)
|
|
|
|
// Validate fields.
|
|
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
}
|
|
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 && u.Password.String != "" {
|
|
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
}
|
|
|
|
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.
|
|
user, err := app.core.GetUser(id, "", "")
|
|
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"))
|
|
}
|
|
}
|
|
}
|
|
|
|
u.Email = null.String{String: email, Valid: true}
|
|
}
|
|
|
|
if u.Name == "" {
|
|
u.Name = u.Username
|
|
}
|
|
|
|
// Update the user in the DB.
|
|
user, err := app.core.UpdateUser(id, u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Clear the pasword before sending outside.
|
|
user.Password = null.String{}
|
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
|
if err := cacheAPIUsers(app.core, app.auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{user})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Cache the API token for validating API queries without hitting the DB every time.
|
|
if err := cacheAPIUsers(app.core, app.auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
}
|
|
|
|
// 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})
|
|
}
|
|
|
|
// 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.
|
|
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}
|
|
}
|
|
|
|
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"))
|
|
}
|
|
}
|
|
|
|
out, err := app.core.UpdateUserProfile(user.ID, u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out.Password = null.String{}
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
}
|
|
|
|
func cacheAPIUsers(co *core.Core, a *auth.Auth) error {
|
|
allUsers, err := co.GetUsers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
apiUsers := make([]models.User, 0, len(allUsers))
|
|
for _, u := range allUsers {
|
|
if u.Type == models.UserTypeAPI && u.Status == models.UserStatusEnabled {
|
|
apiUsers = append(apiUsers, u)
|
|
}
|
|
}
|
|
|
|
a.CacheAPIUsers(apiUsers)
|
|
return nil
|
|
}
|