da12a9ec1c
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.8 KiB
Go
149 lines
5.8 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Пользователи Личного кабинета клиента. Хранятся в PostgreSQL bj (lk.users), пароль —
|
|
// только bcrypt-хеш. Клиент входит по логину/паролю, его портфель и заявки определяются
|
|
// номером договора (contract_number).
|
|
|
|
// LKUser — учётная запись клиента в Личном кабинете.
|
|
type LKUser struct {
|
|
ID int64 `json:"id"`
|
|
Login string `json:"login"`
|
|
Contract string `json:"contract_number"`
|
|
FullName string `json:"full_name"`
|
|
LastName string `json:"last_name"`
|
|
FirstName string `json:"first_name"`
|
|
MiddleName string `json:"middle_name"`
|
|
DocType string `json:"doc_type"`
|
|
DocSeries string `json:"doc_series"`
|
|
DocNumber string `json:"doc_number"`
|
|
Email string `json:"email"`
|
|
Phone string `json:"phone"`
|
|
Active bool `json:"active"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
// CanTransfer — клиенту разрешена подача заявок на перевод. Для реальных
|
|
// депозитарных клиентов false (только просмотр); включаем точечно тестовым.
|
|
CanTransfer bool `json:"can_transfer"`
|
|
// ImpersonatedBy — логин администратора, если он вошёл от имени этого клиента.
|
|
ImpersonatedBy string `json:"impersonated_by,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastLoginAt time.Time `json:"last_login_at,omitempty"`
|
|
}
|
|
|
|
var errLKAuth = errors.New("неверный логин или пароль")
|
|
|
|
// lkCreateUser заводит пользователя ЛК (пароль хешируется bcrypt).
|
|
func lkCreateUser(ctx context.Context, rc *RuntimeConfig, u LKUser, password string) error {
|
|
if strings.TrimSpace(u.Login) == "" || strings.TrimSpace(u.Contract) == "" {
|
|
return errors.New("логин и номер договора обязательны")
|
|
}
|
|
if len(password) < 6 {
|
|
return errors.New("пароль минимум 6 символов")
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
_, err = db.ExecContext(cctx, `INSERT INTO lk.users (login, password_hash, contract_number, full_name, email, phone, active,
|
|
last_name, first_name, middle_name, doc_type, doc_series, doc_number, is_admin, can_transfer)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)`,
|
|
u.Login, string(hash), u.Contract, u.FullName, u.Email, u.Phone, u.Active,
|
|
u.LastName, u.FirstName, u.MiddleName, u.DocType, u.DocSeries, u.DocNumber, u.IsAdmin, u.CanTransfer)
|
|
return err
|
|
}
|
|
|
|
// lkSetPassword меняет пароль пользователя ЛК.
|
|
func lkSetPassword(ctx context.Context, rc *RuntimeConfig, login, password string) error {
|
|
if len(password) < 6 {
|
|
return errors.New("пароль минимум 6 символов")
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
res, err := db.ExecContext(cctx, "UPDATE lk.users SET password_hash=$1 WHERE login=$2", string(hash), login)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return errors.New("пользователь не найден")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// lkAuth проверяет логин/пароль клиента. Возвращает пользователя при успехе.
|
|
func lkAuth(ctx context.Context, rc *RuntimeConfig, login, password string) (*LKUser, error) {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
var u LKUser
|
|
var hash string
|
|
if err := db.QueryRowContext(cctx, `SELECT id, login, password_hash, contract_number,
|
|
COALESCE(full_name,''), COALESCE(email,''), COALESCE(phone,''), active,
|
|
COALESCE(last_name,''), COALESCE(first_name,''), COALESCE(middle_name,''),
|
|
COALESCE(doc_type,''), COALESCE(doc_series,''), COALESCE(doc_number,''), is_admin, can_transfer
|
|
FROM lk.users WHERE login=$1`, login).Scan(&u.ID, &u.Login, &hash, &u.Contract,
|
|
&u.FullName, &u.Email, &u.Phone, &u.Active,
|
|
&u.LastName, &u.FirstName, &u.MiddleName,
|
|
&u.DocType, &u.DocSeries, &u.DocNumber, &u.IsAdmin, &u.CanTransfer); err != nil {
|
|
return nil, errLKAuth // не раскрываем, что логина нет
|
|
}
|
|
if !u.Active {
|
|
return nil, errors.New("учётная запись заблокирована")
|
|
}
|
|
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
|
|
return nil, errLKAuth
|
|
}
|
|
_, _ = db.ExecContext(cctx, "UPDATE lk.users SET last_login_at=now() WHERE id=$1", u.ID) // отметка входа, не критично
|
|
return &u, nil
|
|
}
|
|
|
|
// lkListUsers — список пользователей ЛК (для админки).
|
|
func lkListUsers(ctx context.Context, rc *RuntimeConfig) ([]LKUser, error) {
|
|
db, err := lkDB(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
rows, err := db.QueryContext(cctx, `SELECT id, login, contract_number,
|
|
COALESCE(full_name,''), COALESCE(email,''), COALESCE(phone,''), active, is_admin, can_transfer
|
|
FROM lk.users ORDER BY login`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []LKUser
|
|
for rows.Next() {
|
|
var u LKUser
|
|
if err := rows.Scan(&u.ID, &u.Login, &u.Contract, &u.FullName, &u.Email, &u.Phone, &u.Active, &u.IsAdmin, &u.CanTransfer); err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, u)
|
|
}
|
|
return out, rows.Err()
|
|
}
|