Кабинет: веб-установщик и управление пользователями в ядре
- установщик в самом кабинете: до настройки любая страница ведёт в /install; три шага — проверка сервера (каталог данных, https), связь с ядром (режим, адрес, ключ; связь проверяется до сохранения, в режиме ожидания страница ждёт подключения ядра), администратор кабинета (заводится в ядре через /api/lk/admin/bootstrap, только пока администраторов нет); настройки в data/lk.json 0600, окружение перекрывает файл; после установки установщик закрыт - настройки кабинета можно менять на ходу: Server за мьютексом, один mux на всё время - ключ модуля больше не кладётся в архив: его вводят в установщике; на странице настроек ключ показан целиком 10 минут после создания, потом маска - ядро: карточка «Пользователи кабинета» в настройках — список, заведение, смена пароля (гасит сессии), блокировка, права сотрудника, разрешение поручений, удаление; письма не отправляются - инструкция /admin/help/lk дополнена шагами установщика и разделом о подходящем хостинге - тесты: TestInstaller, TestNotConfigured, TestSetupPageLKUsersAndLink; установщик проверен вживую вторым экземпляром против боевого ядра Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package lkgateway
|
||||
|
||||
// lkusers_admin.go — учётные записи кабинета клиента: управление из настроек ядра и
|
||||
// первый администратор из веб-установщика кабинета.
|
||||
//
|
||||
// Пользователи кабинета живут только здесь, в lk.users: кабинет на хостинге своей
|
||||
// базы не имеет. Здесь заводят клиентов и сотрудников, меняют пароли, блокируют.
|
||||
// Письма никому не отправляются: почта хранится, но рассылка выключена — данные
|
||||
// пока тестовые, и случайное письмо реальному человеку недопустимо.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// globalLKSessions — сессии кабинета: нужны, чтобы гасить их при смене пароля и блокировке.
|
||||
var globalLKSessions *lkSessions
|
||||
|
||||
// lkHasAdmin — есть ли хотя бы один администратор кабинета.
|
||||
func lkHasAdmin(ctx context.Context, rc *RuntimeConfig) (bool, error) {
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
var n int
|
||||
if err := db.QueryRowContext(cctx, `SELECT count(*) FROM lk.users WHERE is_admin AND active`).Scan(&n); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// lkUserSetFlags — включить/выключить учётную запись, права сотрудника, подачу поручений.
|
||||
func lkUserSetFlags(ctx context.Context, rc *RuntimeConfig, id int64, column string, value bool) error {
|
||||
switch column {
|
||||
case "active", "is_admin", "can_transfer":
|
||||
default:
|
||||
return errors.New("неизвестный признак")
|
||||
}
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
res, err := db.ExecContext(cctx, `UPDATE lk.users SET `+column+`=$1 WHERE id=$2`, value, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return errors.New("учётная запись не найдена")
|
||||
}
|
||||
if globalLKSessions != nil {
|
||||
globalLKSessions.forget(id)
|
||||
if !value && column == "active" {
|
||||
globalLKSessions.dropUser(id) // заблокированного выбрасываем сразу
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lkUserUpdate — контакты и договор.
|
||||
func lkUserUpdate(ctx context.Context, rc *RuntimeConfig, id int64, contract, fullName, email, phone string) error {
|
||||
if strings.TrimSpace(contract) == "" {
|
||||
return errors.New("номер договора обязателен")
|
||||
}
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
_, err = db.ExecContext(cctx, `UPDATE lk.users SET contract_number=$1, full_name=$2, email=$3, phone=$4 WHERE id=$5`,
|
||||
strings.TrimSpace(contract), strings.TrimSpace(fullName), strings.TrimSpace(email), strings.TrimSpace(phone), id)
|
||||
if err == nil && globalLKSessions != nil {
|
||||
globalLKSessions.forget(id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// lkUserDelete — удалить учётную запись. Поручения и журнал остаются: они привязаны к договору.
|
||||
func lkUserDelete(ctx context.Context, rc *RuntimeConfig, id int64) error {
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
if _, err := db.ExecContext(cctx, `DELETE FROM lk.users WHERE id=$1`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if globalLKSessions != nil {
|
||||
globalLKSessions.dropUser(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lkUserByLogin — учётная запись по логину (для смены пароля из настроек).
|
||||
func lkUserIDByLogin(ctx context.Context, rc *RuntimeConfig, login string) (int64, error) {
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
var id int64
|
||||
if err := db.QueryRowContext(cctx, `SELECT id FROM lk.users WHERE login=$1`, login).Scan(&id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, errors.New("учётная запись не найдена")
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// lkUsersSave — POST /admin/setup/lk/users: заведение, пароль, признаки, удаление.
|
||||
func (h *setupHandlers) lkUsersSave(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(r.FormValue("id"), 10, 64)
|
||||
var err error
|
||||
msg := "Пользователи кабинета: сохранено"
|
||||
switch op := r.FormValue("op"); op {
|
||||
case "create":
|
||||
u := LKUser{
|
||||
Login: strings.TrimSpace(r.FormValue("login")), Contract: strings.TrimSpace(r.FormValue("contract")),
|
||||
FullName: strings.TrimSpace(r.FormValue("full_name")), Email: strings.TrimSpace(r.FormValue("email")),
|
||||
Phone: strings.TrimSpace(r.FormValue("phone")), Active: true,
|
||||
IsAdmin: r.FormValue("is_admin") == "1", CanTransfer: r.FormValue("can_transfer") == "1",
|
||||
}
|
||||
if u.IsAdmin && u.Contract == "" {
|
||||
u.Contract = "ADMIN" // у сотрудника своего договора нет
|
||||
}
|
||||
err = lkCreateUser(ctx, h.rc, u, r.FormValue("password"))
|
||||
msg = fmt.Sprintf("Учётная запись «%s» создана. Пароль сообщите пользователю лично: письма кабинет не отправляет.", u.Login)
|
||||
case "password":
|
||||
login := strings.TrimSpace(r.FormValue("login"))
|
||||
if err = lkSetPassword(ctx, h.rc, login, r.FormValue("password")); err == nil {
|
||||
if uid, e := lkUserIDByLogin(ctx, h.rc, login); e == nil && globalLKSessions != nil {
|
||||
globalLKSessions.dropUser(uid) // старые сессии после смены пароля не живут
|
||||
}
|
||||
msg = fmt.Sprintf("Пароль «%s» изменён, его сессии завершены.", login)
|
||||
}
|
||||
case "update":
|
||||
err = lkUserUpdate(ctx, h.rc, id, r.FormValue("contract"), r.FormValue("full_name"), r.FormValue("email"), r.FormValue("phone"))
|
||||
case "block", "unblock":
|
||||
err = lkUserSetFlags(ctx, h.rc, id, "active", op == "unblock")
|
||||
msg = map[bool]string{true: "Учётная запись разблокирована", false: "Учётная запись заблокирована, сессии завершены"}[op == "unblock"]
|
||||
case "admin_on", "admin_off":
|
||||
err = lkUserSetFlags(ctx, h.rc, id, "is_admin", op == "admin_on")
|
||||
case "transfer_on", "transfer_off":
|
||||
err = lkUserSetFlags(ctx, h.rc, id, "can_transfer", op == "transfer_on")
|
||||
case "delete":
|
||||
err = lkUserDelete(ctx, h.rc, id)
|
||||
msg = "Учётная запись удалена"
|
||||
default:
|
||||
err = errors.New("неизвестное действие")
|
||||
}
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Пользователи кабинета: "+err.Error())
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, msg)
|
||||
}
|
||||
|
||||
// lkUsersForSetup — список для страницы настроек; ошибка базы — строкой, чтобы страница открылась.
|
||||
func lkUsersForSetup(rc *RuntimeConfig) ([]LKUser, string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
list, err := lkListUsers(ctx, rc)
|
||||
if err != nil {
|
||||
return nil, err.Error()
|
||||
}
|
||||
return list, ""
|
||||
}
|
||||
Reference in New Issue
Block a user