generated from VLADIMIR/template
add docs
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"evening_detective_server/internal/modules/cleaner"
|
||||
"evening_detective_server/internal/modules/storytelling"
|
||||
"evening_detective_server/internal/repos"
|
||||
"evening_detective_server/internal/repos/scenarios_repo"
|
||||
"strings"
|
||||
)
|
||||
@@ -88,30 +89,111 @@ func (s *ScenarioService) UpdateScenarioByID(
|
||||
name string,
|
||||
description string,
|
||||
image string,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.scenariosRepo.UpdateScenarioByID(ctx, id, name, description, image)
|
||||
}
|
||||
|
||||
// ErrScenarioPublished — опубликованный сценарий нельзя снять с публикации
|
||||
// или удалить: при публикации исключительное право отчуждается Оператору
|
||||
// (п. 7.2 Пользовательского соглашения, ст. 1234 ГК РФ), а право на отзыв
|
||||
// действует только до обнародования (ст. 1269 ГК РФ).
|
||||
var ErrScenarioPublished = errors.New("Опубликованный сценарий нельзя снять с публикации или удалить: исключительное право отчуждено Оператору")
|
||||
|
||||
// ErrScenarioNotOwner — сценарий принадлежит другому пользователю.
|
||||
var ErrScenarioNotOwner = errors.New("Сценарий принадлежит другому пользователю")
|
||||
|
||||
// getScenarioForChange проверяет права актора на изменение сценария:
|
||||
// не-админ должен быть автором сценария, сценарий не должен быть удалён.
|
||||
// Возвращает сценарий, чтобы вызывающий код не делал повторное чтение.
|
||||
func (s *ScenarioService) getScenarioForChange(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) (*repos.Scenario, error) {
|
||||
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scenario.IsDeleted {
|
||||
return nil, scenarios_repo.ErrScenarioNotFound
|
||||
}
|
||||
if isAdmin {
|
||||
return scenario, nil
|
||||
}
|
||||
if scenario.Author == nil || scenario.Author.ID != actorId {
|
||||
return nil, ErrScenarioNotOwner
|
||||
}
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
func (s *ScenarioService) UpdateScenarioStatusByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
status string,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
return s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status)
|
||||
scenario, err := s.getScenarioForChange(ctx, id, actorId, isAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isAdmin && scenario.Status == "public" {
|
||||
// Повторная публикация уже опубликованного — идемпотентный успех
|
||||
// (published_at не двигается: CASE WHEN published_at IS NULL).
|
||||
if status == "public" {
|
||||
return nil
|
||||
}
|
||||
return ErrScenarioPublished
|
||||
}
|
||||
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
|
||||
// сможет перевести в draft сценарий, опубликованный между чтением и UPDATE.
|
||||
updated, err := s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status, isAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !updated {
|
||||
return ErrScenarioPublished
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScenarioService) DeleteScenarioByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
return s.scenariosRepo.DeleteScenarioByID(ctx, id)
|
||||
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
|
||||
// сможет удалить сценарий, опубликованный между чтением и UPDATE.
|
||||
updated, err := s.scenariosRepo.DeleteScenarioByID(ctx, id, isAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !updated {
|
||||
return ErrScenarioPublished
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScenarioService) AddScenarioPlace(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
place *storytelling.Place,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
story, err := s.getStory(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -128,7 +210,12 @@ func (s *ScenarioService) UpdateScenarioPlace(
|
||||
id int,
|
||||
code string,
|
||||
place *storytelling.Place,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
story, err := s.getStory(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -146,7 +233,12 @@ func (s *ScenarioService) DeleteScenarioPlace(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
code string,
|
||||
actorId int,
|
||||
isAdmin bool,
|
||||
) error {
|
||||
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
story, err := s.getStory(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/internal/repos"
|
||||
"evening_detective_server/internal/repos/refresh_tokens_repo"
|
||||
"evening_detective_server/internal/repos/user_agreements_repo"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"fmt"
|
||||
"slices"
|
||||
@@ -17,6 +18,17 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Версии документов, акцепт которых фиксируется. Должны совпадать с
|
||||
// редакциями, опубликованными по постоянным URL (/terms и /privacy,
|
||||
// rpc GetTerms/GetPrivacy, текст встраивается из пакета docs): при изменении
|
||||
// текста документа версия ОБЯЗАНА увеличиваться, иначе повторный акцепт
|
||||
// не будет зафиксирован как согласие с новой редакцией
|
||||
// (UNIQUE (user_id, agreement_type, version)).
|
||||
const (
|
||||
termsVersion = "1.0"
|
||||
privacyVersion = "1.0"
|
||||
)
|
||||
|
||||
type UsersService struct {
|
||||
usersRepo *users_repo.UsersRepo
|
||||
passwordGenerator password_generator.IPasswordGenerator
|
||||
@@ -41,11 +53,22 @@ func NewUsersService(
|
||||
}
|
||||
}
|
||||
|
||||
// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
|
||||
var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")
|
||||
|
||||
func (s *UsersService) AddUser(
|
||||
ctx context.Context,
|
||||
username string,
|
||||
email string,
|
||||
acceptTerms bool,
|
||||
acceptPrivacy bool,
|
||||
ip string,
|
||||
userAgent string,
|
||||
) error {
|
||||
if !acceptTerms || !acceptPrivacy {
|
||||
return ErrTermsNotAccepted
|
||||
}
|
||||
|
||||
password, err := s.passwordGenerator.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,12 +77,28 @@ func (s *UsersService) AddUser(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.usersRepo.AddUser(
|
||||
// Создание пользователя и фиксация акцептов — в одной транзакции:
|
||||
// без записей о согласиях пользователь не существует.
|
||||
_, err = s.usersRepo.AddUserWithAgreements(
|
||||
ctx,
|
||||
username,
|
||||
email,
|
||||
string(hashedPassword),
|
||||
[]string{"user"},
|
||||
[]user_agreements_repo.Agreement{
|
||||
{
|
||||
AgreementType: user_agreements_repo.AgreementTypeTerms,
|
||||
Version: termsVersion,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
},
|
||||
{
|
||||
AgreementType: user_agreements_repo.AgreementTypePrivacy,
|
||||
Version: privacyVersion,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -74,6 +113,24 @@ func (s *UsersService) AddUser(
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteAccount удаляет учётную запись и все персональные данные пользователя
|
||||
// (ст. 21 152-ФЗ). Требует подтверждения паролем: обладатель украденного
|
||||
// access-токена не должен мочь удалить чужой аккаунт.
|
||||
func (s *UsersService) DeleteAccount(
|
||||
ctx context.Context,
|
||||
userId int,
|
||||
password string,
|
||||
) error {
|
||||
user, err := s.usersRepo.GetUserByID(ctx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
return errors.New("Неверный пароль")
|
||||
}
|
||||
return s.usersRepo.DeleteUser(ctx, userId)
|
||||
}
|
||||
|
||||
func (s *UsersService) RefreshPassword(
|
||||
ctx context.Context,
|
||||
email string,
|
||||
|
||||
Reference in New Issue
Block a user