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