From 0968e5876634b75757b8d8cc31fcc4365125daaf Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Wed, 10 Apr 2024 00:20:48 +0530 Subject: [PATCH] Add user/password login handler. --- cmd/users.go | 27 +++++++++++++++++++++++++++ i18n/en.json | 3 ++- internal/core/users.go | 17 +++++++++++++++++ internal/migrations/v3.1.0.go | 2 ++ models/queries.go | 1 + queries.sql | 10 ++++++++-- schema.sql | 2 ++ 7 files changed, 59 insertions(+), 3 deletions(-) diff --git a/cmd/users.go b/cmd/users.go index 727a6f84..7c321658 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -156,3 +156,30 @@ func handleDeleteUsers(c echo.Context) error { return c.JSON(http.StatusOK, okResp{true}) } + +// handleLoginUser logs a user in with a username and password. +func handleLoginUser(c echo.Context) error { + var ( + app = c.Get("app").(*App) + ) + + u := struct { + Username string `json:"username"` + Password string `json:"password"` + }{} + + if !strHasLen(u.Username, 1, stdInputMaxLen) { + return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username")) + } + + if !strHasLen(u.Password, 8, stdInputMaxLen) { + return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password")) + } + + _, err := app.core.LoginUser(u.Username, u.Password) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, okResp{true}) +} diff --git a/i18n/en.json b/i18n/en.json index 45e3f153..2920121d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -605,8 +605,9 @@ "users.username": "Username", "users.usernameHelp": "Used with password login", "users.password": "Password", + "users.invalidLogin": "Invalid username or password", "users.passwordRepeat": "Repeat password", - "users.passwordEnable": "Enable password login", + "users.passwordEnable": "Enable logging in with password", "users.passwordMismatch": "Passwords don't match", "users.cantDelete": "User(s) couldn't be deleted. There has to be at least one 'super' user." } diff --git a/internal/core/users.go b/internal/core/users.go index de19c3ac..16e1c5b5 100644 --- a/internal/core/users.go +++ b/internal/core/users.go @@ -1,6 +1,7 @@ package core import ( + "database/sql" "net/http" "github.com/knadh/listmonk/models" @@ -86,3 +87,19 @@ func (c *Core) DeleteUsers(ids []int) error { return nil } + +// LoginUser attempts to log the given user_id in by matching the password. +func (c *Core) LoginUser(username, password string) (models.User, error) { + var out models.User + if err := c.q.LoginUser.Get(&out, username, password); err != nil { + if err == sql.ErrNoRows { + return out, echo.NewHTTPError(http.StatusForbidden, + c.i18n.T("users.invalidLogin")) + } + + return out, echo.NewHTTPError(http.StatusInternalServerError, + c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err))) + } + + return out, nil +} diff --git a/internal/migrations/v3.1.0.go b/internal/migrations/v3.1.0.go index 2adac990..8b893b0f 100644 --- a/internal/migrations/v3.1.0.go +++ b/internal/migrations/v3.1.0.go @@ -12,6 +12,8 @@ import ( func V3_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error { if _, err := db.Exec(` DO $$ + CREATE EXTENSION IF NOT EXISTS pgcrypto; + BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN CREATE TYPE user_status AS ENUM ('enabled', 'disabled', 'super'); diff --git a/models/queries.go b/models/queries.go index 19714969..5b4a900f 100644 --- a/models/queries.go +++ b/models/queries.go @@ -112,6 +112,7 @@ type Queries struct { UpdateUser *sqlx.Stmt `query:"update-user"` DeleteUsers *sqlx.Stmt `query:"delete-users"` GetUsers *sqlx.Stmt `query:"get-users"` + LoginUser *sqlx.Stmt `query:"login-user"` } // CompileSubscriberQueryTpl takes an arbitrary WHERE expressions diff --git a/queries.sql b/queries.sql index 8fcc8e5b..cda43076 100644 --- a/queries.sql +++ b/queries.sql @@ -1028,13 +1028,13 @@ SELECT JSON_BUILD_OBJECT('version', (SELECT VERSION()), 'size_mb', (SELECT ROUND(pg_database_size((SELECT CURRENT_DATABASE()))/(1024^2)))) AS info; -- name: create-user -INSERT INTO users (username, password_login, password, email, name, status) VALUES($1, $2, $3, $4, $5, $6) RETURNING *; +INSERT INTO users (username, password_login, password, email, name, status) VALUES($1, $2, (CASE WHEN $2 AND $3 != '' THEN CRYPT($3, GEN_SALT('bf')) ELSE NULL END), $4, $5, $6) RETURNING *; -- name: update-user UPDATE users SET username=(CASE WHEN $2 != '' THEN $2 ELSE username END), password_login=$3, - password=(CASE WHEN $3 = TRUE THEN (CASE WHEN $4 != '' THEN $4 ELSE password END) ELSE NULL END), + password=(CASE WHEN $3 = TRUE THEN (CASE WHEN $4 != '' THEN CRYPT($4, GEN_SALT('bf')) ELSE password END) ELSE NULL END), email=(CASE WHEN $5 != '' THEN $5 ELSE email END), name=(CASE WHEN $6 != '' THEN $6 ELSE name END), status=(CASE WHEN $7 != '' THEN $7::user_status ELSE status END) @@ -1048,3 +1048,9 @@ DELETE FROM users WHERE id = ALL($1) AND (SELECT num FROM u) > 0; -- name: get-users SELECT * FROM users WHERE $1=0 OR id=$1 ORDER BY created_at; + +-- name: login-user +WITH u AS ( + SELECT * FROM users WHERE username=$1 AND status = 'enabled' AND password_login = TRUE +) +SELECT * FROM u WHERE CRYPT($2, password) = password; diff --git a/schema.sql b/schema.sql index f6727007..af29a0d8 100644 --- a/schema.sql +++ b/schema.sql @@ -9,6 +9,8 @@ DROP TYPE IF EXISTS bounce_type CASCADE; CREATE TYPE bounce_type AS ENUM ('soft' DROP TYPE IF EXISTS template_type CASCADE; CREATE TYPE template_type AS ENUM ('campaign', 'tx'); DROP TYPE IF EXISTS user_status CASCADE; CREATE TYPE user_status AS ENUM ('enabled', 'disabled', 'super'); +CREATE EXTENSION pgcrypto; + -- subscribers DROP TABLE IF EXISTS subscribers CASCADE; CREATE TABLE subscribers (