diff --git a/internal/lib/config.go b/config/config.go similarity index 94% rename from internal/lib/config.go rename to config/config.go index bf06cdb..379d3b1 100644 --- a/internal/lib/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package lib +package config type Config struct { Env string `env:"ENV" env-default:"prod"` diff --git a/go.mod b/go.mod index ec94a67..41b084a 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/valkey-io/valkey-go v1.0.47 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect @@ -46,7 +47,7 @@ require ( golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 0c89821..e264c7b 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/valkey-io/valkey-go v1.0.47 h1:fW5+m2BaLAbxB1EWEEWmj+i2n+YcYFBDG/jKs6qu5j8= +github.com/valkey-io/valkey-go v1.0.47/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -168,6 +170,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= diff --git a/internal/contests/delivery.go b/internal/contests/delivery.go new file mode 100644 index 0000000..b1d0091 --- /dev/null +++ b/internal/contests/delivery.go @@ -0,0 +1,4 @@ +package contests + +type ContestHandlers interface { +} diff --git a/internal/contests/delivery/grpc/handlers.go b/internal/contests/delivery/grpc/handlers.go new file mode 100644 index 0000000..21e034e --- /dev/null +++ b/internal/contests/delivery/grpc/handlers.go @@ -0,0 +1 @@ +package grpc diff --git a/internal/contests/pg_repository.go b/internal/contests/pg_repository.go new file mode 100644 index 0000000..4085d03 --- /dev/null +++ b/internal/contests/pg_repository.go @@ -0,0 +1,13 @@ +package contests + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type ContestRepository interface { + CreateContest(ctx context.Context, contest *models.Contest) (int32, error) + ReadContestById(ctx context.Context, id int32) (*models.Contest, error) + UpdateContest(ctx context.Context, contest *models.Contest) error + DeleteContest(ctx context.Context, id int32) error +} diff --git a/internal/storage/participants.go b/internal/contests/repository/participants.go similarity index 98% rename from internal/storage/participants.go rename to internal/contests/repository/participants.go index e703e18..99ab645 100644 --- a/internal/storage/participants.go +++ b/internal/contests/repository/participants.go @@ -1,4 +1,4 @@ -package storage +package repository import ( "context" diff --git a/internal/contests/repository/pg_repository.go b/internal/contests/repository/pg_repository.go new file mode 100644 index 0000000..00e214b --- /dev/null +++ b/internal/contests/repository/pg_repository.go @@ -0,0 +1,97 @@ +package repository + +import ( + "context" + "errors" + "git.sch9.ru/new_gate/models" + "git.sch9.ru/new_gate/ms-tester/pkg/utils" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +type ContestRepository struct { + db *sqlx.DB + logger *zap.Logger +} + +func NewContestRepository(db *sqlx.DB, logger *zap.Logger) *ContestRepository { + return &ContestRepository{ + db: db, + logger: logger, + } +} + +func (r *ContestRepository) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) { + query := r.db.Rebind(` +INSERT INTO contests + (name) +VALUES (?) +RETURNING id +`) + + rows, err := r.db.QueryxContext( + ctx, + query, + contest.Name, + ) + if err != nil { + return 0, handlePgErr(err) + } + + defer rows.Close() + var id int32 + err = rows.StructScan(&id) + if err != nil { + return 0, handlePgErr(err) + } + + return id, nil + +} + +func (r *ContestRepository) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) { + var contest models.Contest + query := r.db.Rebind("SELECT * from contests WHERE id=? LIMIT 1") + err := r.db.GetContext(ctx, &contest, query, id) + if err != nil { + return nil, handlePgErr(err) + } + return &contest, nil +} + +func (r *ContestRepository) UpdateContest(ctx context.Context, contest *models.Contest) error { + query := r.db.Rebind("UPDATE contests SET name=? WHERE id=?") + _, err := r.db.ExecContext(ctx, query, contest.Name, contest.Id) + if err != nil { + return handlePgErr(err) + } + + return nil +} + +func (r *ContestRepository) DeleteContest(ctx context.Context, id int32) error { + query := r.db.Rebind("DELETE FROM contests WHERE id=?") + _, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return handlePgErr(err) + } + + return nil +} + +func handlePgErr(err error) error { + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + return utils.StorageError(err, utils.ErrUnknown, "unexpected error from postgres") + } + if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { + // TODO: probably should specify which constraint + return utils.StorageError(err, utils.ErrConflict, pgErr.Message) + } + if pgerrcode.IsNoData(pgErr.Code) { + return utils.StorageError(err, utils.ErrNotFound, pgErr.Message) + } + return utils.StorageError(err, utils.ErrUnimplemented, "unimplemented error") +} diff --git a/internal/storage/solution.go b/internal/contests/repository/solution.go similarity index 99% rename from internal/storage/solution.go rename to internal/contests/repository/solution.go index 88c1822..cc46339 100644 --- a/internal/storage/solution.go +++ b/internal/contests/repository/solution.go @@ -1,4 +1,4 @@ -package storage +package repository import ( "context" diff --git a/internal/storage/task.go b/internal/contests/repository/task.go similarity index 98% rename from internal/storage/task.go rename to internal/contests/repository/task.go index 0e08fe4..82e2d85 100644 --- a/internal/storage/task.go +++ b/internal/contests/repository/task.go @@ -1,4 +1,4 @@ -package storage +package repository import ( "context" diff --git a/internal/contests/usecase.go b/internal/contests/usecase.go new file mode 100644 index 0000000..e90643b --- /dev/null +++ b/internal/contests/usecase.go @@ -0,0 +1,13 @@ +package contests + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type ContestUseCase interface { + CreateContest(ctx context.Context, contest *models.Contest) (int32, error) + ReadContestById(ctx context.Context, id int32) (*models.Contest, error) + UpdateContest(ctx context.Context, contest *models.Contest) error + DeleteContest(ctx context.Context, id int32) error +} diff --git a/opa/all.rego b/internal/contests/usecase/all.rego similarity index 100% rename from opa/all.rego rename to internal/contests/usecase/all.rego diff --git a/internal/services/participants.go b/internal/contests/usecase/participants.go similarity index 99% rename from internal/services/participants.go rename to internal/contests/usecase/participants.go index 72eaebd..13351ae 100644 --- a/internal/services/participants.go +++ b/internal/contests/usecase/participants.go @@ -1,4 +1,4 @@ -package services +package usecase import ( "context" diff --git a/internal/services/permission.go b/internal/contests/usecase/permission.go similarity index 100% rename from internal/services/permission.go rename to internal/contests/usecase/permission.go diff --git a/internal/services/solution.go b/internal/contests/usecase/solution.go similarity index 99% rename from internal/services/solution.go rename to internal/contests/usecase/solution.go index 6d61013..73335b1 100644 --- a/internal/services/solution.go +++ b/internal/contests/usecase/solution.go @@ -1,4 +1,4 @@ -package services +package usecase import ( "context" diff --git a/internal/services/task.go b/internal/contests/usecase/task.go similarity index 81% rename from internal/services/task.go rename to internal/contests/usecase/task.go index 401a22f..719fb44 100644 --- a/internal/services/task.go +++ b/internal/contests/usecase/task.go @@ -1,9 +1,9 @@ -package services +package usecase import ( "context" "git.sch9.ru/new_gate/models" - "git.sch9.ru/new_gate/ms-tester/internal/lib" + "git.sch9.ru/new_gate/ms-tester/pkg/utils" ) type TaskStorage interface { @@ -28,14 +28,14 @@ func NewTaskService( func (service *TaskService) CreateTask(ctx context.Context, task models.Task) (int32, error) { if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") { - return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") + return 0, utils.ServiceError(nil, utils.ErrNoPermission, "permission denied") } return service.taskStorage.CreateTask(ctx, task) } func (service *TaskService) DeleteTask(ctx context.Context, id int32) error { if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") { - return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") + return utils.ServiceError(nil, utils.ErrNoPermission, "permission denied") } return service.taskStorage.DeleteTask(ctx, id) } diff --git a/internal/services/contest.go b/internal/contests/usecase/usecase.go similarity index 79% rename from internal/services/contest.go rename to internal/contests/usecase/usecase.go index c792fd8..8c9af4e 100644 --- a/internal/services/contest.go +++ b/internal/contests/usecase/usecase.go @@ -1,20 +1,13 @@ -package services +package usecase import ( "context" "git.sch9.ru/new_gate/models" - "git.sch9.ru/new_gate/ms-tester/internal/lib" + "git.sch9.ru/new_gate/ms-tester/internal/contests" ) -type ContestStorage interface { - CreateContest(ctx context.Context, contest *models.Contest) (int32, error) - ReadContestById(ctx context.Context, id int32) (*models.Contest, error) - UpdateContest(ctx context.Context, contest *models.Contest) error - DeleteContest(ctx context.Context, id int32) error -} - type ContestService struct { - contestStorage ContestStorage + contestStorage contests.ContestRepository permissionService IPermissionService } diff --git a/internal/languages/delivery.go b/internal/languages/delivery.go new file mode 100644 index 0000000..e86683a --- /dev/null +++ b/internal/languages/delivery.go @@ -0,0 +1,4 @@ +package languages + +type languageHandlers interface { +} diff --git a/internal/languages/delivery/grpc/handlers.go b/internal/languages/delivery/grpc/handlers.go new file mode 100644 index 0000000..21e034e --- /dev/null +++ b/internal/languages/delivery/grpc/handlers.go @@ -0,0 +1 @@ +package grpc diff --git a/internal/languages/pg_repository.go b/internal/languages/pg_repository.go new file mode 100644 index 0000000..b966134 --- /dev/null +++ b/internal/languages/pg_repository.go @@ -0,0 +1,10 @@ +package languages + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type LanguageRepository interface { + ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) +} diff --git a/internal/languages/repository/pg_repository.go b/internal/languages/repository/pg_repository.go new file mode 100644 index 0000000..add08ea --- /dev/null +++ b/internal/languages/repository/pg_repository.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + "git.sch9.ru/new_gate/models" + "git.sch9.ru/new_gate/ms-tester/pkg/utils" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +type LanguageRepository struct { + db *sqlx.DB + logger *zap.Logger +} + +func NewLanguageRepository(db *sqlx.DB, logger *zap.Logger) *LanguageRepository { + return &LanguageRepository{ + db: db, + logger: logger, + } +} + +func (r *LanguageRepository) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) { + if id <= int32(len(models.Languages)) { + return nil, utils.StorageError(nil, utils.ErrNotFound, "languages not found") + } + return &models.Languages[id], nil +} diff --git a/internal/languages/usecase.go b/internal/languages/usecase.go new file mode 100644 index 0000000..b6eab08 --- /dev/null +++ b/internal/languages/usecase.go @@ -0,0 +1,10 @@ +package languages + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type LanguageUseCase interface { + ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) +} diff --git a/internal/languages/usecase/all.rego b/internal/languages/usecase/all.rego new file mode 100644 index 0000000..8525c6a --- /dev/null +++ b/internal/languages/usecase/all.rego @@ -0,0 +1,44 @@ +package problem.rbac + +import rego.v1 + +spectator := 0 +participant := 1 +moderator := 2 +admin := 3 + +permissions := { + "read": is_spectator, + "participate": is_participant, + "update": is_moderator, + "create": is_moderator, + "delete": is_moderator, +} + +default allow := false + +allow if is_admin + +allow if { + permissions[input.action] +} + +default is_admin := false +is_admin if { + input.user.role == admin +} + +default is_moderator := false +is_moderator if { + input.user.role >= moderator +} + +default is_participant := false +is_participant if { + input.user.role >= participant +} + +default is_spectator := true +is_spectator if { + input.user.role >= spectator +} diff --git a/internal/languages/usecase/permission.go b/internal/languages/usecase/permission.go new file mode 100644 index 0000000..59af1f8 --- /dev/null +++ b/internal/languages/usecase/permission.go @@ -0,0 +1,39 @@ +package services + +import ( + "context" + "git.sch9.ru/new_gate/models" + "github.com/open-policy-agent/opa/rego" +) + +type PermissionService struct { + query *rego.PreparedEvalQuery +} + +func NewPermissionService() *PermissionService { + query, err := rego.New( + rego.Query("allow = data.problem.rbac.allow"), + rego.Load([]string{"./opa/all.rego"}, nil), + ).PrepareForEval(context.TODO()) + + if err != nil { + panic(err) + } + + return &PermissionService{ + query: &query, + } +} + +func (s *PermissionService) Allowed(ctx context.Context, user *models.User, action string) bool { + input := map[string]interface{}{ + "user": user, + "action": action, + } + + result, err := s.query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + panic(err) + } + return result[0].Bindings["allow"].(bool) +} diff --git a/internal/services/language.go b/internal/languages/usecase/usecase.go similarity index 70% rename from internal/services/language.go rename to internal/languages/usecase/usecase.go index 42db07f..9ce1f82 100644 --- a/internal/services/language.go +++ b/internal/languages/usecase/usecase.go @@ -1,16 +1,13 @@ -package services +package usecase import ( "context" "git.sch9.ru/new_gate/models" + "git.sch9.ru/new_gate/ms-tester/internal/languages" ) -type LanguageStorage interface { - ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) -} - -type LanguageService struct { - languageStorage LanguageStorage +type LanguageUseCase struct { + languageRepo languages.LanguageRepository } func NewLanguageService( diff --git a/internal/lib/lib.go b/internal/lib/lib.go deleted file mode 100644 index 2cc988d..0000000 --- a/internal/lib/lib.go +++ /dev/null @@ -1,17 +0,0 @@ -package lib - -import ( - "time" -) - -func AsTimeP(t time.Time) *time.Time { - return &t -} - -func AsInt32P(v int32) *int32 { - return &v -} - -func AsStringP(str string) *string { - return &str -} diff --git a/internal/problems/delivery.go b/internal/problems/delivery.go new file mode 100644 index 0000000..0e36f9e --- /dev/null +++ b/internal/problems/delivery.go @@ -0,0 +1,13 @@ +package problems + +import ( + "context" + problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1" + "google.golang.org/protobuf/types/known/emptypb" +) + +type Handlers interface { + CreateProblem(server problemv1.ProblemService_CreateProblemServer) error + ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) + DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) +} diff --git a/internal/transport/problem.go b/internal/problems/delivery/grpc/handlers.go similarity index 74% rename from internal/transport/problem.go rename to internal/problems/delivery/grpc/handlers.go index ef5645d..d7e7bbe 100644 --- a/internal/transport/problem.go +++ b/internal/problems/delivery/grpc/handlers.go @@ -1,10 +1,12 @@ -package transport +package grpc import ( "context" "git.sch9.ru/new_gate/models" "git.sch9.ru/new_gate/ms-tester/internal/lib" + "git.sch9.ru/new_gate/ms-tester/internal/problems" problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1" + "git.sch9.ru/new_gate/ms-tester/pkg/utils" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -12,10 +14,16 @@ import ( "os" ) -func (s *TesterServer) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error { +type problemHandlers struct { + problemv1.UnimplementedProblemServiceServer + + problemUC problems.ProblemUseCase +} + +func (h *problemHandlers) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error { ctx := server.Context() - if err := s.problemService.CanCreateProblem(ctx); err != nil { + if err := h.problemUC.CanCreateProblem(ctx); err != nil { return err } @@ -42,7 +50,7 @@ func (s *TesterServer) CreateProblem(server problemv1.ProblemService_CreateProbl return err // FIXME } - id, err := s.problemService.CreateProblem(ctx, p) // FIXME + id, err := h.problemUC.CreateProblem(ctx, p) // FIXME if err != nil { return err } @@ -113,8 +121,8 @@ func readChunks(ctx context.Context, server problemv1.ProblemService_CreateProbl return ch } -func (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) { - problem, err := s.problemService.ReadProblemById(ctx, req.GetId()) +func (h *problemHandlers) ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) { + problem, err := h.problemUC.ReadProblemById(ctx, req.GetId()) if err != nil { return nil, err } @@ -125,13 +133,13 @@ func (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProbl Description: *problem.Description, TimeLimit: *problem.TimeLimit, MemoryLimit: *problem.MemoryLimit, - CreatedAt: AsTimestampP(problem.CreatedAt), - UpdatedAt: AsTimestampP(problem.UpdatedAt), + CreatedAt: utils.TimestampP(problem.CreatedAt), + UpdatedAt: utils.TimestampP(problem.UpdatedAt), }, }, nil } -//func (s *TesterServer) UpdateProblem(ctx context.Context, req *problemv1.UpdateProblemRequest) (*emptypb.Empty, error) { +//func (h *problemHandlers) UpdateProblem(ctx context.Context, req *problemv1.UpdateProblemRequest) (*emptypb.Empty, error) { // problem := req.GetProblem() // if problem == nil { // return nil, status.Errorf(codes.Unknown, "") // FIXME @@ -153,8 +161,8 @@ func (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProbl // return &emptypb.Empty{}, nil //} -func (s *TesterServer) DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) { - err := s.problemService.DeleteProblem(ctx, req.GetId()) +func (h *problemHandlers) DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) { + err := h.problemUC.DeleteProblem(ctx, req.GetId()) if err != nil { return nil, err } diff --git a/internal/problems/pg_repository.go b/internal/problems/pg_repository.go new file mode 100644 index 0000000..f4e1358 --- /dev/null +++ b/internal/problems/pg_repository.go @@ -0,0 +1,13 @@ +package problems + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type ProblemPostgresRepository interface { + CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error) + ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) + UpdateProblem(ctx context.Context, problem *models.Problem) error + DeleteProblem(ctx context.Context, id int32) error +} diff --git a/internal/storage/problem.go b/internal/problems/repository/pg_repository.go similarity index 63% rename from internal/storage/problem.go rename to internal/problems/repository/pg_repository.go index 4f7b52d..c68552b 100644 --- a/internal/storage/problem.go +++ b/internal/problems/repository/pg_repository.go @@ -1,26 +1,29 @@ -package storage +package repository import ( "context" "errors" "git.sch9.ru/new_gate/models" + "git.sch9.ru/new_gate/ms-tester/pkg/utils" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" "github.com/jmoiron/sqlx" "go.uber.org/zap" ) -type ProblemStorage struct { +type ProblemRepository struct { db *sqlx.DB logger *zap.Logger } -func NewProblemStorage(db *sqlx.DB, logger *zap.Logger) *ProblemStorage { - return &ProblemStorage{ +func NewProblemRepository(db *sqlx.DB, logger *zap.Logger) *ProblemRepository { + return &ProblemRepository{ db: db, logger: logger, } } -func (storage *ProblemStorage) CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error) { +func (storage *ProblemRepository) CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error) { tx, err := storage.db.Beginx() if err != nil { return 0, handlePgErr(err) @@ -80,7 +83,7 @@ RETURNING id return id, nil } -func (storage *ProblemStorage) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) { +func (storage *ProblemRepository) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) { var problem models.Problem query := storage.db.Rebind("SELECT * from problems WHERE id=? LIMIT 1") err := storage.db.GetContext(ctx, &problem, query, id) @@ -90,7 +93,7 @@ func (storage *ProblemStorage) ReadProblemById(ctx context.Context, id int32) (* return &problem, nil } -func (storage *ProblemStorage) UpdateProblem(ctx context.Context, problem *models.Problem) error { +func (storage *ProblemRepository) UpdateProblem(ctx context.Context, problem *models.Problem) error { query := storage.db.Rebind("UPDATE problems SET name=?,description=?,time_limit=?,memory_limit=? WHERE id=?") _, err := storage.db.ExecContext(ctx, query, problem.Name, problem.Description, problem.TimeLimit, problem.MemoryLimit, problem.Id) if err != nil { @@ -99,7 +102,7 @@ func (storage *ProblemStorage) UpdateProblem(ctx context.Context, problem *model return nil } -func (storage *ProblemStorage) DeleteProblem(ctx context.Context, id int32) error { +func (storage *ProblemRepository) DeleteProblem(ctx context.Context, id int32) error { query := storage.db.Rebind("DELETE FROM problems WHERE id=?") _, err := storage.db.ExecContext(ctx, query, id) if err != nil { @@ -108,3 +111,18 @@ func (storage *ProblemStorage) DeleteProblem(ctx context.Context, id int32) erro return nil } + +func handlePgErr(err error) error { + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + return utils.StorageError(err, utils.ErrUnknown, "unexpected error from postgres") + } + if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { + // TODO: probably should specify which constraint + return utils.StorageError(err, utils.ErrConflict, pgErr.Message) + } + if pgerrcode.IsNoData(pgErr.Code) { + return utils.StorageError(err, utils.ErrNotFound, pgErr.Message) + } + return utils.StorageError(err, utils.ErrUnimplemented, "unimplemented error") +} diff --git a/internal/problems/usecase.go b/internal/problems/usecase.go new file mode 100644 index 0000000..162f23b --- /dev/null +++ b/internal/problems/usecase.go @@ -0,0 +1,17 @@ +package problems + +import ( + "context" + "git.sch9.ru/new_gate/models" +) + +type ProblemUseCase interface { + CanCreateProblem(ctx context.Context) error + CanReadProblemById(ctx context.Context) error + CanUpdateProblem(ctx context.Context) error + CanDeleteProblem(ctx context.Context) error + CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) + ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) + UpdateProblem(ctx context.Context, problem *models.Problem) error + DeleteProblem(ctx context.Context, id int32) error +} diff --git a/internal/problems/usecase/all.rego b/internal/problems/usecase/all.rego new file mode 100644 index 0000000..8525c6a --- /dev/null +++ b/internal/problems/usecase/all.rego @@ -0,0 +1,44 @@ +package problem.rbac + +import rego.v1 + +spectator := 0 +participant := 1 +moderator := 2 +admin := 3 + +permissions := { + "read": is_spectator, + "participate": is_participant, + "update": is_moderator, + "create": is_moderator, + "delete": is_moderator, +} + +default allow := false + +allow if is_admin + +allow if { + permissions[input.action] +} + +default is_admin := false +is_admin if { + input.user.role == admin +} + +default is_moderator := false +is_moderator if { + input.user.role >= moderator +} + +default is_participant := false +is_participant if { + input.user.role >= participant +} + +default is_spectator := true +is_spectator if { + input.user.role >= spectator +} diff --git a/internal/problems/usecase/permission.go b/internal/problems/usecase/permission.go new file mode 100644 index 0000000..59af1f8 --- /dev/null +++ b/internal/problems/usecase/permission.go @@ -0,0 +1,39 @@ +package services + +import ( + "context" + "git.sch9.ru/new_gate/models" + "github.com/open-policy-agent/opa/rego" +) + +type PermissionService struct { + query *rego.PreparedEvalQuery +} + +func NewPermissionService() *PermissionService { + query, err := rego.New( + rego.Query("allow = data.problem.rbac.allow"), + rego.Load([]string{"./opa/all.rego"}, nil), + ).PrepareForEval(context.TODO()) + + if err != nil { + panic(err) + } + + return &PermissionService{ + query: &query, + } +} + +func (s *PermissionService) Allowed(ctx context.Context, user *models.User, action string) bool { + input := map[string]interface{}{ + "user": user, + "action": action, + } + + result, err := s.query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + panic(err) + } + return result[0].Bindings["allow"].(bool) +} diff --git a/internal/services/problem.go b/internal/problems/usecase/usecase.go similarity index 75% rename from internal/services/problem.go rename to internal/problems/usecase/usecase.go index e65a9d5..b46a988 100644 --- a/internal/services/problem.go +++ b/internal/problems/usecase/usecase.go @@ -1,9 +1,10 @@ -package services +package usecase import ( "context" "git.sch9.ru/new_gate/models" "git.sch9.ru/new_gate/ms-tester/internal/lib" + "git.sch9.ru/new_gate/ms-tester/pkg/external/pandoc" ) type ProblemStorage interface { @@ -13,26 +14,22 @@ type ProblemStorage interface { DeleteProblem(ctx context.Context, id int32) error } -type PandocClient interface { - ConvertLatexToHtml5(ctx context.Context, text string) (string, error) -} - type IPermissionService interface { Allowed(ctx context.Context, user *models.User, action string) bool } -type ProblemService struct { +type ProblemUseCase struct { problemStorage ProblemStorage - pandocClient PandocClient + pandocClient pandoc.PandocClient permissionService IPermissionService } -func NewProblemService( +func NewProblemUseCase( problemStorage ProblemStorage, - pandocClient PandocClient, + pandocClient pandoc.PandocClient, permissionService IPermissionService, -) *ProblemService { - return &ProblemService{ +) *ProblemUseCase { + return &ProblemUseCase{ problemStorage: problemStorage, pandocClient: pandocClient, permissionService: permissionService, @@ -43,35 +40,35 @@ func extractUser(ctx context.Context) *models.User { return ctx.Value("user").(*models.User) } -func (service *ProblemService) CanCreateProblem(ctx context.Context) error { +func (service *ProblemUseCase) CanCreateProblem(ctx context.Context) error { if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") { return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") } return nil } -func (service *ProblemService) CanReadProblemById(ctx context.Context) error { +func (service *ProblemUseCase) CanReadProblemById(ctx context.Context) error { if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") { return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") } return nil } -func (service *ProblemService) CanUpdateProblem(ctx context.Context) error { +func (service *ProblemUseCase) CanUpdateProblem(ctx context.Context) error { if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") { return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") } return nil } -func (service *ProblemService) CanDeleteProblem(ctx context.Context) error { +func (service *ProblemUseCase) CanDeleteProblem(ctx context.Context) error { if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") { return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied") } return nil } -func (service *ProblemService) CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) { +func (service *ProblemUseCase) CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) { if err := service.CanCreateProblem(ctx); err != nil { return 0, err } @@ -82,21 +79,21 @@ func (service *ProblemService) CreateProblem(ctx context.Context, problem *model return service.problemStorage.CreateProblem(ctx, problem, nil) } -func (service *ProblemService) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) { +func (service *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) { if err := service.CanReadProblemById(ctx); err != nil { return nil, err } return service.problemStorage.ReadProblemById(ctx, id) } -func (service *ProblemService) UpdateProblem(ctx context.Context, problem *models.Problem) error { +func (service *ProblemUseCase) UpdateProblem(ctx context.Context, problem *models.Problem) error { if err := service.CanUpdateProblem(ctx); err != nil { return err } return service.problemStorage.UpdateProblem(ctx, problem) } -func (service *ProblemService) DeleteProblem(ctx context.Context, id int32) error { +func (service *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error { if err := service.CanDeleteProblem(ctx); err != nil { return err } diff --git a/internal/services/user.go b/internal/services/user.go deleted file mode 100644 index ecdec84..0000000 --- a/internal/services/user.go +++ /dev/null @@ -1,29 +0,0 @@ -package services - -import ( - "context" - "git.sch9.ru/new_gate/models" -) - -type UserStorage interface { - CreateUser(ctx context.Context, user *models.User) error - ReadUserById(ctx context.Context, userId int32) (*models.User, error) -} - -type UserService struct { - userStorage UserStorage -} - -func NewUserService(userStorage UserStorage) *UserService { - return &UserService{ - userStorage: userStorage, - } -} - -func (s *UserService) CreateUser(ctx context.Context, user *models.User) error { - return s.userStorage.CreateUser(ctx, user) -} - -func (s *UserService) ReadUserById(ctx context.Context, userId int32) (*models.User, error) { - return s.userStorage.ReadUserById(ctx, userId) -} diff --git a/internal/storage/contests.go b/internal/storage/contests.go deleted file mode 100644 index 146bcd6..0000000 --- a/internal/storage/contests.go +++ /dev/null @@ -1,78 +0,0 @@ -package storage - -import ( - "context" - "git.sch9.ru/new_gate/models" - "github.com/jmoiron/sqlx" - "go.uber.org/zap" -) - -type ContestStorage struct { - db *sqlx.DB - logger *zap.Logger -} - -func NewContestStorage(db *sqlx.DB, logger *zap.Logger) *ContestStorage { - return &ContestStorage{ - db: db, - logger: logger, - } -} - -func (storage *ContestStorage) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) { - query := storage.db.Rebind(` -INSERT INTO contests - (name) -VALUES (?) -RETURNING id -`) - - rows, err := storage.db.QueryxContext( - ctx, - query, - contest.Name, - ) - if err != nil { - return 0, handlePgErr(err) - } - - defer rows.Close() - var id int32 - err = rows.StructScan(&id) - if err != nil { - return 0, handlePgErr(err) - } - - return id, nil - -} - -func (storage *ContestStorage) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) { - var contest models.Contest - query := storage.db.Rebind("SELECT * from contests WHERE id=? LIMIT 1") - err := storage.db.GetContext(ctx, &contest, query, id) - if err != nil { - return nil, handlePgErr(err) - } - return &contest, nil -} - -func (storage *ContestStorage) UpdateContest(ctx context.Context, contest *models.Contest) error { - query := storage.db.Rebind("UPDATE contests SET name=? WHERE id=?") - _, err := storage.db.ExecContext(ctx, query, contest.Name, contest.Id) - if err != nil { - return handlePgErr(err) - } - - return nil -} - -func (storage *ContestStorage) DeleteContest(ctx context.Context, id int32) error { - query := storage.db.Rebind("DELETE FROM contests WHERE id=?") - _, err := storage.db.ExecContext(ctx, query, id) - if err != nil { - return handlePgErr(err) - } - - return nil -} diff --git a/internal/storage/errhandling.go b/internal/storage/errhandling.go deleted file mode 100644 index ff720e3..0000000 --- a/internal/storage/errhandling.go +++ /dev/null @@ -1,23 +0,0 @@ -package storage - -import ( - "errors" - "git.sch9.ru/new_gate/ms-tester/internal/lib" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v5/pgconn" -) - -func handlePgErr(err error) error { - var pgErr *pgconn.PgError - if !errors.As(err, &pgErr) { - return lib.StorageError(err, lib.ErrUnknown, "unexpected error from postgres") - } - if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { - // TODO: probably should specify which constraint - return lib.StorageError(err, lib.ErrConflict, pgErr.Message) - } - if pgerrcode.IsNoData(pgErr.Code) { - return lib.StorageError(err, lib.ErrNotFound, pgErr.Message) - } - return lib.StorageError(err, lib.ErrUnimplemented, "unimplemented error") -} diff --git a/internal/storage/language.go b/internal/storage/language.go deleted file mode 100644 index b8be4e2..0000000 --- a/internal/storage/language.go +++ /dev/null @@ -1,28 +0,0 @@ -package storage - -import ( - "context" - "git.sch9.ru/new_gate/models" - "git.sch9.ru/new_gate/ms-tester/internal/lib" - "github.com/jmoiron/sqlx" - "go.uber.org/zap" -) - -type LanguageStorage struct { - db *sqlx.DB - logger *zap.Logger -} - -func NewLanguageStorage(db *sqlx.DB, logger *zap.Logger) *LanguageStorage { - return &LanguageStorage{ - db: db, - logger: logger, - } -} - -func (storage *LanguageStorage) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) { - if id <= int32(len(models.Languages)) { - return nil, lib.StorageError(nil, lib.ErrNotFound, "language not found") - } - return &models.Languages[id], nil -} diff --git a/internal/storage/user.go b/internal/storage/user.go deleted file mode 100644 index 64a0172..0000000 --- a/internal/storage/user.go +++ /dev/null @@ -1,42 +0,0 @@ -package storage - -import ( - "context" - "git.sch9.ru/new_gate/models" - "github.com/jmoiron/sqlx" -) - -type UserStorage struct { - db *sqlx.DB -} - -func NewUserStorage(db *sqlx.DB) *UserStorage { - return &UserStorage{ - db: db, - } -} - -func (storage *UserStorage) CreateUser(ctx context.Context, user *models.User) error { - query := storage.db.Rebind("INSERT INTO users (user_id, role) VALUES (?, ?)") - _, err := storage.db.ExecContext(ctx, query, user.UserId, user.Role) - if err != nil { - return err - } - return nil -} - -func (storage *UserStorage) ReadUserById(ctx context.Context, userId int32) (*models.User, error) { - query := storage.db.Rebind(` -SELECT * -FROM users -WHERE user_id = ? -LIMIT 1; -`) - - var user models.User - err := storage.db.GetContext(ctx, &user, query, userId) - if err != nil { - return nil, err - } - return &user, nil -} diff --git a/internal/tester/delivery.go b/internal/tester/delivery.go new file mode 100644 index 0000000..3fdd542 --- /dev/null +++ b/internal/tester/delivery.go @@ -0,0 +1 @@ +package tester diff --git a/internal/tester/delivery/grpc/handlers.go b/internal/tester/delivery/grpc/handlers.go new file mode 100644 index 0000000..21e034e --- /dev/null +++ b/internal/tester/delivery/grpc/handlers.go @@ -0,0 +1 @@ +package grpc diff --git a/internal/tester/pg_repository.go b/internal/tester/pg_repository.go new file mode 100644 index 0000000..3fdd542 --- /dev/null +++ b/internal/tester/pg_repository.go @@ -0,0 +1 @@ +package tester diff --git a/internal/tester/repository/pg_repository.go b/internal/tester/repository/pg_repository.go new file mode 100644 index 0000000..50a4378 --- /dev/null +++ b/internal/tester/repository/pg_repository.go @@ -0,0 +1 @@ +package repository diff --git a/internal/tester/usecase.go b/internal/tester/usecase.go new file mode 100644 index 0000000..3fdd542 --- /dev/null +++ b/internal/tester/usecase.go @@ -0,0 +1 @@ +package tester diff --git a/internal/tester/usecase/all.rego b/internal/tester/usecase/all.rego new file mode 100644 index 0000000..8525c6a --- /dev/null +++ b/internal/tester/usecase/all.rego @@ -0,0 +1,44 @@ +package problem.rbac + +import rego.v1 + +spectator := 0 +participant := 1 +moderator := 2 +admin := 3 + +permissions := { + "read": is_spectator, + "participate": is_participant, + "update": is_moderator, + "create": is_moderator, + "delete": is_moderator, +} + +default allow := false + +allow if is_admin + +allow if { + permissions[input.action] +} + +default is_admin := false +is_admin if { + input.user.role == admin +} + +default is_moderator := false +is_moderator if { + input.user.role >= moderator +} + +default is_participant := false +is_participant if { + input.user.role >= participant +} + +default is_spectator := true +is_spectator if { + input.user.role >= spectator +} diff --git a/internal/tester/usecase/permission.go b/internal/tester/usecase/permission.go new file mode 100644 index 0000000..59af1f8 --- /dev/null +++ b/internal/tester/usecase/permission.go @@ -0,0 +1,39 @@ +package services + +import ( + "context" + "git.sch9.ru/new_gate/models" + "github.com/open-policy-agent/opa/rego" +) + +type PermissionService struct { + query *rego.PreparedEvalQuery +} + +func NewPermissionService() *PermissionService { + query, err := rego.New( + rego.Query("allow = data.problem.rbac.allow"), + rego.Load([]string{"./opa/all.rego"}, nil), + ).PrepareForEval(context.TODO()) + + if err != nil { + panic(err) + } + + return &PermissionService{ + query: &query, + } +} + +func (s *PermissionService) Allowed(ctx context.Context, user *models.User, action string) bool { + input := map[string]interface{}{ + "user": user, + "action": action, + } + + result, err := s.query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + panic(err) + } + return result[0].Bindings["allow"].(bool) +} diff --git a/internal/tester/usecase/usecase.go b/internal/tester/usecase/usecase.go new file mode 100644 index 0000000..aed2454 --- /dev/null +++ b/internal/tester/usecase/usecase.go @@ -0,0 +1 @@ +package usecase diff --git a/main.go b/main.go index cfec229..40012c1 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,11 @@ package main import ( "fmt" - "git.sch9.ru/new_gate/ms-tester/internal/lib" + "git.sch9.ru/new_gate/ms-tester/config" "git.sch9.ru/new_gate/ms-tester/internal/services" "git.sch9.ru/new_gate/ms-tester/internal/storage" "git.sch9.ru/new_gate/ms-tester/internal/transport" + "git.sch9.ru/new_gate/ms-tester/pkg/external/pandoc" sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1" "github.com/ilyakaznacheev/cleanenv" _ "github.com/jackc/pgx/v5/stdlib" @@ -21,7 +22,7 @@ import ( ) func main() { - var cfg lib.Config + var cfg config.Config err := cleanenv.ReadConfig(".env", &cfg) if err != nil { panic(fmt.Sprintf("error reading config: %s", err.Error())) @@ -45,7 +46,7 @@ func main() { //contestStorage := storage.NewContestStorage(db, logger) //contestService := services.NewContestService(contestStorage) - pandocClient := lib.NewPandocClient(&http.Client{}, cfg.Pandoc) + pandocClient := pandoc.NewPandocClient(&http.Client{}, cfg.Pandoc) grpcSessionClient, err := grpc.NewClient(cfg.Auth, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { diff --git a/migrations/20240727123308_initial.sql b/migrations/20240727123308_initial.sql index 43ce8a3..5d534d8 100644 --- a/migrations/20240727123308_initial.sql +++ b/migrations/20240727123308_initial.sql @@ -211,21 +211,6 @@ CREATE TABLE IF NOT EXISTS participant_task ); - - -CREATE TABLE IF NOT EXISTS users -( - user_id INT NOT NULL, - role INT NOT NULL, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CHECK ( role BETWEEN 0 AND 3), - PRIMARY KEY (user_id) -); - - - - CREATE FUNCTION on_new_participant() RETURNS TRIGGER AS $$ BEGIN --RAISE NOTICE 'NEW.ID:%, NEW.contest_id:%', NEW.id,NEW.contest_id; @@ -288,7 +273,6 @@ $$; --CREATE TRIGGER languages_upd_trg BEFORE UPDATE ON languages FOR EACH ROW EXECUTE FUNCTION updated_at_update(); CREATE TRIGGER problems_upd_trg BEFORE UPDATE ON problems FOR EACH ROW EXECUTE FUNCTION updated_at_update(); CREATE TRIGGER contests_upd_trg BEFORE UPDATE ON contests FOR EACH ROW EXECUTE FUNCTION updated_at_update(); -CREATE TRIGGER users_upd_trg BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION updated_at_update(); -- +goose StatementEnd -- +goose Down @@ -312,5 +296,4 @@ DROP TABLE IF EXISTS subtasks CASCADE; DROP TABLE IF EXISTS participants CASCADE; DROP TABLE IF EXISTS participant_task CASCADE; DROP TABLE IF EXISTS participant_subtask CASCADE; -DROP TABLE IF EXISTS users CASCADE; -- +goose StatementEnd diff --git a/pkg/external/aws/aws.go b/pkg/external/aws/aws.go new file mode 100644 index 0000000..a1f9c0e --- /dev/null +++ b/pkg/external/aws/aws.go @@ -0,0 +1 @@ +package aws diff --git a/pkg/external/kafka/kafka.go b/pkg/external/kafka/kafka.go new file mode 100644 index 0000000..82b3441 --- /dev/null +++ b/pkg/external/kafka/kafka.go @@ -0,0 +1 @@ +package kafka diff --git a/pkg/external/ms-auth/client.go b/pkg/external/ms-auth/client.go new file mode 100644 index 0000000..a8d771d --- /dev/null +++ b/pkg/external/ms-auth/client.go @@ -0,0 +1 @@ +package ms_auth diff --git a/internal/lib/pandoc.go b/pkg/external/pandoc/pandoc.go similarity index 68% rename from internal/lib/pandoc.go rename to pkg/external/pandoc/pandoc.go index 42701b0..af2427a 100644 --- a/internal/lib/pandoc.go +++ b/pkg/external/pandoc/pandoc.go @@ -1,4 +1,4 @@ -package lib +package pandoc import ( "bytes" @@ -8,13 +8,17 @@ import ( "net/http" ) -type PandocClient struct { +type Client struct { client *http.Client address string } -func NewPandocClient(client *http.Client, address string) *PandocClient { - return &PandocClient{ +type PandocClient interface { + ConvertLatexToHtml5(ctx context.Context, text string) (string, error) +} + +func NewPandocClient(client *http.Client, address string) *Client { + return &Client{ client: client, address: address, } @@ -26,7 +30,7 @@ type convertRequest struct { To string `json:"to"` } -func (client *PandocClient) convert(ctx context.Context, text, from, to string) (string, error) { +func (client *Client) convert(ctx context.Context, text, from, to string) (string, error) { body, err := json.Marshal(convertRequest{ Text: text, From: from, @@ -62,6 +66,6 @@ func (client *PandocClient) convert(ctx context.Context, text, from, to string) return string(body), nil } -func (client *PandocClient) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) { +func (client *Client) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) { return client.convert(ctx, text, "latex", "html5") } diff --git a/pkg/external/postgres/postgres.go b/pkg/external/postgres/postgres.go new file mode 100644 index 0000000..b83463e --- /dev/null +++ b/pkg/external/postgres/postgres.go @@ -0,0 +1,30 @@ +package postgres + +import ( + "github.com/jmoiron/sqlx" + "time" +) + +const ( + maxOpenConns = 60 + connMaxLifetime = 120 + maxIdleConns = 30 + connMaxIdleTime = 20 +) + +func NewPostgresDB(dsn string) (*sqlx.DB, error) { + db, err := sqlx.Open("pgx", dsn) + if err != nil { + return nil, err + } + + db.SetMaxOpenConns(maxOpenConns) + db.SetConnMaxLifetime(connMaxLifetime * time.Second) + db.SetMaxIdleConns(maxIdleConns) + db.SetConnMaxIdleTime(connMaxIdleTime * time.Second) + if err = db.Ping(); err != nil { + return nil, err + } + + return db, nil +} diff --git a/pkg/external/valkey/valkey.go b/pkg/external/valkey/valkey.go new file mode 100644 index 0000000..c5a4900 --- /dev/null +++ b/pkg/external/valkey/valkey.go @@ -0,0 +1,12 @@ +package valkey + +import "github.com/valkey-io/valkey-go" + +func NewValkeyClient(dsn string) (valkey.Client, error) { + opts, err := valkey.ParseURL(dsn) + if err != nil { + return nil, err + } + + return valkey.NewClient(opts) +} diff --git a/internal/lib/errors.go b/pkg/utils/errors.go similarity index 99% rename from internal/lib/errors.go rename to pkg/utils/errors.go index 888d17d..98bd286 100644 --- a/internal/lib/errors.go +++ b/pkg/utils/errors.go @@ -1,4 +1,4 @@ -package lib +package utils import ( "errors" diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..8492339 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,33 @@ +package utils + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + "time" +) + +func AsTimeP(t time.Time) *time.Time { + return &t +} + +func AsInt32P(v int32) *int32 { + return &v +} + +func AsStringP(str string) *string { + return &str +} + +func TimeP(t *timestamppb.Timestamp) *time.Time { + if t == nil { + return nil + } + tt := t.AsTime() + return &tt +} + +func TimestampP(t *time.Time) *timestamppb.Timestamp { + if t == nil { + return nil + } + return timestamppb.New(*t) +}