Files
zuevav da12a9ec1c feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- 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>
2026-09-15 19:41:12 +03:00

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()
}