diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index eae491b..0c3435d 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -111,7 +111,7 @@ func main() { processorJWT := processor_jwt.NewProcessor(os.Getenv("JWT_SECRET")) refreshTokensRepo := refresh_tokens_repo.NewRefreshTokensRepo(dbpool) usersService := users_service.NewUsersService(usersRepo, passwordGenerator, emailSender, processorJWT, refreshTokensRepo, os.Getenv("APP_BASE_URL")) - uiService := ui_service.NewUiService() + uiService := ui_service.NewUIService() fileStorage, err := file_storage.NewRustFSStorage( context.Background(), os.Getenv("S3_HOST"), diff --git a/internal/app/server.go b/internal/app/server.go index fa0527b..6b3d278 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -21,7 +21,7 @@ type server struct { proto.UnsafeEveningDetectiveServerServer usersService *users_service.UsersService - uiService *ui_service.UiService + uiService *ui_service.UIService fileService *file_service.FileService scenarioService *scenarios_service.ScenarioService gameService *game_service.GameService @@ -35,9 +35,10 @@ type server struct { mapService *map_service.MapService } +// NewServer creates the application server. func NewServer( usersService *users_service.UsersService, - uiService *ui_service.UiService, + uiService *ui_service.UIService, fileService *file_service.FileService, scenarioService *scenarios_service.ScenarioService, gameService *game_service.GameService, diff --git a/internal/modules/cleaner/interface.go b/internal/modules/cleaner/interface.go index 56806a9..c01ddaa 100644 --- a/internal/modules/cleaner/interface.go +++ b/internal/modules/cleaner/interface.go @@ -1,6 +1,7 @@ // Package cleaner normalizes user-provided text and codes. package cleaner +// ICleaner cleans user-provided text and codes. type ICleaner interface { ClearCode(code string) string ClearText(text string) string diff --git a/internal/modules/cleaner/service.go b/internal/modules/cleaner/service.go index 3c670ff..ee64dd6 100644 --- a/internal/modules/cleaner/service.go +++ b/internal/modules/cleaner/service.go @@ -26,6 +26,7 @@ var ( type service struct{} +// NewCleaner creates a text cleaner. func NewCleaner() ICleaner { return &service{} } diff --git a/internal/repos/scenario.go b/internal/repos/scenario.go index 8025e15..7211ed0 100644 --- a/internal/repos/scenario.go +++ b/internal/repos/scenario.go @@ -2,6 +2,7 @@ package repos import "time" +// Scenario describes a stored scenario. type Scenario struct { ID int Name string diff --git a/internal/repos/scenarios_repo/repo.go b/internal/repos/scenarios_repo/repo.go index 7d4b6ae..c683035 100644 --- a/internal/repos/scenarios_repo/repo.go +++ b/internal/repos/scenarios_repo/repo.go @@ -11,19 +11,23 @@ import ( ) var ( + // ErrScenarioNotFound indicates that a scenario was not found. ErrScenarioNotFound = errors.New("Сценарий не найден") ) +// ScenariosRepo persists scenarios. type ScenariosRepo struct { pool *pgxpool.Pool } +// NewScenariosRepo creates a scenarios repository. func NewScenariosRepo(pool *pgxpool.Pool) *ScenariosRepo { return &ScenariosRepo{ pool: pool, } } +// AddScenario stores a scenario. func (r *ScenariosRepo) AddScenario( ctx context.Context, name string, @@ -45,6 +49,7 @@ func (r *ScenariosRepo) AddScenario( return id, nil } +// GetScenariosByAuthorID returns scenarios created by an author. func (r *ScenariosRepo) GetScenariosByAuthorID( ctx context.Context, authorID int, @@ -107,6 +112,7 @@ func (r *ScenariosRepo) GetScenariosByAuthorID( return scenarios, nil } +// GetScenariosByStatus returns scenarios with the requested status. func (r *ScenariosRepo) GetScenariosByStatus( ctx context.Context, status string, @@ -169,6 +175,7 @@ func (r *ScenariosRepo) GetScenariosByStatus( return scenarios, nil } +// GetScenarioByID returns a scenario by ID. func (r *ScenariosRepo) GetScenarioByID( ctx context.Context, id int, @@ -313,6 +320,7 @@ func (r *ScenariosRepo) DeleteScenarioByID( return tag.RowsAffected() > 0, nil } +// GetStoryByScenarioID returns the story JSON for a scenario. func (r *ScenariosRepo) GetStoryByScenarioID( ctx context.Context, id int, diff --git a/internal/repos/teams_repo/participation.go b/internal/repos/teams_repo/participation.go index d8ebe4f..b1bd843 100644 --- a/internal/repos/teams_repo/participation.go +++ b/internal/repos/teams_repo/participation.go @@ -109,8 +109,6 @@ func (r *TeamsRepo) AnswerParticipation(ctx context.Context, token, answer strin return p, ErrParticipationClosed } -// Пятиминутная аренда переживает перезапуски и не позволяет рабочим процессам -// отправить одну задачу повторно. // ClaimParticipationEmail claims the next participation email job. func (r *TeamsRepo) ClaimParticipationEmail(ctx context.Context) (*Participation, error) { p := &Participation{} diff --git a/internal/repos/tickets_repo/repo.go b/internal/repos/tickets_repo/repo.go index 0cbf901..a1b28f9 100644 --- a/internal/repos/tickets_repo/repo.go +++ b/internal/repos/tickets_repo/repo.go @@ -10,11 +10,15 @@ import ( ) var ( + // ErrTicketNotFound indicates that a ticket was not found. ErrTicketNotFound = errors.New("Билет не найден") - ErrTicketUsed = errors.New("Билет уже использован") - ErrGameFinished = errors.New("Игра уже завершена") + // ErrTicketUsed indicates that a ticket was already used. + ErrTicketUsed = errors.New("Билет уже использован") + // ErrGameFinished indicates that the game has finished. + ErrGameFinished = errors.New("Игра уже завершена") ) +// Info describes a support ticket. type Info struct { ID int Status string @@ -26,14 +30,17 @@ type Info struct { UsedBy *string } +// Repo persists support tickets. type Repo struct { pool *pgxpool.Pool } +// New creates a tickets repository. func New(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} } +// CreateBatch creates tickets for a game. func (r *Repo) CreateBatch(ctx context.Context, gameID int, tokenHashes [][]byte) ([]int, error) { tx, err := r.pool.Begin(ctx) if err != nil { @@ -58,6 +65,7 @@ func (r *Repo) CreateBatch(ctx context.Context, gameID int, tokenHashes [][]byte return ids, nil } +// GetByHash returns a ticket by token hash. func (r *Repo) GetByHash(ctx context.Context, tokenHash []byte) (*Info, error) { info := &Info{} err := r.pool.QueryRow(ctx, ` @@ -74,6 +82,7 @@ func (r *Repo) GetByHash(ctx context.Context, tokenHash []byte) (*Info, error) { return info, err } +// Accept assigns a ticket to a user. func (r *Repo) Accept(ctx context.Context, tokenHash []byte, userID int) (*Info, error) { var status, gameStatus string if err := r.pool.QueryRow(ctx, ` diff --git a/internal/services/game_review_service/mapper.go b/internal/services/game_review_service/mapper.go index 766e36b..10d0df1 100644 --- a/internal/services/game_review_service/mapper.go +++ b/internal/services/game_review_service/mapper.go @@ -7,6 +7,7 @@ import ( "evening_detective_server/internal/repos/game_reviews_repo" ) +// Review describes a game review exposed by the service layer. type Review struct { ID int GameID int diff --git a/internal/services/game_review_service/service.go b/internal/services/game_review_service/service.go index 1e59cf9..73545a0 100644 --- a/internal/services/game_review_service/service.go +++ b/internal/services/game_review_service/service.go @@ -18,15 +18,23 @@ import ( ) var ( - ErrTeamNotFound = errors.New("Команда не найдена") - ErrGameNotFound = errors.New("Игра не найдена") - ErrReviewNotFound = errors.New("Отзыв не найден") - ErrReviewExists = errors.New("Отзыв от команды уже оставлен") - ErrInvalidStatus = errors.New("Недопустимый статус модерации") + // ErrTeamNotFound indicates that a team was not found. + ErrTeamNotFound = errors.New("Команда не найдена") + // ErrGameNotFound indicates that a game was not found. + ErrGameNotFound = errors.New("Игра не найдена") + // ErrReviewNotFound indicates that a review was not found. + ErrReviewNotFound = errors.New("Отзыв не найден") + // ErrReviewExists indicates that a team already submitted a review. + ErrReviewExists = errors.New("Отзыв от команды уже оставлен") + // ErrInvalidStatus indicates that a moderation status is invalid. + ErrInvalidStatus = errors.New("Недопустимый статус модерации") + // ErrGameNotFinished indicates that the game is not finished. ErrGameNotFinished = errors.New("Оценить можно только завершённую игру") - ErrInvalidReview = errors.New("Оценка должна быть от 1 до 5, текст обязателен") + // ErrInvalidReview indicates that review data is invalid. + ErrInvalidReview = errors.New("Оценка должна быть от 1 до 5, текст обязателен") ) +// GameReviewService handles game reviews. type GameReviewService struct { reviews *game_reviews_repo.Repo games *games_repo.GamesRepo @@ -34,11 +42,13 @@ type GameReviewService struct { appBaseURL string } +// NewGameReviewService creates a game review service. // NewGameReviewService creates a game review service. func NewGameReviewService(reviews *game_reviews_repo.Repo, games *games_repo.GamesRepo, teams *teams_repo.TeamsRepo, appBaseURL string) *GameReviewService { return &GameReviewService{reviews: reviews, games: games, teams: teams, appBaseURL: appBaseURL} } +// Create stores a game review. func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, password string, rating int32, text string) (*Review, error) { if rating < 1 || rating > 5 || strings.TrimSpace(text) == "" { return nil, ErrInvalidReview @@ -73,6 +83,7 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass return mapReview(review), nil } +// List returns reviews for a game. func (s *GameReviewService) List(ctx context.Context, gameID int, all bool) ([]*Review, float64, int32, error) { if _, err := s.games.GetGameByID(ctx, gameID); err != nil { if errors.Is(err, games_repo.ErrGameNotFound) { @@ -84,11 +95,13 @@ func (s *GameReviewService) List(ctx context.Context, gameID int, all bool) ([]* return mapReviews(reviews), average, count, err } +// ListForModeration returns reviews awaiting moderation. func (s *GameReviewService) ListForModeration(ctx context.Context) ([]*Review, error) { reviews, err := s.reviews.ListForModeration(ctx) return mapReviews(reviews), err } +// Moderate updates a review moderation status. func (s *GameReviewService) Moderate(ctx context.Context, id int, status string) (*Review, error) { review, err := s.reviews.Moderate(ctx, id, status) if err != nil { @@ -103,10 +116,12 @@ func (s *GameReviewService) Moderate(ctx context.Context, id int, status string) return mapReview(review), nil } +// ScenarioRating returns aggregate ratings for a scenario. func (s *GameReviewService) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, error) { return s.reviews.ScenarioRating(ctx, scenarioID) } +// ListByScenario returns reviews for a scenario. func (s *GameReviewService) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, float64, int32, error) { reviews, average, count, err := s.reviews.ListByScenario(ctx, scenarioID) return mapReviews(reviews), average, count, err @@ -121,6 +136,7 @@ func reviewReminderEmail(job *teams_repo.ReviewReminder, baseURL string) email_s return email_sender.Message{To: job.Email, Subject: "Поделитесь впечатлениями об игре", Body: body, HTML: email_sender.WrapHTML(content, strings.TrimRight(baseURL, "/"))} } +// RunReviewReminderEmails sends pending review reminder emails. func (s *GameReviewService) RunReviewReminderEmails(ctx context.Context, sender email_sender.IEmailSender) { base, err := url.Parse(s.appBaseURL) if err != nil || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.User != nil || base.RawQuery != "" || base.Fragment != "" { diff --git a/internal/services/ticket_service/service.go b/internal/services/ticket_service/service.go index 928e609..5bff83c 100644 --- a/internal/services/ticket_service/service.go +++ b/internal/services/ticket_service/service.go @@ -18,18 +18,25 @@ import ( const maxTicketsPerRequest = 100 var ( + // ErrInvalidTicketCount indicates that the requested ticket count is invalid. ErrInvalidTicketCount = errors.New("количество билетов должно быть от 1 до 100") - ErrInvalidBaseURL = errors.New("необходимо настроить APP_BASE_URL — публичный адрес приложения") - ErrTicketNotFound = errors.New("Билет не найден") - ErrTicketUsed = errors.New("Билет уже использован") - ErrGameFinished = errors.New("Игра уже завершена") + // ErrInvalidBaseURL indicates that the public application URL is missing. + ErrInvalidBaseURL = errors.New("необходимо настроить APP_BASE_URL — публичный адрес приложения") + // ErrTicketNotFound indicates that a ticket was not found. + ErrTicketNotFound = errors.New("Билет не найден") + // ErrTicketUsed indicates that a ticket was already used. + ErrTicketUsed = errors.New("Билет уже использован") + // ErrGameFinished indicates that the game has finished. + ErrGameFinished = errors.New("Игра уже завершена") ) +// Ticket describes an issued support ticket. type Ticket struct { ID int URL string } +// Page contains tickets returned for a user. type Page struct { ID int Status string @@ -41,15 +48,18 @@ type Page struct { UsedBy *string } +// TicketService handles support tickets. type TicketService struct { repo *tickets_repo.Repo appBaseURL string } +// NewTicketService creates a ticket service. func NewTicketService(repo *tickets_repo.Repo, appBaseURL string) *TicketService { return &TicketService{repo: repo, appBaseURL: appBaseURL} } +// Issue creates tickets for a game. func (s *TicketService) Issue(ctx context.Context, gameID, count int) ([]*Ticket, error) { if count < 1 || count > maxTicketsPerRequest { return nil, ErrInvalidTicketCount @@ -87,6 +97,7 @@ func (s *TicketService) Issue(ctx context.Context, gameID, count int) ([]*Ticket return tickets, nil } +// Get returns tickets associated with a token. func (s *TicketService) Get(ctx context.Context, token string) (*Page, error) { hash, err := tokenHash(token) if err != nil { @@ -108,6 +119,7 @@ func (s *TicketService) Get(ctx context.Context, token string) (*Page, error) { }, nil } +// Accept assigns a ticket to a user. func (s *TicketService) Accept(ctx context.Context, token string, userID int) (*Page, error) { hash, err := tokenHash(token) if err != nil { diff --git a/internal/services/ui_service/service.go b/internal/services/ui_service/service.go index 406311b..9bafee9 100644 --- a/internal/services/ui_service/service.go +++ b/internal/services/ui_service/service.go @@ -28,13 +28,16 @@ var ( } ) -type UiService struct{} +// UIService provides user interface permissions. +type UIService struct{} -func NewUiService() *UiService { - return &UiService{} +// NewUIService creates a UI service. +func NewUIService() *UIService { + return &UIService{} } -func (s *UiService) GetPermissions(roles []string) []string { +// GetPermissions returns permissions for the supplied roles. +func (s *UIService) GetPermissions(roles []string) []string { resMap := s.getPermissionsMap(roles) res := make([]string, 0, len(resMap)) for permission := range resMap { @@ -43,7 +46,7 @@ func (s *UiService) GetPermissions(roles []string) []string { return res } -func (s *UiService) getPermissionsMap(roles []string) map[string]any { +func (s *UIService) getPermissionsMap(roles []string) map[string]any { resMap := make(map[string]any, 100) if slices.Contains(roles, rolesModule.Admin) { diff --git a/internal/services/users_service/service.go b/internal/services/users_service/service.go index 6ee3729..29d3027 100644 --- a/internal/services/users_service/service.go +++ b/internal/services/users_service/service.go @@ -53,6 +53,7 @@ type usersRepository interface { DeleteUser(ctx context.Context, userID int) error } +// UsersService handles user accounts and authentication. type UsersService struct { usersRepo usersRepository passwordGenerator password_generator.IPasswordGenerator @@ -64,6 +65,7 @@ type UsersService struct { appBaseURL string } +// NewUsersService creates a users service. func NewUsersService( usersRepo usersRepository, passwordGenerator password_generator.IPasswordGenerator, @@ -84,14 +86,20 @@ func NewUsersService( var ( // ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений. - ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности") - ErrInvalidEmail = errors.New("Некорректный email") - ErrInvalidCredentials = errors.New("Не верен email или пароль") + ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности") + // ErrInvalidEmail indicates that an email address is invalid. + ErrInvalidEmail = errors.New("Некорректный email") + // ErrInvalidCredentials indicates that the supplied credentials are invalid. + ErrInvalidCredentials = errors.New("Не верен email или пароль") + // ErrInvalidAccountPassword indicates that the account password is invalid. ErrInvalidAccountPassword = errors.New("Неверный пароль") - ErrInvalidRefreshToken = errors.New("Пользователь не найден") - ErrInvalidRole = errors.New("Недопустимая роль") + // ErrInvalidRefreshToken indicates that a refresh token is invalid. + ErrInvalidRefreshToken = errors.New("Пользователь не найден") + // ErrInvalidRole indicates that a role is invalid. + ErrInvalidRole = errors.New("Недопустимая роль") ) +// AddUser creates a user account. func (s *UsersService) AddUser( ctx context.Context, username string, @@ -192,6 +200,7 @@ func (s *UsersService) DeleteAccount( return err } +// RefreshPassword generates and sends a new password. func (s *UsersService) RefreshPassword( ctx context.Context, email string, @@ -252,6 +261,7 @@ func validateEmail(email string) error { return nil } +// Login authenticates a user and returns tokens. func (s *UsersService) Login( ctx context.Context, email string, @@ -294,6 +304,7 @@ func (s *UsersService) Login( return accessToken, refreshToken, nil } +// Refresh exchanges a refresh token for new tokens. func (s *UsersService) Refresh( ctx context.Context, token string, @@ -344,6 +355,7 @@ func (s *UsersService) Refresh( return accessToken, refreshToken, nil } +// GetUsers returns users. func (s *UsersService) GetUsers( ctx context.Context, ) ([]*User, error) { @@ -354,6 +366,7 @@ func (s *UsersService) GetUsers( return mapUsers(users), nil } +// GetUserByID returns a user by ID. func (s *UsersService) GetUserByID( ctx context.Context, userID int, @@ -368,6 +381,7 @@ func (s *UsersService) GetUserByID( return mapUser(user), nil } +// AddUserRole assigns a role to a user. func (s *UsersService) AddUserRole( ctx context.Context, userID int, @@ -383,6 +397,7 @@ func (s *UsersService) AddUserRole( return err } +// DeleteUserRole removes a role from a user. func (s *UsersService) DeleteUserRole( ctx context.Context, userID int, @@ -399,13 +414,16 @@ func (s *UsersService) DeleteUserRole( } var ( + // ErrPasswordChangeForbidden indicates that the password cannot be changed. ErrPasswordChangeForbidden = errors.New("Изменять чужой пароль может только администратор") - ErrInvalidPasswordChange = errors.New("Укажите чужой положительный ID и пароль от 8 до 16 символов") + // ErrInvalidPasswordChange indicates that the password change request is invalid. + ErrInvalidPasswordChange = errors.New("Укажите чужой положительный ID и пароль от 8 до 16 символов") // ErrUserNotFound — целевой пользователь не найден. ErrUserNotFound = errors.New("Пользователь не найден") ) // ChangeUserPassword сохраняет только хеш; действующие сеансы не изменяются. +// ChangeUserPassword changes a user's password subject to authorization checks. func (s *UsersService) ChangeUserPassword(ctx context.Context, actorID, userID int, password string) error { actor, err := s.usersRepo.GetUserByID(ctx, actorID) if errors.Is(err, users_repo.ErrUserNotFound) { diff --git a/internal/services/users_service/user.go b/internal/services/users_service/user.go index b4f08a9..43fdaea 100644 --- a/internal/services/users_service/user.go +++ b/internal/services/users_service/user.go @@ -3,6 +3,7 @@ package users_service import "time" // User — пользователь, доступный прикладным сценариям и выдаче наружу. +// User describes a user exposed by the service layer. type User struct { ID int Username string