From 5e86cf19a37d61f5eb6fa50ec4a334175d761e44 Mon Sep 17 00:00:00 2001 From: dragonmuffin Date: Sat, 17 Aug 2024 17:23:35 +0500 Subject: [PATCH] feat: add solution storage --- internal/models/participant.go | 10 ++ internal/models/result.go | 22 +++ internal/models/solution.go | 2 +- internal/storage/errhandling.go | 27 ++++ internal/storage/solution.go | 77 +++++++++ internal/storage/tester.go | 267 -------------------------------- 6 files changed, 137 insertions(+), 268 deletions(-) create mode 100644 internal/models/participant.go create mode 100644 internal/models/result.go create mode 100644 internal/storage/errhandling.go create mode 100644 internal/storage/solution.go delete mode 100644 internal/storage/tester.go diff --git a/internal/models/participant.go b/internal/models/participant.go new file mode 100644 index 0000000..fd68947 --- /dev/null +++ b/internal/models/participant.go @@ -0,0 +1,10 @@ +package models + +import "time" + +type Solution struct { + Id *int32 `db:"id"` + UserId *int32 `db:"user_id"` + ContestId *int32 `db:"contest_id"` + Name *string `db:"name"` +} diff --git a/internal/models/result.go b/internal/models/result.go new file mode 100644 index 0000000..c608d78 --- /dev/null +++ b/internal/models/result.go @@ -0,0 +1,22 @@ +package models + +import "time" + +type Result int32 + +const ( + NotTested Result = 0 + Accepted Result = 1 + TimeLimitExceeded Result = 2 + MemoryLimitExceeded Result = 3 + CompilationError Result = 4 + SystemFailDuringTesting Result = 5 +) + +func (result Result) Valid() error { + switch result { + case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceede, CompilationError, SystemFailDuringTesting: + return nil + } + return lib.ErrBadResult +} diff --git a/internal/models/solution.go b/internal/models/solution.go index aceae6c..0efa6e2 100644 --- a/internal/models/solution.go +++ b/internal/models/solution.go @@ -5,7 +5,7 @@ import "time" type Solution struct { Id *int32 `db:"id"` ParticipantId *int32 `db:"participant_id"` - ProblemId *int32 `db:"problem_id"` + TaskId *int32 `db:"task_id"` LanguageId *int32 `db:"language_id"` SolutionHash *string `db:"solution_hash"` Result *int32 `db:"result"` diff --git a/internal/storage/errhandling.go b/internal/storage/errhandling.go new file mode 100644 index 0000000..2104940 --- /dev/null +++ b/internal/storage/errhandling.go @@ -0,0 +1,27 @@ +package storage + +import ( + "context" + "errors" + "git.sch9.ru/new_gate/ms-auth/internal/lib" + "git.sch9.ru/new_gate/ms-auth/internal/models" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + "time" +) + +func (storage *UserStorage) HandlePgErr(err error) error { + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + storage.logger.DPanic("unexpected error from postgres", zap.String("err", err.Error())) + return lib.ErrUnexpected + } + if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { + return errors.New("unique key violation") // FIXME + } + storage.logger.DPanic("unexpected internal error from postgres", zap.String("err", err.Error())) + return lib.ErrInternal +} + diff --git a/internal/storage/solution.go b/internal/storage/solution.go new file mode 100644 index 0000000..b3d2b01 --- /dev/null +++ b/internal/storage/solution.go @@ -0,0 +1,77 @@ +package storage + +import ( + "context" + "errors" + "git.sch9.ru/new_gate/ms-auth/internal/lib" + "git.sch9.ru/new_gate/ms-auth/internal/models" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" + "time" +) + +type SolutionStorage struct { + db *sqlx.DB + logger *zap.Logger +} + +func NewSolutionStorage(db *sqlx.DB, logger *zap.Logger) *SolutionStorage { + return &SolutionStorage{ + db: db, + logger: logger, + } +} + +func (storage *SolutionStorage) CreateSolution(ctx context.Context, solution *models.Solution) (int32, error) { + query := storage.db.Rebind(` +INSERT INTO solutions + (participant_id,task_id,language_id,solution_hash,result) +VALUES (?, ?, ?, ?, ?) +RETURNING id +`) + + rows, err := storage.db.QueryxContext( + ctx, + query, + solution.ParticipantId, + solution.TaskId, + solution.LanguageId, + "",//FIXME + models.NotTested, + ) + if err != nil { + return 0, storage.HandlePgErr(err) + } + + defer rows.Close() + var id int32 + err = rows.StructScan(&id) + if err != nil { + return 0, storage.HandlePgErr(err) + } + + return id, nil +} + +func (storage *SolutionStorage) ReadSolutionById(ctx context.Context, id int32) (*models.Solution, error) { + var solution models.Solution + query := storage.db.Rebind("SELECT * from solutions WHERE id=? LIMIT 1") + err := storage.db.GetContext(ctx, &solution, query, id) + if err != nil { + return nil, storage.HandlePgErr(err) + } + return &solution, nil +} + +func (storage *SolutionStorage) DeleteSolution(ctx context.Context, id int32) error { + query := storage.db.Rebind("UPDATE solutions SET expired_at=now() WHERE id = ?") + _, err := storage.db.ExecContext(ctx, query, id) + if err != nil { + return storage.HandlePgErr(err) + } + + return nil +} + diff --git a/internal/storage/tester.go b/internal/storage/tester.go deleted file mode 100644 index 00d8194..0000000 --- a/internal/storage/tester.go +++ /dev/null @@ -1,267 +0,0 @@ -package storage - -import ( - "context" - "errors" - "git.sch9.ru/new_gate/ms-tester/internal/lib" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v5/pgconn" - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" - "strings" - "time" - - "github.com/jmoiron/sqlx" -) - -type PostgresqlStorage struct { - db *sqlx.DB - logger *zap.Logger -} - -func NewUserStorage(dsn string, logger *zap.Logger) *PostgresqlStorage { - db, err := sqlx.Connect("pgx", dsn) - if err != nil { - panic(err.Error()) - } - - return &PostgresqlStorage{db: db, logger: logger} -} - -func (storage *PostgresqlStorage) Stop() error { - return storage.db.Close() -} - -const ( - shortUserLifetime = time.Hour * 24 * 30 - defaultUserLifetime = time.Hour * 24 * 365 * 100 -) - -type User struct { - Id int32 `db:"id"` - - Username string `db:"username"` - HashedPassword [60]byte `db:"hashed_pwd"` - - Email *string `db:"email"` - - ExpiresAt time.Time `db:"expires_at"` - CreatedAt time.Time `db:"created_at"` - - Role int32 `db:"role"` -} - -func (user *User) IsAdmin() bool { - return lib.IsAdmin(user.Role) -} - -func (user *User) IsModerator() bool { - return lib.IsModerator(user.Role) -} - -func (user *User) IsParticipant() bool { - return lib.IsParticipant(user.Role) -} - -func (user *User) IsSpectator() bool { - return lib.IsSpectator(user.Role) -} - -func (user *User) AtLeast(role int32) bool { - return user.Role >= role -} - -func (user *User) ComparePassword(password string) error { - if bcrypt.CompareHashAndPassword(user.HashedPassword[:], []byte(password)) != nil { - return lib.ErrBadHandleOrPassword - } - return nil -} - -func (storage *PostgresqlStorage) CreateUser( - ctx context.Context, - username string, - password string, - email *string, - expiresAt *time.Time, - role *int32, -) (*int32, error) { - if err := lib.ValidUsername(username); err != nil { - return nil, err - } - if err := lib.ValidPassword(password); err != nil { - return nil, err - } - if email != nil { - if err := lib.ValidEmail(*email); err != nil { - return nil, err - } - } - if role != nil { - if err := lib.ValidRole(*role); err != nil { - return nil, err - } - } - - username = strings.ToLower(username) - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - storage.logger.Error(err.Error()) - return nil, lib.ErrInternal - } - now := time.Now() - username = strings.ToLower(username) - if email != nil { - *email = strings.ToLower(*email) - } - if role == nil { - role = lib.AsInt32P(lib.RoleSpectator) - } - if expiresAt == nil { - if email == nil { - expiresAt = lib.AsTimeP(now.Add(shortUserLifetime)) - } else { - expiresAt = lib.AsTimeP(now.Add(defaultUserLifetime)) - } - } - - query := storage.db.Rebind(` -INSERT INTO users - (username, hashed_pwd, email, expires_at, role) -VALUES (?, ?, ?, ?, ?) -RETURNING id -`) - - rows, err := storage.db.QueryxContext(ctx, query, username, hashedPassword, email, expiresAt, role) - if err != nil { - return nil, storage.handlePgErr(err) - } - defer rows.Close() - var id int32 - err = rows.StructScan(&id) - if err != nil { - return nil, storage.handlePgErr(err) - } - return &id, nil -} -func (storage *PostgresqlStorage) ReadUserByEmail(ctx context.Context, email string) (*User, error) { - if err := lib.ValidEmail(email); err != nil { - return nil, err - } - - email = strings.ToLower(email) - - var user User - query := storage.db.Rebind("SELECT * from users WHERE email=? LIMIT 1") - err := storage.db.GetContext(ctx, &user, query, email) - if err != nil { - return nil, storage.handlePgErr(err) - } - return &user, nil -} -func (storage *PostgresqlStorage) ReadUserByUsername(ctx context.Context, username string) (*User, error) { - if err := lib.ValidUsername(username); err != nil { - return nil, err - } - - username = strings.ToLower(username) - - var user User - query := storage.db.Rebind("SELECT * from users WHERE username=? LIMIT 1") - err := storage.db.GetContext(ctx, &user, query, username) - if err != nil { - return nil, storage.handlePgErr(err) - } - return &user, nil -} -func (storage *PostgresqlStorage) ReadUserById(ctx context.Context, id int32) (*User, error) { - var user User - query := storage.db.Rebind("SELECT * from users WHERE id=? LIMIT 1") - err := storage.db.GetContext(ctx, &user, query, id) - if err != nil { - return nil, storage.handlePgErr(err) - } - return &user, nil -} - -func (storage *PostgresqlStorage) UpdateUser( - ctx context.Context, - id int32, - username *string, - password *string, - email *string, - expiresAt *time.Time, - role *int32, -) error { - var err error - if username != nil { - if err = lib.ValidUsername(*username); err != nil { - return err - } - } - var hashedPassword []byte - if password != nil { - if err = lib.ValidPassword(*password); err != nil { - return err - } - hashedPassword, err = bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost) - if err != nil { - storage.logger.Error(err.Error()) - return lib.ErrInternal - } - } - if email != nil { - if err = lib.ValidEmail(*email); err != nil { - return err - } - } - if role != nil { - if err = lib.ValidRole(*role); err != nil { - return err - } - } - - if username != nil { - *username = strings.ToLower(*username) - } - if email != nil { - *email = strings.ToLower(*email) - } - - query := storage.db.Rebind(` -UPDATE users -SET username = COALESCE(?, username), - hashed_pwd = COALESCE(?, hashed_pwd), - email = COALESCE(?, email), - expires_at = COALESCE(?, expires_at), - role = COALESCE(?, role) -WHERE id = ?`) - - _, err = storage.db.ExecContext(ctx, query, username, hashedPassword, email, expiresAt, role, id) - if err != nil { - return storage.handlePgErr(err) - } - return nil -} -func (storage *PostgresqlStorage) DeleteUser(ctx context.Context, id int32) error { - query := storage.db.Rebind("UPDATE users SET expired_at=NOW() WHERE id = ?") - _, err := storage.db.ExecContext(ctx, query, id) - if err != nil { - return storage.handlePgErr(err) - } - - return nil -} - -func (storage *PostgresqlStorage) handlePgErr(err error) error { - var pgErr *pgconn.PgError - if !errors.As(err, &pgErr) { - storage.logger.DPanic("unexpected error from postgres", zap.String("err", err.Error())) - return lib.ErrUnexpected - } - if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { - return errors.New("unique key violation") // FIXME - } - storage.logger.DPanic("unexpected internal error from postgres", zap.String("err", err.Error())) - return lib.ErrInternal -}