feat(tester): extend GetContestResponse

This commit is contained in:
Vyacheslav1557 2025-03-02 00:29:31 +05:00
parent e6088953b9
commit 81d7aa2366
17 changed files with 539 additions and 238 deletions

View file

@ -1,143 +0,0 @@
package interceptors
//var defaultUser = &models.User{
// UserId: nil,
// Role: models.RoleSpectator.AsPointer(),
// UpdatedAt: nil,
//}
//
//func extractToken(ctx context.Context) string {
// md, ok := metadata.FromIncomingContext(ctx)
// if !ok {
// return ""
// }
// tokens := md.Get("token")
//
// if len(tokens) == 0 {
// return ""
// }
//
// return tokens[0]
//}
//
//func (s *TesterServer) readSessionAndReadUser(ctx context.Context, token string) (*models.User, error) {
// // FIXME: possible bottle neck: should we cache it? (think of it in future)
// // FIXME: maybe use single connection instead of multiple requests
// userId, err := s.sessionClient.Read(ctx, &sessionv1.ReadSessionRequest{Token: token})
// if err != nil {
// return nil, err
// }
//
// user, err := s.userService.ReadUserById(ctx, userId.GetUserId()) // FIXME: must be cached!
// if err != nil {
// if errors.Is(err, utils.ErrNotFound) {
// user = &models.User{
// UserId: utils.AsInt32P(userId.GetUserId()),
// Role: models.RoleParticipant.AsPointer(),
// }
// err = s.userService.CreateUser(ctx, user)
// if err != nil {
// return nil, err
// }
// } else {
// return nil, err
// }
// }
//
// return user, nil
//}
//
//func insertUser(ctx context.Context, user *models.User) context.Context {
// return context.WithValue(ctx, "user", user)
//}
//
//func (s *TesterServer) AuthUnaryInterceptor() grpc.UnaryServerInterceptor {
// return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// token := extractToken(ctx)
// if token == "" {
// return handler(insertUser(ctx, defaultUser), req)
// }
//
// user, err := s.readSessionAndReadUser(ctx, token)
// if err != nil {
// return handler(insertUser(ctx, defaultUser), req)
// }
//
// return handler(insertUser(ctx, user), req)
// }
//}
//
//type ssWrapper struct {
// grpc.ServerStream
// ctx context.Context
//}
//
//func (s *ssWrapper) Context() context.Context {
// return s.ctx
//}
//
//func (s *TesterServer) AuthStreamInterceptor() grpc.StreamServerInterceptor {
// return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// ctx := ss.Context()
//
// token := extractToken(ctx)
// if token == "" {
// return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
// }
//
// user, err := s.readSessionAndReadUser(ctx, token)
// if err != nil {
// return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
// }
//
// return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, user)})
// }
//}
//
//func ToGrpcError(err error) error {
// if err == nil {
// return nil
// }
//
// // should I use map instead?
// switch {
// case errors.Is(err, utils.ErrValidationFailed):
// return status.Error(codes.InvalidArgument, err.Error())
// case errors.Is(err, utils.ErrInternal):
// return status.Error(codes.Internal, err.Error())
// case errors.Is(err, utils.ErrExternal):
// return status.Error(codes.Unavailable, err.Error())
// case errors.Is(err, utils.ErrNoPermission):
// return status.Error(codes.PermissionDenied, err.Error())
// case errors.Is(err, utils.ErrUnknown):
// return status.Error(codes.Unknown, err.Error())
// case errors.Is(err, utils.ErrDeadlineExceeded):
// return status.Error(codes.DeadlineExceeded, err.Error())
// case errors.Is(err, utils.ErrNotFound):
// return status.Error(codes.NotFound, err.Error())
// case errors.Is(err, utils.ErrAlreadyExists):
// return status.Error(codes.AlreadyExists, err.Error())
// case errors.Is(err, utils.ErrConflict):
// return status.Error(codes.Unimplemented, err.Error())
// case errors.Is(err, utils.ErrUnimplemented):
// return status.Error(codes.Unimplemented, err.Error())
// case errors.Is(err, utils.ErrUnauthenticated):
// return status.Error(codes.Unauthenticated, err.Error())
// default:
// return status.Error(codes.Unknown, err.Error())
// }
//}
//
//func (s *TesterServer) ErrUnwrappingUnaryInterceptor() grpc.UnaryServerInterceptor {
// return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// resp, err := handler(ctx, req)
// return resp, ToGrpcError(err)
// }
//}
//
//func (s *TesterServer) ErrUnwrappingStreamInterceptor() grpc.StreamServerInterceptor {
// return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// err := handler(server, ss)
// return ToGrpcError(err)
// }
//}

View file

@ -3,16 +3,16 @@ package models
import "time"
type Problem struct {
Id *int32 `db:"id"`
Title *string `db:"title"`
Legend *string `db:"legend"`
InputFormat *string `db:"input_format"`
OutputFormat *string `db:"output_format"`
Notes *string `db:"notes"`
Tutorial *string `db:"tutorial"`
LatexSummary *string `db:"latex_summary"`
TimeLimit *int32 `db:"time_limit"`
MemoryLimit *int32 `db:"memory_limit"`
CreatedAt *time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
Id int32 `db:"id"`
Title string `db:"title"`
Legend string `db:"legend"`
InputFormat string `db:"input_format"`
OutputFormat string `db:"output_format"`
Notes string `db:"notes"`
Tutorial string `db:"tutorial"`
LatexSummary string `db:"latex_summary"`
TimeLimit int32 `db:"time_limit"`
MemoryLimit int32 `db:"memory_limit"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

162
internal/models/session.go Normal file
View file

@ -0,0 +1,162 @@
package models
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/open-policy-agent/opa/v1/rego"
)
type JWT struct {
SessionId string `json:"session_id"`
UserId int32 `json:"user_id"`
Role Role `json:"role"`
ExpiresAt int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
NotBefore int64 `json:"nbf"`
Permissions []grant `json:"permissions"`
}
func (j JWT) Valid() error {
if uuid.Validate(j.SessionId) != nil {
return errors.New("invalid session id")
}
if j.UserId == 0 {
return errors.New("empty user id")
}
if j.ExpiresAt == 0 {
return errors.New("empty expires at")
}
if j.IssuedAt == 0 {
return errors.New("empty issued at")
}
if j.NotBefore == 0 {
return errors.New("empty not before")
}
if len(j.Permissions) == 0 {
return errors.New("empty permissions")
}
return nil
}
type Role int32
const (
RoleGuest Role = -1
RoleStudent Role = 0
RoleTeacher Role = 1
RoleAdmin Role = 2
)
func (r Role) String() string {
switch r {
case RoleGuest:
return "guest"
case RoleStudent:
return "student"
case RoleTeacher:
return "teacher"
case RoleAdmin:
return "admin"
}
panic("invalid role")
}
type Action string
const (
Create Action = "create"
Read Action = "read"
Update Action = "update"
Delete Action = "delete"
)
type Resource string
const (
ResourceAnotherUser Resource = "another-user"
ResourceMeUser Resource = "me-user"
ResourceListUser Resource = "list-user"
ResourceOwnSession Resource = "own-session"
)
type grant struct {
Action Action `json:"action"`
Resource Resource `json:"resource"`
}
var Grants = map[string][]grant{
RoleGuest.String(): {},
RoleStudent.String(): {
{Read, ResourceAnotherUser},
{Read, ResourceMeUser},
{Update, ResourceOwnSession},
{Delete, ResourceOwnSession},
},
RoleTeacher.String(): {
{Create, ResourceAnotherUser},
{Read, ResourceAnotherUser},
{Read, ResourceMeUser},
{Read, ResourceListUser},
{Update, ResourceOwnSession},
{Delete, ResourceOwnSession},
},
RoleAdmin.String(): {
{Create, ResourceAnotherUser},
{Read, ResourceAnotherUser},
{Read, ResourceMeUser},
{Read, ResourceListUser},
{Update, ResourceAnotherUser},
{Update, ResourceOwnSession},
{Delete, ResourceAnotherUser},
{Delete, ResourceOwnSession},
},
}
const module = `package app.rbac
default allow := false
allow if {
some grant in input.role_grants[input.role]
input.action == grant.action
input.resource == grant.resource
}
`
var query rego.PreparedEvalQuery
func (r Role) HasPermission(action Action, resource Resource) bool {
ctx := context.TODO()
input := map[string]interface{}{
"action": action,
"resource": resource,
"role": r.String(),
"role_grants": Grants,
}
results, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
panic(err)
}
return results.Allowed()
}
func init() {
var err error
ctx := context.TODO()
query, err = rego.New(
rego.Query("data.app.rbac.allow"),
rego.Module("ms-auth.rego", module),
).PrepareForEval(ctx)
if err != nil {
panic(err)
}
}

View file

@ -1,13 +1,17 @@
package models
//type Solution struct {
// Id *int32 `db:"id"`
// TaskId *int32 `db:"task_id"`
// ParticipantId *int32 `db:"participant_id"`
// State *int32 `db:"state"`
// Score *int32 `db:"score"`
// Penalty *int32 `db:"penalty"`
// TotalScore *int32 `db:"total_score"`
// Language *int32 `db:"language"`
// CreatedAt *time.Time `db:"created_at"`
//}
import "time"
type Solution struct {
Id int32 `db:"id"`
TaskId int32 `db:"task_id"`
ParticipantId int32 `db:"participant_id"`
Solution string `db:"solution"`
State int32 `db:"state"`
Score int32 `db:"score"`
Penalty int32 `db:"penalty"`
TotalScore int32 `db:"total_score"`
Language int32 `db:"language"`
UpdatedAt time.Time `db:"updated_at"`
CreatedAt time.Time `db:"created_at"`
}

View file

@ -1,10 +1,24 @@
package models
//type Task struct {
// Id *int32 `db:"id"`
// ProblemId *int32 `db:"problem_id"`
// ContestId *int32 `db:"contest_id"`
// Position *int32 `db:"position"`
// CreatedAt *time.Time `db:"created_at"`
// UpdatedAt *time.Time `db:"updated_at"`
//}
import "time"
type Task struct {
Id int32 `db:"id"`
ProblemId int32 `db:"problem_id"`
ContestId int32 `db:"contest_id"`
Position int32 `db:"position"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type RichTask struct {
Id int32 `db:"id"`
ProblemId int32 `db:"problem_id"`
ContestId int32 `db:"contest_id"`
Position int32 `db:"position"`
Title string `db:"title"`
MemoryLimit int32 `db:"memory_limit"`
TimeLimit int32 `db:"time_limit"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

View file

@ -53,14 +53,49 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetContestResponse{
//token, ok := c.Locals(TokenKey).(*models.JWT)
//if !ok {
// return c.SendStatus(fiber.StatusUnauthorized)
//}
tasks, err := h.contestsUC.ReadRichTasks(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.GetContestResponse{
Contest: testerv1.Contest{
Id: *contest.Id,
Id: id,
Title: *contest.Title,
CreatedAt: *contest.CreatedAt,
UpdatedAt: *contest.UpdatedAt,
},
})
Tasks: make([]struct {
BestSolution testerv1.BestSolution `json:"best_solution"`
Task testerv1.RichTask `json:"task"`
}, len(tasks)),
}
for i, task := range tasks {
resp.Tasks[i] = struct {
BestSolution testerv1.BestSolution `json:"best_solution"`
Task testerv1.RichTask `json:"task"`
}{
BestSolution: testerv1.BestSolution{},
Task: testerv1.RichTask{
Id: task.Id,
ProblemId: task.ProblemId,
Position: task.Position,
Title: task.Title,
MemoryLimit: task.MemoryLimit,
TimeLimit: task.TimeLimit,
CreatedAt: task.CreatedAt,
UpdatedAt: task.UpdatedAt,
},
}
}
return c.JSON(resp)
}
func (h *TesterHandlers) DeleteParticipant(c *fiber.Ctx, id int32, params testerv1.DeleteParticipantParams) error {
@ -130,17 +165,17 @@ func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error {
return c.JSON(
testerv1.GetProblemResponse{Problem: testerv1.Problem{
Id: *problem.Id,
Legend: *problem.Legend,
InputFormat: *problem.InputFormat,
OutputFormat: *problem.OutputFormat,
Notes: *problem.Notes,
Tutorial: *problem.Tutorial,
LatexSummary: *problem.LatexSummary,
TimeLimit: *problem.TimeLimit,
MemoryLimit: *problem.MemoryLimit,
CreatedAt: *problem.CreatedAt,
UpdatedAt: *problem.UpdatedAt,
Id: problem.Id,
Legend: problem.Legend,
InputFormat: problem.InputFormat,
OutputFormat: problem.OutputFormat,
Notes: problem.Notes,
Tutorial: problem.Tutorial,
LatexSummary: problem.LatexSummary,
TimeLimit: problem.TimeLimit,
MemoryLimit: problem.MemoryLimit,
CreatedAt: problem.CreatedAt,
UpdatedAt: problem.UpdatedAt,
}},
)
}

View file

@ -0,0 +1,71 @@
package rest
import (
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
"strings"
)
const (
TokenKey = "token"
)
func AuthMiddleware(jwtSecret string) fiber.Handler {
return func(c *fiber.Ctx) error {
const op = "AuthMiddleware"
authHeader := c.Get("Authorization", "")
if authHeader == "" {
c.Locals(TokenKey, nil)
return c.Next()
}
authParts := strings.Split(authHeader, " ")
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
c.Locals(TokenKey, nil)
return c.Next()
}
parsedToken, err := jwt.ParseWithClaims(authParts[1], &models.JWT{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(jwtSecret), nil
})
if err != nil {
c.Locals(TokenKey, nil)
return c.Next()
}
token, ok := parsedToken.Claims.(*models.JWT)
if !ok {
c.Locals(TokenKey, nil)
return c.Next()
}
err = token.Valid()
if err != nil {
c.Locals(TokenKey, nil)
return c.Next()
}
//ctx := c.Context()
// check if session exists
//_, err = userUC.ReadSession(ctx, token.SessionId)
//if err != nil {
// if errors.Is(err, pkg.ErrNotFound) {
// c.Locals(TokenKey, nil)
// return c.Next()
// }
//
// return c.SendStatus(pkg.ToREST(err))
//}
c.Locals(TokenKey, token)
return c.Next()
}
}

View file

@ -19,4 +19,5 @@ type ContestRepository interface {
DeleteTask(ctx context.Context, taskId int32) error
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
DeleteParticipant(ctx context.Context, participantId int32) error
ReadRichTasks(ctx context.Context, contestId int32) ([]*models.RichTask, error)
}

View file

@ -4,18 +4,15 @@ import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type ContestRepository struct {
db *sqlx.DB
logger *zap.Logger
db *sqlx.DB
}
func NewContestRepository(db *sqlx.DB, logger *zap.Logger) *ContestRepository {
func NewContestRepository(db *sqlx.DB) *ContestRepository {
return &ContestRepository{
db: db,
logger: logger,
db: db,
}
}
@ -136,3 +133,28 @@ func (r *ContestRepository) DeleteParticipant(ctx context.Context, participantId
}
return nil
}
const readTasksQuery = `SELECT tasks.id,
problem_id,
contest_id,
position,
title,
memory_limit,
time_limit,
tasks.created_at,
tasks.updated_at
FROM tasks
INNER JOIN problems ON tasks.problem_id = problems.id
WHERE contest_id = ? ORDER BY position`
func (r *ContestRepository) ReadRichTasks(ctx context.Context, contestId int32) ([]*models.RichTask, error) {
const op = "ContestRepository.ReadTasks"
var tasks []*models.RichTask
query := r.db.Rebind(readTasksQuery)
err := r.db.SelectContext(ctx, &tasks, query, contestId)
if err != nil {
return nil, handlePgErr(err, op)
}
return tasks, nil
}

View file

@ -4,18 +4,17 @@ import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type ProblemRepository struct {
db *sqlx.DB
logger *zap.Logger
db *sqlx.DB
//logger *zap.Logger
}
func NewProblemRepository(db *sqlx.DB, logger *zap.Logger) *ProblemRepository {
func NewProblemRepository(db *sqlx.DB) *ProblemRepository {
return &ProblemRepository{
db: db,
logger: logger,
db: db,
//logger: logger,
}
}

View file

@ -19,4 +19,5 @@ type ContestUseCase interface {
DeleteTask(ctx context.Context, taskId int32) error
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
DeleteParticipant(ctx context.Context, participantId int32) error
ReadRichTasks(ctx context.Context, contestId int32) ([]*models.RichTask, error)
}

View file

@ -45,3 +45,7 @@ func (uc *ContestUseCase) AddParticipant(ctx context.Context, contestId int32, u
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
return uc.contestRepo.DeleteParticipant(ctx, participantId)
}
func (uc *ContestUseCase) ReadRichTasks(ctx context.Context, contestId int32) ([]*models.RichTask, error) {
return uc.contestRepo.ReadRichTasks(ctx, contestId)
}