refactor:
This commit is contained in:
parent
81e75e5a9c
commit
d62ae666d5
57 changed files with 656 additions and 310 deletions
13
internal/problems/delivery.go
Normal file
13
internal/problems/delivery.go
Normal file
|
@ -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)
|
||||
}
|
170
internal/problems/delivery/grpc/handlers.go
Normal file
170
internal/problems/delivery/grpc/handlers.go
Normal file
|
@ -0,0 +1,170 @@
|
|||
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"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type problemHandlers struct {
|
||||
problemv1.UnimplementedProblemServiceServer
|
||||
|
||||
problemUC problems.ProblemUseCase
|
||||
}
|
||||
|
||||
func (h *problemHandlers) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error {
|
||||
ctx := server.Context()
|
||||
|
||||
if err := h.problemUC.CanCreateProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := server.Recv() // receive problem
|
||||
if err != nil {
|
||||
return lib.TransportError(err, lib.ErrBadInput, "can't receive problem")
|
||||
}
|
||||
problem := req.GetProblem()
|
||||
if problem == nil {
|
||||
return lib.TransportError(nil, lib.ErrBadInput, "empty problem")
|
||||
}
|
||||
|
||||
p := &models.Problem{
|
||||
Name: lib.AsStringP(problem.Name),
|
||||
Description: lib.AsStringP(problem.Description),
|
||||
TimeLimit: lib.AsInt32P(problem.TimeLimit),
|
||||
MemoryLimit: lib.AsInt32P(problem.MemoryLimit),
|
||||
}
|
||||
|
||||
ch := readChunks(ctx, server)
|
||||
|
||||
err = writeChunks(ctx, ch) // temp stub
|
||||
if err != nil {
|
||||
return err // FIXME
|
||||
}
|
||||
|
||||
id, err := h.problemUC.CreateProblem(ctx, p) // FIXME
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = server.SendAndClose(&problemv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
return lib.TransportError(err, lib.ErrBadInput, "can't send response")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeChunks(ctx context.Context, chunks <-chan []byte) error {
|
||||
// use s3
|
||||
// FIXME: use ctx?
|
||||
f, err := os.Create("out.txt") // FIXME: uuidv4 as initial temp name?
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var off int64 = 0
|
||||
for chunk := range chunks {
|
||||
_, err = f.WriteAt(chunk, off)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
off += int64(len(chunk))
|
||||
}
|
||||
|
||||
// TODO: rename file to its hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func readChunks(ctx context.Context, server problemv1.ProblemService_CreateProblemServer) <-chan []byte {
|
||||
ch := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // FIXME
|
||||
default:
|
||||
req, err := server.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return // FIXME
|
||||
}
|
||||
if status.Code(err) == codes.Canceled {
|
||||
return // FIXME
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
test := req.GetTest()
|
||||
if test == nil {
|
||||
return // FIXME
|
||||
}
|
||||
|
||||
ch <- test.Chunk
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &problemv1.ReadProblemResponse{
|
||||
Problem: &problemv1.ReadProblemResponse_Problem{
|
||||
Id: *problem.Id,
|
||||
Name: *problem.Name,
|
||||
Description: *problem.Description,
|
||||
TimeLimit: *problem.TimeLimit,
|
||||
MemoryLimit: *problem.MemoryLimit,
|
||||
CreatedAt: utils.TimestampP(problem.CreatedAt),
|
||||
UpdatedAt: utils.TimestampP(problem.UpdatedAt),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
//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
|
||||
// }
|
||||
// err := s.problemService.UpdateProblem(
|
||||
// ctx,
|
||||
// &models.Problem{
|
||||
// Id: lib.AsInt32P(problem.Id),
|
||||
// Name: problem.Name,
|
||||
// Description: problem.Description,
|
||||
// TimeLimit: problem.TimeLimit,
|
||||
// MemoryLimit: problem.MemoryLimit,
|
||||
// },
|
||||
// )
|
||||
// if err != nil {
|
||||
// return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME
|
||||
// }
|
||||
//
|
||||
// return &emptypb.Empty{}, nil
|
||||
//}
|
||||
|
||||
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
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
13
internal/problems/pg_repository.go
Normal file
13
internal/problems/pg_repository.go
Normal file
|
@ -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
|
||||
}
|
128
internal/problems/repository/pg_repository.go
Normal file
128
internal/problems/repository/pg_repository.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
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 ProblemRepository struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewProblemRepository(db *sqlx.DB, logger *zap.Logger) *ProblemRepository {
|
||||
return &ProblemRepository{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO problems
|
||||
(name,description,time_limit,memory_limit)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err := tx.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
problem.Name,
|
||||
problem.Description,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
for _, tgd := range testGroupData {
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO testgroups
|
||||
(problem_id,testing_strategy)
|
||||
VALUES ((select last_value from problems_id_seq),?)
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
var i int32 = 0
|
||||
for ; i < tgd.TestAmount; i++ {
|
||||
query := tx.Rebind(`
|
||||
INSERT INTO tests
|
||||
(testgroup_id)
|
||||
VALUES ((select last_value from testgroups_id_seq))
|
||||
RETURNING id
|
||||
`)
|
||||
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
|
||||
}
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
//add test saving
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
err = rows.StructScan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
17
internal/problems/usecase.go
Normal file
17
internal/problems/usecase.go
Normal file
|
@ -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
|
||||
}
|
44
internal/problems/usecase/all.rego
Normal file
44
internal/problems/usecase/all.rego
Normal file
|
@ -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
|
||||
}
|
39
internal/problems/usecase/permission.go
Normal file
39
internal/problems/usecase/permission.go
Normal file
|
@ -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)
|
||||
}
|
101
internal/problems/usecase/usecase.go
Normal file
101
internal/problems/usecase/usecase.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
type IPermissionService interface {
|
||||
Allowed(ctx context.Context, user *models.User, action string) bool
|
||||
}
|
||||
|
||||
type ProblemUseCase struct {
|
||||
problemStorage ProblemStorage
|
||||
pandocClient pandoc.PandocClient
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewProblemUseCase(
|
||||
problemStorage ProblemStorage,
|
||||
pandocClient pandoc.PandocClient,
|
||||
permissionService IPermissionService,
|
||||
) *ProblemUseCase {
|
||||
return &ProblemUseCase{
|
||||
problemStorage: problemStorage,
|
||||
pandocClient: pandocClient,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func extractUser(ctx context.Context) *models.User {
|
||||
return ctx.Value("user").(*models.User)
|
||||
}
|
||||
|
||||
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 *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 *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 *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 *ProblemUseCase) CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) {
|
||||
if err := service.CanCreateProblem(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err := service.pandocClient.ConvertLatexToHtml5(ctx, *problem.Description)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return service.problemStorage.CreateProblem(ctx, problem, nil)
|
||||
}
|
||||
|
||||
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 *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 *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
if err := service.CanDeleteProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.problemStorage.DeleteProblem(ctx, id)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue