Compare commits
No commits in common. "develop" and "v0.0.0" have entirely different histories.
110 changed files with 2396 additions and 6217 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
.env
|
||||
.idea
|
||||
/pkg/go/gen
|
||||
/ms-tester
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,4 +1,3 @@
|
|||
[submodule "proto"]
|
||||
path = contracts
|
||||
path = proto
|
||||
url = https://git.sch9.ru/new_gate/contracts
|
||||
update = rebase
|
||||
|
|
14
Makefile
14
Makefile
|
@ -1,9 +1,9 @@
|
|||
tag = latest
|
||||
|
||||
gen:
|
||||
@oapi-codegen --config=config.yaml ./contracts/tester/v1/openapi.yaml
|
||||
dev: gen
|
||||
@buf generate
|
||||
dev:
|
||||
@make gen
|
||||
@go run main.go
|
||||
build: gen
|
||||
@docker build . -t ms-tester:${tag}
|
||||
@#docker push ms-tester:${tag}
|
||||
|
||||
build:
|
||||
@make gen
|
||||
# TODO: build dockerfile
|
||||
|
|
108
README.md
108
README.md
|
@ -1,108 +0,0 @@
|
|||
# ms-tester
|
||||
|
||||
`ms-tester` is a microservice designed for managing programming competitions. It provides backend functionality for handling problems, contests, participants, and their submissions. The service is developed in Go. PostgreSQL serves as the relational database. Pandoc is used to convert problem statements from LaTeX to HTML.
|
||||
|
||||
For understanding the architecture, see the [documentation](https://git.sch9.ru/new_gate/docs).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following dependencies installed:
|
||||
|
||||
* **Docker** and **Docker Compose**: To run PostgreSQL, Pandoc.
|
||||
* **Goose**: For applying database migrations (`go install github.com/pressly/goose/v3/cmd/goose@latest`).
|
||||
* **oapi-codegen**: For generating OpenAPI code (`go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest`).
|
||||
|
||||
### 1. Running Dependencies
|
||||
|
||||
You can run PostgreSQL and Pandoc using Docker Compose, for example:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
pandoc:
|
||||
image: pandoc/latex
|
||||
ports:
|
||||
- "4000:3030" # Exposes Pandoc server on port 4000 locally
|
||||
command: "server" # Runs Pandoc in server mode
|
||||
postgres:
|
||||
image: postgres:14.1-alpine # Uses PostgreSQL 14.1 Alpine image
|
||||
restart: always # Ensures the container restarts if it stops
|
||||
environment:
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: supersecretpassword # Default password (change for production!)
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
ports:
|
||||
- '5432:5432' # Exposes PostgreSQL on the standard port 5432
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data # Persists database data locally
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -d postgres # Command to check if PostgreSQL is ready
|
||||
interval: 10s # Check every 10 seconds
|
||||
timeout: 3s # Wait 3 seconds for the check to respond
|
||||
retries: 5 # Try 5 times before marking as unhealthy
|
||||
volumes:
|
||||
postgres-data: # Defines the named volume for data persistence
|
||||
```
|
||||
|
||||
Start the services in detached mode:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
The application uses environment variables for configuration. Create a `.env` file in the project root. The minimum required variables are:
|
||||
|
||||
```dotenv
|
||||
# Environment type (development or production)
|
||||
ENV=dev # or prod
|
||||
|
||||
# Address of the running Pandoc service
|
||||
PANDOC=http://localhost:4000
|
||||
|
||||
# Address and port where the ms-tester service will listen
|
||||
ADDRESS=localhost:8080
|
||||
|
||||
# PostgreSQL connection string (Data Source Name)
|
||||
# Format: postgres://user:password@host:port/database?sslmode=disable
|
||||
POSTGRES_DSN=postgres://username:supersecretpassword@localhost:5432/db_name?sslmode=disable
|
||||
|
||||
# Secret key for signing and verifying JWT tokens
|
||||
JWT_SECRET=your_super_secret_jwt_key
|
||||
```
|
||||
|
||||
**Important:** Replace `supersecretpassword` and `your_super_secret_jwt_key` with secure, unique values, especially for a production environment.
|
||||
|
||||
### 3. Database Migrations
|
||||
|
||||
The project uses `goose` to manage the database schema.
|
||||
|
||||
1. Ensure `goose` is installed:
|
||||
```bash
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
```
|
||||
2. Apply the migrations to the running PostgreSQL database. Make sure the connection string in the command matches the `POSTGRES_DSN` from your `.env` file:
|
||||
```bash
|
||||
goose -dir ./migrations postgres "postgres://postgres:supersecretpassword@localhost:5432/postgres?sslmode=disable" up
|
||||
```
|
||||
|
||||
### 4. OpenAPI Code Generation
|
||||
|
||||
The project uses OpenAPI to define its API. Go code for handlers and models is generated based on this specification using `oapi-codegen`.
|
||||
|
||||
Run the generation command:
|
||||
|
||||
```bash
|
||||
make gen
|
||||
```
|
||||
|
||||
### 5. Running the Application
|
||||
|
||||
Start the `ms-tester` service:
|
||||
|
||||
```bash
|
||||
go run ./main.go
|
||||
```
|
||||
|
||||
After starting, the service will be available at the address specified in the `ADDRESS` variable in your `.env` file (e.g., `http://localhost:8080`).
|
12
buf.gen.yaml
Normal file
12
buf.gen.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
version: v1
|
||||
managed:
|
||||
enabled: true
|
||||
go_package_prefix:
|
||||
default: git.sch9.ru/new_gate/ms-tester/pkg/go/gen
|
||||
plugins:
|
||||
- name: go
|
||||
out: pkg/go/gen
|
||||
opt: paths=source_relative
|
||||
- name: go-grpc
|
||||
out: pkg/go/gen
|
||||
opt: paths=source_relative
|
7
buf.yaml
Normal file
7
buf.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
version: v1
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
|
@ -1,5 +0,0 @@
|
|||
package: testerv1
|
||||
generate:
|
||||
fiber-server: true
|
||||
models: true
|
||||
output: ./contracts/tester/v1/tester.go
|
|
@ -1,22 +0,0 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
|
||||
Pandoc string `env:"PANDOC" required:"true"`
|
||||
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
|
||||
RedisDSN string `env:"REDIS_DSN" required:"true"`
|
||||
|
||||
JWTSecret string `env:"JWT_SECRET" required:"true"`
|
||||
|
||||
AdminUsername string `env:"ADMIN_USERNAME" env-default:"admin"`
|
||||
AdminPassword string `env:"ADMIN_PASSWORD" env-default:"admin"`
|
||||
|
||||
//RabbitDSN string `env:"RABBIT_DSN" required:"true"`
|
||||
//InstanceName string `env:"INSTANCE_NAME" required:"true"`
|
||||
//RQueueName string `env:"R_QUEUE_NAME" required:"true"`
|
||||
//TQueueName string `env:"T_QUEUE_NAME" required:"true"`
|
||||
//NQueueName string `env:"N_QUEUE_NAME" required:"true"`
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 91b7c6804671bcd533ab09939f21d27aacd5f793
|
83
go.mod
83
go.mod
|
@ -1,59 +1,60 @@
|
|||
module git.sch9.ru/new_gate/ms-tester
|
||||
|
||||
go 1.23.6
|
||||
go 1.21.3
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/valkey-io/valkey-go v1.0.57
|
||||
github.com/valkey-io/valkey-go/mock v1.0.57
|
||||
go.uber.org/mock v0.5.1
|
||||
github.com/open-policy-agent/opa v0.67.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
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/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
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
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/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
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
237
go.sum
237
go.sum
|
@ -1,139 +1,190 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
|
||||
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/open-policy-agent/opa v0.67.1 h1:rzy26J6g1X+CKknAcx0Vfbt41KqjuSzx4E0A8DAZf3E=
|
||||
github.com/open-policy-agent/opa v0.67.1/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valkey-io/valkey-go v1.0.57 h1:rMpREZ7kvWwv9vHkB1WTpI9rX4dQHsvPHimSWenScvI=
|
||||
github.com/valkey-io/valkey-go v1.0.57/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM=
|
||||
github.com/valkey-io/valkey-go/mock v1.0.57 h1:ft06MuqCCKlob/R5dzUv4zNnNu+GaqElalApOFS5Fc4=
|
||||
github.com/valkey-io/valkey-go/mock v1.0.57/go.mod h1:VDiXrmHdRCz/UT4xzMkfQEc5iHa7naDpqsZ+lotmJE8=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
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/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=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
|
||||
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
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/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=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AuthHandlers interface {
|
||||
ListSessions(c *fiber.Ctx) error
|
||||
Terminate(c *fiber.Ctx) error
|
||||
Login(c *fiber.Ctx) error
|
||||
Logout(c *fiber.Ctx) error
|
||||
Refresh(c *fiber.Ctx) error
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/auth"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
authUC auth.UseCase
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewHandlers(authUC auth.UseCase, jwtSecret string) *Handlers {
|
||||
return &Handlers{
|
||||
authUC: authUC,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
sessionKey = "session"
|
||||
)
|
||||
|
||||
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
|
||||
const op = "sessionFromCtx"
|
||||
|
||||
session, ok := ctx.Value(sessionKey).(*models.Session)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *Handlers) ListSessions(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *Handlers) Terminate(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Terminate(ctx, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Login(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
username, pwd, err := parseBasicAuth(authHeader)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
credentials := &models.Credentials{
|
||||
Username: strings.ToLower(username),
|
||||
Password: pwd,
|
||||
}
|
||||
device := &models.Device{
|
||||
Ip: c.IP(),
|
||||
UseAgent: c.Get("User-Agent", ""),
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := h.authUC.Login(ctx, credentials, device)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{
|
||||
SessionId: session.Id,
|
||||
UserId: session.UserId,
|
||||
Role: session.Role,
|
||||
IssuedAt: time.Now().Unix(),
|
||||
})
|
||||
|
||||
token, err := claims.SignedString([]byte(h.jwtSecret))
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
c.Set("Authorization", "Bearer "+token)
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Logout(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Logout(c.Context(), session.Id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Refresh(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Refresh(c.Context(), session.Id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func parseBasicAuth(header string) (string, string, error) {
|
||||
const (
|
||||
op = "parseBasicAuth"
|
||||
msg = "invalid auth header"
|
||||
)
|
||||
|
||||
authParts := strings.Split(header, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1])
|
||||
if err != nil {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
authParts = strings.Split(string(decodedAuth), ":")
|
||||
if len(authParts) != 2 {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
return authParts[0], authParts[1], nil
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error)
|
||||
Refresh(ctx context.Context, sessionId string) error
|
||||
Logout(ctx context.Context, sessionId string) error
|
||||
Terminate(ctx context.Context, userId int32) error
|
||||
ListSessions(ctx context.Context, userId int32) ([]*models.Session, error)
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UseCase struct {
|
||||
usersUC users.UseCase
|
||||
sessionsUC sessions.UseCase
|
||||
}
|
||||
|
||||
func NewUseCase(usersUC users.UseCase, sessionsUC sessions.UseCase) *UseCase {
|
||||
return &UseCase{
|
||||
usersUC: usersUC,
|
||||
sessionsUC: sessionsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UseCase) Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error) {
|
||||
const op = "UseCase.Login"
|
||||
|
||||
user, err := uc.usersUC.ReadUserByUsername(ctx, credentials.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.IsSamePwd(credentials.Password) {
|
||||
return nil, pkg.Wrap(pkg.ErrNotFound, nil, op, "password mismatch")
|
||||
}
|
||||
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: user.Id,
|
||||
Role: user.Role,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(40 * time.Minute),
|
||||
UserAgent: device.UseAgent,
|
||||
Ip: device.Ip,
|
||||
}
|
||||
|
||||
err = uc.sessionsUC.CreateSession(ctx, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (uc *UseCase) Logout(ctx context.Context, sessionId string) error {
|
||||
return uc.sessionsUC.DeleteSession(ctx, sessionId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) Refresh(ctx context.Context, sessionId string) error {
|
||||
return uc.sessionsUC.UpdateSession(ctx, sessionId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) Terminate(ctx context.Context, userId int32) error {
|
||||
return uc.sessionsUC.DeleteAllSessions(ctx, userId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) ListSessions(ctx context.Context, userId int32) ([]*models.Session, error) {
|
||||
// TODO: implement me
|
||||
panic("implement me")
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package contests
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ContestsHandlers interface {
|
||||
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
|
||||
CreateContest(c *fiber.Ctx) error
|
||||
DeleteContest(c *fiber.Ctx, id int32) error
|
||||
GetContest(c *fiber.Ctx, id int32) error
|
||||
UpdateContest(c *fiber.Ctx, id int32) error
|
||||
DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error
|
||||
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
|
||||
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
|
||||
CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error
|
||||
ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error
|
||||
CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error
|
||||
GetSolution(c *fiber.Ctx, id int32) error
|
||||
DeleteTask(c *fiber.Ctx, id int32) error
|
||||
CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error
|
||||
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
|
||||
GetTask(c *fiber.Ctx, id int32) error
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
problemsUC problems.UseCase
|
||||
contestsUC contests.UseCase
|
||||
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewHandlers(problemsUC problems.UseCase, contestsUC contests.UseCase, jwtSecret string) *Handlers {
|
||||
return &Handlers{
|
||||
problemsUC: problemsUC,
|
||||
contestsUC: contestsUC,
|
||||
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
sessionKey = "session"
|
||||
)
|
||||
|
||||
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
|
||||
const op = "sessionFromCtx"
|
||||
|
||||
session, ok := ctx.Value(sessionKey).(*models.Session)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateContest(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
id, err := h.contestsUC.CreateContest(ctx, "Название контеста")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(&testerv1.CreateContestResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetContest(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
contest, err := h.contestsUC.GetContest(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
solutions := make([]*models.SolutionsListItem, 0)
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
|
||||
if err == nil { // Admin or Teacher may not participate in contest
|
||||
solutions, err = h.contestsUC.GetBestSolutions(ctx, id, participantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
|
||||
case models.RoleStudent:
|
||||
contest, err := h.contestsUC.GetContest(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
|
||||
solutions, err := h.contestsUC.GetBestSolutions(c.Context(), id, participantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateContest(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req testerv1.UpdateContestRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateContest(ctx, id, models.ContestUpdate{
|
||||
Title: req.Title,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteContest(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
err := h.contestsUC.DeleteContest(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
filter := models.ContestsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
contestsList, err := h.contestsUC.ListContests(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListContestsResponseDTO(contestsList))
|
||||
case models.RoleStudent:
|
||||
filter.UserId = &session.UserId
|
||||
contestsList, err := h.contestsUC.ListContests(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListContestsResponseDTO(contestsList))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func GetContestResponseDTO(contest *models.Contest,
|
||||
tasks []*models.TasksListItem,
|
||||
solutions []*models.SolutionsListItem) *testerv1.GetContestResponse {
|
||||
|
||||
m := make(map[int32]*models.SolutionsListItem)
|
||||
|
||||
for i := 0; i < len(solutions); i++ {
|
||||
m[solutions[i].TaskPosition] = solutions[i]
|
||||
}
|
||||
|
||||
resp := testerv1.GetContestResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]struct {
|
||||
Solution testerv1.SolutionsListItem `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}, len(tasks)),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
solution := testerv1.SolutionsListItem{}
|
||||
if sol, ok := m[task.Position]; ok {
|
||||
solution = SolutionsListItemDTO(*sol)
|
||||
}
|
||||
resp.Tasks[i] = struct {
|
||||
Solution testerv1.SolutionsListItem `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}{
|
||||
Solution: solution,
|
||||
Task: TasksListItemDTO(*task),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func ListContestsResponseDTO(contestsList *models.ContestsList) *testerv1.ListContestsResponse {
|
||||
resp := testerv1.ListContestsResponse{
|
||||
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
|
||||
Pagination: PaginationDTO(contestsList.Pagination),
|
||||
}
|
||||
|
||||
for i, contest := range contestsList.Contests {
|
||||
resp.Contests[i] = ContestsListItemDTO(*contest)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func ListSolutionsResponseDTO(solutionsList *models.SolutionsList) *testerv1.ListSolutionsResponse {
|
||||
resp := testerv1.ListSolutionsResponse{
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
|
||||
Pagination: PaginationDTO(solutionsList.Pagination),
|
||||
}
|
||||
|
||||
for i, solution := range solutionsList.Solutions {
|
||||
resp.Solutions[i] = SolutionsListItemDTO(*solution)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func GetTaskResponseDTO(contest *models.Contest, tasks []*models.TasksListItem, task *models.Task) *testerv1.GetTaskResponse {
|
||||
resp := testerv1.GetTaskResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Task: *TaskDTO(task),
|
||||
}
|
||||
|
||||
for i, t := range tasks {
|
||||
resp.Tasks[i] = TasksListItemDTO(*t)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func ContestDTO(c models.Contest) testerv1.Contest {
|
||||
return testerv1.Contest{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ContestsListItemDTO(c models.ContestsListItem) testerv1.ContestsListItem {
|
||||
return testerv1.ContestsListItem{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TasksListItemDTO(t models.TasksListItem) testerv1.TasksListItem {
|
||||
return testerv1.TasksListItem{
|
||||
Id: t.Id,
|
||||
Position: t.Position,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
ProblemId: t.ProblemId,
|
||||
TimeLimit: t.TimeLimit,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TaskDTO(t *models.Task) *testerv1.Task {
|
||||
return &testerv1.Task{
|
||||
Id: t.Id,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
TimeLimit: t.TimeLimit,
|
||||
|
||||
InputFormatHtml: t.InputFormatHtml,
|
||||
LegendHtml: t.LegendHtml,
|
||||
NotesHtml: t.NotesHtml,
|
||||
OutputFormatHtml: t.OutputFormatHtml,
|
||||
Position: t.Position,
|
||||
ScoringHtml: t.ScoringHtml,
|
||||
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ParticipantsListItemDTO(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
|
||||
return testerv1.ParticipantsListItem{
|
||||
Id: p.Id,
|
||||
UserId: p.UserId,
|
||||
Name: p.Name,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SolutionsListItemDTO(s models.SolutionsListItem) testerv1.SolutionsListItem {
|
||||
return testerv1.SolutionsListItem{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SolutionDTO(s models.Solution) testerv1.Solution {
|
||||
return testerv1.Solution{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
Solution: s.Solution,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
|
||||
contest, err := h.contestsUC.GetContest(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
monitor, err := h.contestsUC.GetMonitor(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetMonitorResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
|
||||
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
|
||||
}
|
||||
|
||||
for i, participant := range monitor.Participants {
|
||||
resp.Participants[i] = testerv1.ParticipantsStat{
|
||||
Id: participant.Id,
|
||||
Name: participant.Name,
|
||||
PenaltyInTotal: participant.PenaltyInTotal,
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
|
||||
SolvedInTotal: participant.SolvedInTotal,
|
||||
}
|
||||
|
||||
for j, solution := range participant.Solutions {
|
||||
resp.Participants[i].Solutions[j] = SolutionsListItemDTO(*solution)
|
||||
}
|
||||
}
|
||||
|
||||
for i, problem := range monitor.Summary {
|
||||
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
|
||||
Id: problem.Id,
|
||||
Success: problem.Success,
|
||||
Total: problem.Total,
|
||||
}
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TasksListItemDTO(*task)
|
||||
}
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
id, err := h.contestsUC.CreateParticipant(ctx, params.ContestId, params.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateParticipantResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req testerv1.UpdateParticipantRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateParticipant(ctx, params.ParticipantId, models.ParticipantUpdate{
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ContestId: params.ContestId,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListParticipantsResponse{
|
||||
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
|
||||
Pagination: PaginationDTO(participantsList.Pagination),
|
||||
}
|
||||
|
||||
for i, participant := range participantsList.Participants {
|
||||
resp.Participants[i] = ParticipantsListItemDTO(*participant)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSolutionSize int64 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
|
||||
s, err := c.FormFile("solution")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Size == 0 || s.Size > maxSolutionSize {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
f, err := s.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := h.contestsUC.CreateSolution(ctx, &models.SolutionCreation{
|
||||
UserId: session.UserId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Solution: string(b),
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateSolutionResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetSolution(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
solution, err := h.contestsUC.GetSolution(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
|
||||
case models.RoleStudent:
|
||||
_, err := h.contestsUC.GetParticipantId3(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
solution, err := h.contestsUC.GetSolution(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
filter := models.SolutionsFilter{
|
||||
ContestId: params.ContestId,
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ParticipantId: params.ParticipantId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Order: params.Order,
|
||||
State: params.State,
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListSolutionsResponseDTO(solutionsList))
|
||||
case models.RoleStudent:
|
||||
if params.ContestId == nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, *params.ContestId, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
// Student cannot view other users' solutions
|
||||
if params.ParticipantId != nil && *params.ParticipantId != participantId {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
filter.ParticipantId = &participantId
|
||||
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListSolutionsResponseDTO(solutionsList))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
id, err := h.contestsUC.CreateTask(ctx, params.ContestId, params.ProblemId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateTaskResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetTask(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
contest, err := h.contestsUC.GetContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.GetTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
|
||||
case models.RoleStudent:
|
||||
_, err = h.contestsUC.GetParticipantId2(ctx, id, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
contest, err := h.contestsUC.GetContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.GetTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteTask(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
err := h.contestsUC.DeleteTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package contests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
GetContest(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
|
||||
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
GetTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
|
||||
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error)
|
||||
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
|
||||
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
|
||||
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
|
||||
|
||||
GetMonitor(ctx context.Context, id int32, penalty int32) (*models.Monitor, error)
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sqlx.DB) *Repository {
|
||||
return &Repository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const CreateContestQuery = "INSERT INTO contests (title) VALUES ($1) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
const op = "Repository.CreateContest"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, CreateContestQuery, title)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const GetContestQuery = "SELECT * from contests WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
const op = "Repository.GetContest"
|
||||
|
||||
var contest models.Contest
|
||||
err := r.db.GetContext(ctx, &contest, GetContestQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &contest, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateContestQuery = "UPDATE contests SET title = COALESCE($1, title) WHERE id = $2"
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
const op = "Repository.UpdateContest"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, UpdateContestQuery, contestUpdate.Title, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const DeleteContestQuery = "DELETE FROM contests WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteContest(ctx context.Context, id int32) error {
|
||||
const op = "Repository.DeleteContest"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteContestQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildListContestsQueries(filter models.ContestsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
|
||||
columns := []string{
|
||||
"c.id",
|
||||
"c.title",
|
||||
"c.created_at",
|
||||
"c.updated_at",
|
||||
}
|
||||
|
||||
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).From("contests c")
|
||||
|
||||
if filter.UserId != nil {
|
||||
qb = qb.Join("participants p ON c.id = p.contest_id")
|
||||
qb = qb.Where(sq.Eq{"p.user_id": *filter.UserId})
|
||||
}
|
||||
|
||||
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
|
||||
|
||||
if filter.Order != nil && *filter.Order < 0 {
|
||||
qb = qb.OrderBy("c.created_at DESC")
|
||||
} else {
|
||||
qb = qb.OrderBy("c.created_at ASC")
|
||||
}
|
||||
|
||||
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
|
||||
|
||||
return qb, countQb
|
||||
}
|
||||
|
||||
func (r *Repository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
const op = "Repository.ListContests"
|
||||
|
||||
baseQb, countQb := buildListContestsQueries(filter)
|
||||
|
||||
query, args, err := baseQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var contests []*models.ContestsListItem
|
||||
err = r.db.SelectContext(ctx, &contests, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
query, args, err = countQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ContestsList{
|
||||
Contests: contests,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
|
||||
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
assert.NoError(t, err)
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
return sqlxDB, mock
|
||||
}
|
||||
|
||||
func TestRepository_CreateContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
contest := models.Contest{
|
||||
Id: 1,
|
||||
Title: "Test Contest",
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateContestQuery).
|
||||
WithArgs(contest.Title).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(contest.Id))
|
||||
|
||||
id, err := repo.CreateContest(ctx, contest.Title)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contest.Id, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_GetContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
contest := models.Contest{
|
||||
Id: 1,
|
||||
Title: "Test Contest",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.GetContestQuery).
|
||||
WithArgs(contest.Id).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}).
|
||||
AddRow(contest.Id, contest.Title, contest.CreatedAt, contest.UpdatedAt))
|
||||
|
||||
result, err := repo.GetContest(ctx, contest.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualExportedValues(t, &contest, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var contestId int32 = 1
|
||||
update := models.ContestUpdate{
|
||||
Title: sp("Updated Contest"),
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateContestQuery).
|
||||
WithArgs(update.Title, contestId).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.UpdateContest(ctx, contestId, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(repository.DeleteContestQuery).
|
||||
WithArgs(1).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteContest(ctx, 1)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func sp(s string) *string {
|
||||
return &s
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
ReadStatisticsQuery = `
|
||||
SELECT t.id as task_id,
|
||||
t.position,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
|
||||
FROM tasks t LEFT JOIN solutions s ON t.id = s.task_id
|
||||
WHERE t.contest_id = $1
|
||||
GROUP BY t.id, t.position
|
||||
ORDER BY t.position;
|
||||
`
|
||||
|
||||
SolutionsQuery = `
|
||||
WITH RankedSolutions AS (
|
||||
SELECT
|
||||
s.id,
|
||||
|
||||
s.participant_id,
|
||||
p2.name as participant_name,
|
||||
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
|
||||
s.task_id,
|
||||
t.position as task_position,
|
||||
p.title as task_title,
|
||||
|
||||
t.contest_id,
|
||||
c.title as contest_title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.task_id, s.participant_id
|
||||
ORDER BY s.score DESC, s.created_at
|
||||
) as rn
|
||||
FROM solutions s
|
||||
LEFT JOIN tasks t ON s.task_id = t.id
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
LEFT JOIN contests c ON t.contest_id = c.id
|
||||
LEFT JOIN participants p2 on s.participant_id = p2.id
|
||||
WHERE t.contest_id = $1
|
||||
)
|
||||
SELECT
|
||||
rs.id,
|
||||
|
||||
rs.participant_id,
|
||||
rs.participant_name,
|
||||
|
||||
rs.state,
|
||||
rs.score,
|
||||
rs.penalty,
|
||||
rs.time_stat,
|
||||
rs.memory_stat,
|
||||
rs.language,
|
||||
|
||||
rs.task_id,
|
||||
rs.task_position,
|
||||
rs.task_title,
|
||||
|
||||
rs.contest_id,
|
||||
rs.contest_title,
|
||||
|
||||
rs.updated_at,
|
||||
rs.created_at
|
||||
FROM RankedSolutions rs
|
||||
WHERE rs.rn = 1`
|
||||
|
||||
ParticipantsQuery = `
|
||||
WITH Attempts AS (
|
||||
SELECT
|
||||
s.participant_id,
|
||||
s.task_id,
|
||||
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
|
||||
SELECT MIN(s2.created_at)
|
||||
FROM solutions s2
|
||||
WHERE s2.participant_id = s.participant_id
|
||||
AND s2.task_id = s.task_id
|
||||
AND s2.state = 5
|
||||
)) as failed_attempts,
|
||||
MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty
|
||||
FROM solutions s JOIN tasks t ON t.id = s.task_id
|
||||
WHERE t.contest_id = $1
|
||||
GROUP BY s.participant_id, s.task_id
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
|
||||
COALESCE(SUM(a.failed_attempts), 0) * $2 + COALESCE(SUM(a.success_penalty), 0) as penalty_in_total
|
||||
FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id
|
||||
WHERE p.contest_id = $1
|
||||
GROUP BY p.id, p.name
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetMonitor(ctx context.Context, contestId int32, penalty int32) (*models.Monitor, error) {
|
||||
const op = "Repository.GetMonitor"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, ReadStatisticsQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var monitor models.Monitor
|
||||
for rows.Next() {
|
||||
var stat models.ProblemStatSummary
|
||||
err = rows.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
monitor.Summary = append(monitor.Summary, &stat)
|
||||
}
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
err = r.db.SelectContext(ctx, &solutions, SolutionsQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
rows3, err := r.db.QueryxContext(ctx, ParticipantsQuery, contestId, penalty)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
solutionsMap := make(map[int32][]*models.SolutionsListItem)
|
||||
for _, solution := range solutions {
|
||||
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
|
||||
}
|
||||
|
||||
for rows3.Next() {
|
||||
var stat models.ParticipantsStat
|
||||
err = rows3.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
if sols, ok := solutionsMap[stat.Id]; ok {
|
||||
stat.Solutions = sols
|
||||
}
|
||||
|
||||
monitor.Participants = append(monitor.Participants, &stat)
|
||||
}
|
||||
|
||||
return &monitor, nil
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package repository
|
|
@ -1,126 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const GetParticipantIdQuery = "SELECT id FROM participants WHERE user_id=$1 AND contest_id=$2 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantIdQuery, userId, contestId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const GetParticipantId2Query = "SELECT p.id FROM participants p JOIN tasks t ON p.contest_id=t.contest_id WHERE user_id=$1 AND t.id=$2 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId2"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantId2Query, userId, taskId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const GetParticipantId3Query = "SELECT participant_id FROM solutions WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId3"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantId3Query, solutionId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const CreateParticipantQuery = "INSERT INTO participants (user_id, contest_id, name) VALUES ($1, $2, $3) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.CreateParticipant"
|
||||
|
||||
name := ""
|
||||
rows, err := r.db.QueryxContext(ctx, CreateParticipantQuery, userId, contestId, name)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const DeleteParticipantQuery = "DELETE FROM participants WHERE id=$1"
|
||||
|
||||
const (
|
||||
UpdateParticipantQuery = "UPDATE participants SET name = COALESCE($1, name) WHERE id = $2"
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
const op = "Repository.UpdateParticipant"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, UpdateParticipantQuery, participantUpdate.Name, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
const op = "Repository.DeleteParticipant"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteParticipantQuery, participantId)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ReadParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = $1 LIMIT $2 OFFSET $3`
|
||||
CountParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = $1"
|
||||
)
|
||||
|
||||
func (r *Repository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
const op = "Repository.ReadParticipants"
|
||||
|
||||
var participants []*models.ParticipantsListItem
|
||||
err := r.db.SelectContext(ctx, &participants,
|
||||
ReadParticipantsListQuery, filter.ContestId, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, CountParticipantsQuery, filter.ContestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ParticipantsList{
|
||||
Participants: participants,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepository_CreateParticipant(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
expectedId int32 = 1
|
||||
userId int32 = 2
|
||||
contestId int32 = 3
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectQuery(repository.CreateParticipantQuery).
|
||||
WithArgs(userId, contestId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateParticipant(ctx, contestId, userId)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteParticipant(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var participantId int32 = 1
|
||||
|
||||
mock.ExpectExec(repository.DeleteParticipantQuery).
|
||||
WithArgs(participantId).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteParticipant(ctx, participantId)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
const (
|
||||
GetSolutionQuery = "SELECT * FROM solutions WHERE id = $1"
|
||||
)
|
||||
|
||||
func (r *Repository) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
const op = "Repository.GetSolution"
|
||||
|
||||
var solution models.Solution
|
||||
err := r.db.GetContext(ctx, &solution, GetSolutionQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
const (
|
||||
CreateSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func (r *Repository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
const op = "Repository.CreateSolution"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx,
|
||||
CreateSolutionQuery,
|
||||
creation.TaskId,
|
||||
creation.ParticipantId,
|
||||
creation.Language,
|
||||
creation.Penalty,
|
||||
creation.Solution,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
|
||||
columns := []string{
|
||||
"s.id",
|
||||
"s.participant_id",
|
||||
"p2.name AS participant_name",
|
||||
"s.state",
|
||||
"s.score",
|
||||
"s.penalty",
|
||||
"s.time_stat",
|
||||
"s.memory_stat",
|
||||
"s.language",
|
||||
"s.task_id",
|
||||
"t.position AS task_position",
|
||||
"p.title AS task_title",
|
||||
"t.contest_id",
|
||||
"c.title",
|
||||
"s.updated_at",
|
||||
"s.created_at",
|
||||
}
|
||||
|
||||
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).
|
||||
From("solutions s").
|
||||
LeftJoin("tasks t ON s.task_id = t.id").
|
||||
LeftJoin("problems p ON t.problem_id = p.id").
|
||||
LeftJoin("contests c ON t.contest_id = c.id").
|
||||
LeftJoin("participants p2 ON s.participant_id = p2.id")
|
||||
|
||||
if filter.ContestId != nil {
|
||||
qb = qb.Where(sq.Eq{"s.contest_id": *filter.ContestId})
|
||||
}
|
||||
if filter.ParticipantId != nil {
|
||||
qb = qb.Where(sq.Eq{"s.participant_id": *filter.ParticipantId})
|
||||
}
|
||||
if filter.TaskId != nil {
|
||||
qb = qb.Where(sq.Eq{"s.task_id": *filter.TaskId})
|
||||
}
|
||||
if filter.Language != nil {
|
||||
qb = qb.Where(sq.Eq{"s.language": *filter.Language})
|
||||
}
|
||||
if filter.State != nil {
|
||||
qb = qb.Where(sq.Eq{"s.state": *filter.State})
|
||||
}
|
||||
|
||||
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
|
||||
|
||||
if filter.Order != nil && *filter.Order < 0 {
|
||||
qb = qb.OrderBy("s.id DESC")
|
||||
} else {
|
||||
qb = qb.OrderBy("s.id ASC")
|
||||
}
|
||||
|
||||
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
|
||||
|
||||
return qb, countQb
|
||||
}
|
||||
|
||||
func (r *Repository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
const op = "ContestRepository.ListSolutions"
|
||||
|
||||
baseQb, countQb := buildListSolutionsQueries(filter)
|
||||
|
||||
query, args, err := countQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var totalCount int32
|
||||
err = r.db.GetContext(ctx, &totalCount, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
query, args, err = baseQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
rows, err := r.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
solutions := make([]*models.SolutionsListItem, 0)
|
||||
for rows.Next() {
|
||||
var solution models.SolutionsListItem
|
||||
err = rows.StructScan(&solution)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
solutions = append(solutions, &solution)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.SolutionsList{
|
||||
Solutions: solutions,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(totalCount, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
GetBestSolutions = `
|
||||
WITH contest_tasks AS (
|
||||
SELECT t.id AS task_id,
|
||||
t.position AS task_position,
|
||||
t.contest_id,
|
||||
t.problem_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
p.title AS task_title,
|
||||
c.title AS contest_title
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON p.id = t.problem_id
|
||||
LEFT JOIN contests c ON c.id = t.contest_id
|
||||
WHERE t.contest_id = ?
|
||||
),
|
||||
best_solutions AS (
|
||||
SELECT DISTINCT ON (s.task_id)
|
||||
*
|
||||
FROM solutions s
|
||||
WHERE s.participant_id = ?
|
||||
ORDER BY s.task_id, s.score DESC, s.created_at DESC
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.participant_id,
|
||||
p.name AS participant_name,
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
ct.task_id,
|
||||
ct.task_position,
|
||||
ct.task_title,
|
||||
ct.contest_id,
|
||||
ct.contest_title,
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM contest_tasks ct
|
||||
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
|
||||
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
|
||||
ORDER BY ct.task_position
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
|
||||
const op = "Repository.GetBestSolutions"
|
||||
var solutions []*models.SolutionsListItem
|
||||
query := r.db.Rebind(GetBestSolutions)
|
||||
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
|
||||
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return solutions, nil
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package repository
|
|
@ -1,101 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const CreateTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
|
||||
VALUES ($1, $2, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = $2), 0) + 1)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *Repository) CreateTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
|
||||
const op = "Repository.AddTask"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, CreateTaskQuery, problemId, contestId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const DeleteTaskQuery = "DELETE FROM tasks WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
const op = "Repository.DeleteTask"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteTaskQuery, taskId)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const GetTasksQuery = `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 = $1 ORDER BY position`
|
||||
|
||||
func (r *Repository) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
const op = "Repository.ReadTasks"
|
||||
|
||||
var tasks []*models.TasksListItem
|
||||
err := r.db.SelectContext(ctx, &tasks, GetTasksQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
const (
|
||||
GetTaskQuery = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.position,
|
||||
p.title,
|
||||
p.time_limit,
|
||||
p.memory_limit,
|
||||
t.problem_id,
|
||||
t.contest_id,
|
||||
p.legend_html,
|
||||
p.input_format_html,
|
||||
p.output_format_html,
|
||||
p.notes_html,
|
||||
p.scoring_html,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
WHERE t.id = ?
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
const op = "Repository.ReadTask"
|
||||
|
||||
query := r.db.Rebind(GetTaskQuery)
|
||||
var task models.Task
|
||||
err := r.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepository_CreateTask(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
expectedId int32 = 1
|
||||
problemId int32 = 2
|
||||
contestId int32 = 3
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectQuery(repository.CreateTaskQuery).
|
||||
WithArgs(problemId, contestId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateTask(ctx, contestId, problemId)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteTask(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(repository.DeleteTaskQuery).
|
||||
WithArgs(1).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteTask(ctx, 1)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package contests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
GetContest(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
|
||||
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
GetTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
|
||||
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error)
|
||||
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
|
||||
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
|
||||
|
||||
GetMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ContestUseCase struct {
|
||||
contestRepo contests.Repository
|
||||
}
|
||||
|
||||
func NewContestUseCase(
|
||||
contestRepo contests.Repository,
|
||||
) *ContestUseCase {
|
||||
return &ContestUseCase{
|
||||
contestRepo: contestRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
return uc.contestRepo.CreateContest(ctx, title)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
return uc.contestRepo.GetContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
|
||||
return uc.contestRepo.DeleteContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
return uc.contestRepo.ListContests(ctx, filter)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
return uc.contestRepo.GetMonitor(ctx, contestId, 20)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId2(ctx, taskId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId3(ctx, solutionId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
|
||||
return uc.contestRepo.CreateParticipant(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
return uc.contestRepo.DeleteParticipant(ctx, participantId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
return uc.contestRepo.ListParticipants(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
return uc.contestRepo.GetSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
participantId, err := uc.contestRepo.GetParticipantId2(ctx, creation.TaskId, creation.UserId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
creation.ParticipantId = participantId
|
||||
|
||||
return uc.contestRepo.CreateSolution(ctx, creation)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
return uc.contestRepo.ListSolutions(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
|
||||
return uc.contestRepo.GetBestSolutions(ctx, contestId, participantId)
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) CreateTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
|
||||
return uc.contestRepo.CreateTask(ctx, contestId, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
return uc.contestRepo.GetTask(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
return uc.contestRepo.GetTasks(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
return uc.contestRepo.DeleteTask(ctx, taskId)
|
||||
}
|
9
internal/lib/config.go
Normal file
9
internal/lib/config.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package lib
|
||||
|
||||
type Config struct {
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
Pandoc string `env:"PANDOC" required:"true"`
|
||||
Auth string `env:"PANDOC" required:"true"`
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
|
||||
}
|
132
internal/lib/errors.go
Normal file
132
internal/lib/errors.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type code uint8
|
||||
|
||||
const (
|
||||
ErrValidationFailed code = 1
|
||||
ErrInternal code = 2
|
||||
ErrExternal code = 3
|
||||
ErrNoPermission code = 4
|
||||
ErrUnknown code = 5
|
||||
ErrDeadlineExceeded code = 6
|
||||
ErrNotFound code = 7
|
||||
ErrAlreadyExists code = 8
|
||||
ErrConflict code = 9
|
||||
ErrUnimplemented code = 10
|
||||
ErrBadInput code = 11
|
||||
ErrUnauthenticated code = 12
|
||||
)
|
||||
|
||||
func (c code) String() string {
|
||||
switch {
|
||||
case errors.Is(c, ErrValidationFailed):
|
||||
return "validation error"
|
||||
case errors.Is(c, ErrInternal):
|
||||
return "internal error"
|
||||
case errors.Is(c, ErrExternal):
|
||||
return "external error"
|
||||
case errors.Is(c, ErrNoPermission):
|
||||
return "permission error"
|
||||
case errors.Is(c, ErrUnknown):
|
||||
return "unknown error"
|
||||
case errors.Is(c, ErrDeadlineExceeded):
|
||||
return "deadline error"
|
||||
case errors.Is(c, ErrNotFound):
|
||||
return "not found error"
|
||||
case errors.Is(c, ErrAlreadyExists):
|
||||
return "already exists error"
|
||||
case errors.Is(c, ErrConflict):
|
||||
return "conflict error"
|
||||
case errors.Is(c, ErrUnimplemented):
|
||||
return "unimplemented error"
|
||||
case errors.Is(c, ErrBadInput):
|
||||
return "bad input error"
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (c code) Error() string {
|
||||
return c.String()
|
||||
}
|
||||
|
||||
type layer uint8
|
||||
|
||||
const (
|
||||
LayerTransport layer = 1
|
||||
LayerService layer = 2
|
||||
LayerStorage layer = 3
|
||||
)
|
||||
|
||||
func (l layer) String() string {
|
||||
switch l {
|
||||
case LayerTransport:
|
||||
return "transport"
|
||||
case LayerService:
|
||||
return "service"
|
||||
case LayerStorage:
|
||||
return "storage"
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func location(skip int) string {
|
||||
_, file, line, _ := runtime.Caller(skip)
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
src error
|
||||
layer layer
|
||||
code code
|
||||
msg string
|
||||
loc string
|
||||
}
|
||||
|
||||
func wrap(src error, layer layer, class code, msg string, loc string) *Error {
|
||||
return &Error{
|
||||
src: src,
|
||||
layer: layer,
|
||||
code: class,
|
||||
msg: msg,
|
||||
loc: loc,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() []error {
|
||||
return []error{e.src, e.code}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.code.String(), e.msg)
|
||||
}
|
||||
|
||||
func (e *Error) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
|
||||
if e.src != nil {
|
||||
encoder.AddString("src", e.src.Error())
|
||||
}
|
||||
encoder.AddString("layer", e.layer.String())
|
||||
encoder.AddString("code", e.code.String())
|
||||
encoder.AddString("msg", e.msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TransportError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerTransport, code, msg, location(2))
|
||||
}
|
||||
|
||||
func ServiceError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerService, code, msg, location(2))
|
||||
}
|
||||
|
||||
func StorageError(src error, code code, msg string) error {
|
||||
return wrap(src, LayerStorage, code, msg, location(2))
|
||||
}
|
17
internal/lib/lib.go
Normal file
17
internal/lib/lib.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
}
|
67
internal/lib/pandoc.go
Normal file
67
internal/lib/pandoc.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PandocClient struct {
|
||||
client *http.Client
|
||||
address string
|
||||
}
|
||||
|
||||
func NewPandocClient(client *http.Client, address string) *PandocClient {
|
||||
return &PandocClient{
|
||||
client: client,
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
type convertRequest struct {
|
||||
Text string `json:"text"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
func (client *PandocClient) convert(ctx context.Context, text, from, to string) (string, error) {
|
||||
body, err := json.Marshal(convertRequest{
|
||||
Text: text,
|
||||
From: from,
|
||||
To: to,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(body)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.address, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (client *PandocClient) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) {
|
||||
return client.convert(ctx, text, "latex", "html5")
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
|
||||
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 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token, ok := parsedToken.Claims.(*models.JWT)
|
||||
if !ok {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
err = token.Valid()
|
||||
if err != nil {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
// check if session exists
|
||||
_, err = sessionsUC.ReadSession(ctx, token.SessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrNotFound) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
c.Locals(TokenKey, token)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Contest struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ContestsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ContestsList struct {
|
||||
Contests []*ContestsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ContestsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
UserId *int32
|
||||
Order *int32
|
||||
}
|
||||
|
||||
func (f ContestsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ContestUpdate struct {
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
Participants []*ParticipantsStat
|
||||
Summary []*ProblemStatSummary
|
||||
}
|
||||
|
||||
type ParticipantsStat struct {
|
||||
Id int32 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
SolvedInTotal int32 `db:"solved_in_total"`
|
||||
PenaltyInTotal int32 `db:"penalty_in_total"`
|
||||
Solutions []*SolutionsListItem `db:"solutions"`
|
||||
}
|
||||
|
||||
type ProblemStatSummary struct {
|
||||
Id int32 `db:"task_id"`
|
||||
Position int32 `db:"position"`
|
||||
Success int32 `db:"success"`
|
||||
Total int32 `db:"total"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Id int32 `db:"id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type TasksListItem 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"`
|
||||
}
|
||||
|
||||
type Participant struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsList struct {
|
||||
Participants []*ParticipantsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ParticipantsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
|
||||
ContestId int32
|
||||
}
|
||||
|
||||
func (f ParticipantsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ParticipantUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package models
|
||||
|
||||
type Pagination struct {
|
||||
Page int32 `json:"page"`
|
||||
Total int32 `json:"total"`
|
||||
}
|
||||
|
||||
func Total(count int32, pageSize int32) int32 {
|
||||
if count%pageSize == 0 {
|
||||
return count / pageSize
|
||||
}
|
||||
return count/pageSize + 1
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Problem struct {
|
||||
Id int32 `db:"id"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
Legend string `db:"legend"`
|
||||
InputFormat string `db:"input_format"`
|
||||
OutputFormat string `db:"output_format"`
|
||||
Notes string `db:"notes"`
|
||||
Scoring string `db:"scoring"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ProblemsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
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"`
|
||||
SolvedCount int32 `db:"solved_count"`
|
||||
}
|
||||
|
||||
type ProblemsList struct {
|
||||
Problems []*ProblemsListItem `json:"problems"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type ProblemsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
}
|
||||
|
||||
func (f ProblemsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ProblemUpdate struct {
|
||||
Title *string `db:"title"`
|
||||
MemoryLimit *int32 `db:"memory_limit"`
|
||||
TimeLimit *int32 `db:"time_limit"`
|
||||
|
||||
Legend *string `db:"legend"`
|
||||
InputFormat *string `db:"input_format"`
|
||||
OutputFormat *string `db:"output_format"`
|
||||
Notes *string `db:"notes"`
|
||||
Scoring *string `db:"scoring"`
|
||||
|
||||
LegendHtml *string `db:"legend_html"`
|
||||
InputFormatHtml *string `db:"input_format_html"`
|
||||
OutputFormatHtml *string `db:"output_format_html"`
|
||||
NotesHtml *string `db:"notes_html"`
|
||||
ScoringHtml *string `db:"scoring_html"`
|
||||
}
|
||||
|
||||
type ProblemStatement struct {
|
||||
Legend string `db:"legend"`
|
||||
InputFormat string `db:"input_format"`
|
||||
OutputFormat string `db:"output_format"`
|
||||
Notes string `db:"notes"`
|
||||
Scoring string `db:"scoring"`
|
||||
}
|
||||
|
||||
type Html5ProblemStatement struct {
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id string `json:"id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
|
||||
func (s *Session) Valid() error {
|
||||
if uuid.Validate(s.Id) != nil {
|
||||
return errors.New("invalid session id")
|
||||
}
|
||||
if s.UserId == 0 {
|
||||
return errors.New("empty user id")
|
||||
}
|
||||
if s.CreatedAt.IsZero() {
|
||||
return errors.New("empty created at")
|
||||
}
|
||||
if s.ExpiresAt.IsZero() {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
if s.UserAgent == "" {
|
||||
return errors.New("empty user agent")
|
||||
}
|
||||
if s.Ip == "" {
|
||||
return errors.New("empty ip")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) JSON() ([]byte, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
func (s *Session) UserIdHash() string {
|
||||
return sha256string(strconv.FormatInt(int64(s.UserId), 10))
|
||||
}
|
||||
|
||||
func (s *Session) SessionIdHash() string {
|
||||
return sha256string(s.Id)
|
||||
}
|
||||
|
||||
func (s *Session) Key() string {
|
||||
return fmt.Sprintf("userid:%s:sessionid:%s", s.UserIdHash(), s.SessionIdHash())
|
||||
}
|
||||
func sha256string(s string) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(s))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
type JWT struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
}
|
||||
|
||||
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.IssuedAt == 0 {
|
||||
return errors.New("empty issued at")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Ip string
|
||||
UseAgent string
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Solution struct {
|
||||
Id int32 `db:"id"`
|
||||
|
||||
ParticipantId int32 `db:"participant_id"`
|
||||
ParticipantName string `db:"participant_name"`
|
||||
|
||||
Solution string `db:"solution"`
|
||||
|
||||
State int32 `db:"state"`
|
||||
Score int32 `db:"score"`
|
||||
Penalty int32 `db:"penalty"`
|
||||
TimeStat int32 `db:"time_stat"`
|
||||
MemoryStat int32 `db:"memory_stat"`
|
||||
Language int32 `db:"language"`
|
||||
|
||||
TaskId int32 `db:"task_id"`
|
||||
TaskPosition int32 `db:"task_position"`
|
||||
TaskTitle string `db:"task_title"`
|
||||
|
||||
ContestId int32 `db:"contest_id"`
|
||||
ContestTitle string `db:"contest_title"`
|
||||
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
type SolutionCreation struct {
|
||||
Solution string
|
||||
TaskId int32
|
||||
UserId int32
|
||||
ParticipantId int32
|
||||
Language int32
|
||||
Penalty int32
|
||||
}
|
||||
|
||||
type SolutionsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
|
||||
ParticipantId int32 `db:"participant_id"`
|
||||
ParticipantName string `db:"participant_name"`
|
||||
|
||||
State int32 `db:"state"`
|
||||
Score int32 `db:"score"`
|
||||
Penalty int32 `db:"penalty"`
|
||||
TimeStat int32 `db:"time_stat"`
|
||||
MemoryStat int32 `db:"memory_stat"`
|
||||
Language int32 `db:"language"`
|
||||
|
||||
TaskId int32 `db:"task_id"`
|
||||
TaskPosition int32 `db:"task_position"`
|
||||
TaskTitle string `db:"task_title"`
|
||||
|
||||
ContestId int32 `db:"contest_id"`
|
||||
ContestTitle string `db:"contest_title"`
|
||||
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
type SolutionsList struct {
|
||||
Solutions []*SolutionsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type SolutionsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
ContestId *int32
|
||||
ParticipantId *int32
|
||||
TaskId *int32
|
||||
Language *int32
|
||||
State *int32
|
||||
Order *int32
|
||||
}
|
||||
|
||||
func (f SolutionsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
//type Result int32
|
||||
//
|
||||
//const (
|
||||
// NotTested Result = 1 // change only with schema change
|
||||
// Accepted Result = 2
|
||||
// WrongAnswer Result = 3
|
||||
// PresentationError Result = 4
|
||||
// CompilationError Result = 5
|
||||
// MemoryLimitExceeded Result = 6
|
||||
// TimeLimitExceeded Result = 7
|
||||
// RuntimeError Result = 8
|
||||
// SystemFailDuringTesting Result = 9
|
||||
// Testing Result = 10
|
||||
//)
|
||||
//
|
||||
//var ErrBadResult = errors.New("bad result")
|
||||
//
|
||||
//func (result Result) Valid() error {
|
||||
// switch result {
|
||||
// case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
|
||||
// return nil
|
||||
// }
|
||||
// return ErrBadResult
|
||||
//}
|
||||
//
|
||||
//type Language struct {
|
||||
// Name string
|
||||
// CompileCmd []string //source: src;result:executable
|
||||
// RunCmd []string //source: executable
|
||||
//}
|
||||
//
|
||||
//var Languages = []Language{
|
||||
// {Name: "gcc std=c90",
|
||||
// CompileCmd: []string{"gcc", "src", "-std=c90", "-o", "executable"},
|
||||
// RunCmd: []string{"executable"}},
|
||||
//}
|
|
@ -1,71 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Role int32
|
||||
|
||||
type User struct {
|
||||
Id int32 `db:"id"`
|
||||
Username string `db:"username"`
|
||||
HashedPassword string `db:"hashed_pwd"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Role Role `db:"role"`
|
||||
}
|
||||
|
||||
type UserCreation struct {
|
||||
Username string
|
||||
Password string
|
||||
Role Role
|
||||
}
|
||||
|
||||
func (u *UserCreation) HashPassword() error {
|
||||
hpwd, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hpwd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) IsSamePwd(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type UsersListFilters struct {
|
||||
PageSize int32
|
||||
Page int32
|
||||
}
|
||||
|
||||
func (f UsersListFilters) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type UsersList struct {
|
||||
Users []*User
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
Username *string
|
||||
Role *Role
|
||||
}
|
||||
|
||||
const (
|
||||
RoleGuest Role = -1
|
||||
RoleStudent Role = 0
|
||||
RoleTeacher Role = 1
|
||||
RoleAdmin Role = 2
|
||||
)
|
||||
|
||||
type Grant struct {
|
||||
Action string
|
||||
Resource string
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ProblemsHandlers interface {
|
||||
ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error
|
||||
CreateProblem(c *fiber.Ctx) error
|
||||
DeleteProblem(c *fiber.Ctx, id int32) error
|
||||
GetProblem(c *fiber.Ctx, id int32) error
|
||||
UpdateProblem(c *fiber.Ctx, id int32) error
|
||||
UploadProblem(c *fiber.Ctx, id int32) error
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
problemsUC problems.UseCase
|
||||
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
const (
|
||||
sessionKey = "session"
|
||||
)
|
||||
|
||||
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
|
||||
const op = "sessionFromCtx"
|
||||
|
||||
session, ok := ctx.Value(sessionKey).(*models.Session)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func NewHandlers(problemsUC problems.UseCase) *Handlers {
|
||||
return &Handlers{
|
||||
problemsUC: problemsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListProblemsResponse{
|
||||
Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)),
|
||||
Pagination: PaginationDTO(problemsList.Pagination),
|
||||
}
|
||||
|
||||
for i, problem := range problemsList.Problems {
|
||||
resp.Problems[i] = ProblemsListItemDTO(*problem)
|
||||
}
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateProblem(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
err := h.problemsUC.DeleteProblem(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
problem, err := h.problemsUC.GetProblemById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetProblemResponse{Problem: *ProblemDTO(problem)},
|
||||
)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req testerv1.UpdateProblemRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.problemsUC.UpdateProblem(c.Context(), id, &models.ProblemUpdate{
|
||||
Title: req.Title,
|
||||
MemoryLimit: req.MemoryLimit,
|
||||
TimeLimit: req.TimeLimit,
|
||||
|
||||
Legend: req.Legend,
|
||||
InputFormat: req.InputFormat,
|
||||
OutputFormat: req.OutputFormat,
|
||||
Notes: req.Notes,
|
||||
Scoring: req.Scoring,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UploadProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
//session, err := sessionFromCtx(ctx)
|
||||
//if err != nil {
|
||||
// return c.SendStatus(pkg.ToREST(err))
|
||||
//}
|
||||
|
||||
session := models.Session{
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
a, err := c.FormFile("archive")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.Size == 0 { // FIXME: check max size
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
f, err := a.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = h.problemsUC.UploadProblem(ctx, id, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func ProblemsListItemDTO(p models.ProblemsListItem) testerv1.ProblemsListItem {
|
||||
return testerv1.ProblemsListItem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
TimeLimit: p.TimeLimit,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
SolvedCount: p.SolvedCount,
|
||||
}
|
||||
}
|
||||
|
||||
func ProblemDTO(p *models.Problem) *testerv1.Problem {
|
||||
return &testerv1.Problem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
TimeLimit: p.TimeLimit,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
|
||||
Legend: p.Legend,
|
||||
InputFormat: p.InputFormat,
|
||||
OutputFormat: p.OutputFormat,
|
||||
Notes: p.Notes,
|
||||
Scoring: p.Scoring,
|
||||
|
||||
LegendHtml: p.LegendHtml,
|
||||
InputFormatHtml: p.InputFormatHtml,
|
||||
OutputFormatHtml: p.OutputFormatHtml,
|
||||
NotesHtml: p.NotesHtml,
|
||||
ScoringHtml: p.ScoringHtml,
|
||||
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
Rebind(query string) string
|
||||
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
|
||||
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
}
|
||||
|
||||
type Tx interface {
|
||||
Querier
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
BeginTx(ctx context.Context) (Tx, error)
|
||||
DB() Querier
|
||||
CreateProblem(ctx context.Context, q Querier, title string) (int32, error)
|
||||
GetProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, q Querier, id int32) error
|
||||
ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, q Querier, id int32, heading *models.ProblemUpdate) error
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
_db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sqlx.DB) *Repository {
|
||||
return &Repository{
|
||||
_db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) BeginTx(ctx context.Context) (problems.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DB() problems.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const CreateProblemQuery = "INSERT INTO problems (title) VALUES ($1) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateProblem(ctx context.Context, q problems.Querier, title string) (int32, error) {
|
||||
const op = "Repository.CreateProblem"
|
||||
|
||||
rows, err := q.QueryxContext(ctx, CreateProblemQuery, title)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const GetProblemByIdQuery = "SELECT * from problems WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetProblemById(ctx context.Context, q problems.Querier, id int32) (*models.Problem, error) {
|
||||
const op = "Repository.ReadProblemById"
|
||||
|
||||
var problem models.Problem
|
||||
err := q.GetContext(ctx, &problem, GetProblemByIdQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
const DeleteProblemQuery = "DELETE FROM problems WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteProblem(ctx context.Context, q problems.Querier, id int32) error {
|
||||
const op = "Repository.DeleteProblem"
|
||||
|
||||
_, err := q.ExecContext(ctx, DeleteProblemQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListProblemsQuery = `SELECT p.id,
|
||||
p.title,
|
||||
p.memory_limit,
|
||||
p.time_limit,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
COALESCE(solved_count, 0) AS solved_count
|
||||
FROM problems p
|
||||
LEFT JOIN (SELECT t.problem_id,
|
||||
COUNT(DISTINCT s.participant_id) AS solved_count
|
||||
FROM solutions s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.state = 5
|
||||
GROUP BY t.problem_id) sol ON p.id = sol.problem_id
|
||||
LIMIT $1 OFFSET $2`
|
||||
CountProblemsQuery = "SELECT COUNT(*) FROM problems"
|
||||
)
|
||||
|
||||
func (r *Repository) ListProblems(ctx context.Context, q problems.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
const op = "ContestRepository.ListProblems"
|
||||
|
||||
var list []*models.ProblemsListItem
|
||||
err := q.SelectContext(ctx, &list, ListProblemsQuery, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, CountProblemsQuery)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ProblemsList{
|
||||
Problems: list,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateProblemQuery = `UPDATE problems
|
||||
SET title = COALESCE($2, title),
|
||||
time_limit = COALESCE($3, time_limit),
|
||||
memory_limit = COALESCE($4, memory_limit),
|
||||
|
||||
legend = COALESCE($5, legend),
|
||||
input_format = COALESCE($6, input_format),
|
||||
output_format = COALESCE($7, output_format),
|
||||
notes = COALESCE($8, notes),
|
||||
scoring = COALESCE($9, scoring),
|
||||
|
||||
legend_html = COALESCE($10, legend_html),
|
||||
input_format_html = COALESCE($11, input_format_html),
|
||||
output_format_html = COALESCE($12, output_format_html),
|
||||
notes_html = COALESCE($13, notes_html),
|
||||
scoring_html = COALESCE($14, scoring_html)
|
||||
|
||||
WHERE id=$1`
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateProblem(ctx context.Context, q problems.Querier, id int32, problem *models.ProblemUpdate) error {
|
||||
const op = "Repository.UpdateProblem"
|
||||
|
||||
query := q.Rebind(UpdateProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query,
|
||||
id,
|
||||
|
||||
problem.Title,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
|
||||
problem.Legend,
|
||||
problem.InputFormat,
|
||||
problem.OutputFormat,
|
||||
problem.Notes,
|
||||
problem.Scoring,
|
||||
|
||||
problem.LegendHtml,
|
||||
problem.InputFormatHtml,
|
||||
problem.OutputFormatHtml,
|
||||
problem.NotesHtml,
|
||||
problem.ScoringHtml,
|
||||
)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
|
||||
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
assert.NoError(t, err)
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
return sqlxDB, mock
|
||||
}
|
||||
|
||||
func TestRepository_CreateProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
problem := models.Problem{
|
||||
Id: 1,
|
||||
Title: "Test Problem",
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateProblemQuery).
|
||||
WithArgs(problem.Title).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(problem.Id))
|
||||
|
||||
id, err := repo.CreateProblem(ctx, db, problem.Title)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, problem.Id, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_GetProblemById(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := &models.Problem{
|
||||
Id: 1,
|
||||
Title: "Test Problem",
|
||||
TimeLimit: 1000,
|
||||
MemoryLimit: 1024,
|
||||
Legend: "Test Legend",
|
||||
InputFormat: "Test Input Format",
|
||||
OutputFormat: "Test Output Format",
|
||||
Notes: "Test Notes",
|
||||
Scoring: "Test Scoring",
|
||||
LegendHtml: "Test Legend HTML",
|
||||
InputFormatHtml: "Test Input Format HTML",
|
||||
OutputFormatHtml: "Test Output Format HTML",
|
||||
NotesHtml: "Test Notes HTML",
|
||||
ScoringHtml: "Test Scoring HTML",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"title",
|
||||
"time_limit",
|
||||
"memory_limit",
|
||||
|
||||
"legend",
|
||||
"input_format",
|
||||
"output_format",
|
||||
"notes",
|
||||
"scoring",
|
||||
|
||||
"legend_html",
|
||||
"input_format_html",
|
||||
"output_format_html",
|
||||
"notes_html",
|
||||
"scoring_html",
|
||||
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
expected.Id,
|
||||
expected.Title,
|
||||
expected.TimeLimit,
|
||||
expected.MemoryLimit,
|
||||
|
||||
expected.Legend,
|
||||
expected.InputFormat,
|
||||
expected.OutputFormat,
|
||||
expected.Notes,
|
||||
expected.Scoring,
|
||||
|
||||
expected.LegendHtml,
|
||||
expected.InputFormatHtml,
|
||||
expected.OutputFormatHtml,
|
||||
expected.NotesHtml,
|
||||
expected.ScoringHtml,
|
||||
|
||||
expected.CreatedAt,
|
||||
expected.UpdatedAt)
|
||||
|
||||
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(expected.Id).WillReturnRows(rows)
|
||||
|
||||
problem, err := repo.GetProblemById(ctx, db, expected.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualExportedValues(t, expected, problem)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
_, err := repo.GetProblemById(ctx, db, id)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteProblemQuery).
|
||||
WithArgs(id).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteProblem(ctx, db, id)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteProblemQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
err := repo.DeleteProblem(ctx, db, id)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ListProblems(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := make([]*models.ProblemsListItem, 0)
|
||||
for i := 0; i < 10; i++ {
|
||||
problem := &models.ProblemsListItem{
|
||||
Id: int32(i + 1),
|
||||
Title: fmt.Sprintf("Test Problem %d", i+1),
|
||||
TimeLimit: 1000,
|
||||
MemoryLimit: 1024,
|
||||
SolvedCount: int32(123 * i),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
expected = append(expected, problem)
|
||||
}
|
||||
|
||||
filter := models.ProblemsFilter{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
var totalCount int32 = 10
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"title",
|
||||
"time_limit",
|
||||
"memory_limit",
|
||||
"solved_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns)
|
||||
for _, problem := range expected {
|
||||
rows = rows.AddRow(
|
||||
problem.Id,
|
||||
problem.Title,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
problem.SolvedCount,
|
||||
problem.CreatedAt,
|
||||
problem.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.ListProblemsQuery).WillReturnRows(rows)
|
||||
mock.ExpectQuery(repository.CountProblemsQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
|
||||
|
||||
problems, err := repo.ListProblems(ctx, db, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, problems.Problems)
|
||||
assert.Equal(t, models.Pagination{
|
||||
Page: 1,
|
||||
Total: 1,
|
||||
}, problems.Pagination)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var id int32 = 1
|
||||
|
||||
update := &models.ProblemUpdate{
|
||||
Title: sp("Test Problem"),
|
||||
TimeLimit: ip(1000),
|
||||
MemoryLimit: ip(1024),
|
||||
Legend: sp("Test Legend"),
|
||||
InputFormat: sp("Test Input Format"),
|
||||
OutputFormat: sp("Test Output Format"),
|
||||
Notes: sp("Test Notes"),
|
||||
Scoring: sp("Test Scoring"),
|
||||
LegendHtml: sp("Test Legend HTML"),
|
||||
InputFormatHtml: sp("Test Input Format HTML"),
|
||||
OutputFormatHtml: sp("Test Output Format HTML"),
|
||||
NotesHtml: sp("Test Notes HTML"),
|
||||
ScoringHtml: sp("Test Scoring HTML"),
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateProblemQuery).WithArgs(
|
||||
id,
|
||||
|
||||
update.Title,
|
||||
update.TimeLimit,
|
||||
update.MemoryLimit,
|
||||
|
||||
update.Legend,
|
||||
update.InputFormat,
|
||||
update.OutputFormat,
|
||||
update.Notes,
|
||||
update.Scoring,
|
||||
|
||||
update.LegendHtml,
|
||||
update.InputFormatHtml,
|
||||
update.OutputFormatHtml,
|
||||
update.NotesHtml,
|
||||
update.ScoringHtml,
|
||||
).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
err := repo.UpdateProblem(ctx, db, id, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func sp(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func ip(s int32) *int32 {
|
||||
return &s
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateProblem(ctx context.Context, title string) (int32, error)
|
||||
GetProblemById(ctx context.Context, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, id int32) error
|
||||
ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, id int32, problem *models.ProblemUpdate) error
|
||||
UploadProblem(ctx context.Context, id int32, archive []byte) error
|
||||
}
|
|
@ -1,337 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
type UseCase struct {
|
||||
problemRepo problems.Repository
|
||||
pandocClient pkg.PandocClient
|
||||
}
|
||||
|
||||
func NewUseCase(
|
||||
problemRepo problems.Repository,
|
||||
pandocClient pkg.PandocClient,
|
||||
) *UseCase {
|
||||
return &UseCase{
|
||||
problemRepo: problemRepo,
|
||||
pandocClient: pandocClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
|
||||
}
|
||||
|
||||
func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
|
||||
}
|
||||
|
||||
func (u *UseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate *models.ProblemUpdate) error {
|
||||
if isEmpty(*problemUpdate) {
|
||||
return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update")
|
||||
}
|
||||
|
||||
tx, err := u.problemRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
problem, err := u.problemRepo.GetProblemById(ctx, tx, id)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
statement := models.ProblemStatement{
|
||||
Legend: problem.Legend,
|
||||
InputFormat: problem.InputFormat,
|
||||
OutputFormat: problem.OutputFormat,
|
||||
Notes: problem.Notes,
|
||||
Scoring: problem.Scoring,
|
||||
}
|
||||
|
||||
if problemUpdate.Legend != nil {
|
||||
statement.Legend = *problemUpdate.Legend
|
||||
}
|
||||
if problemUpdate.InputFormat != nil {
|
||||
statement.InputFormat = *problemUpdate.InputFormat
|
||||
}
|
||||
if problemUpdate.OutputFormat != nil {
|
||||
statement.OutputFormat = *problemUpdate.OutputFormat
|
||||
}
|
||||
if problemUpdate.Notes != nil {
|
||||
statement.Notes = *problemUpdate.Notes
|
||||
}
|
||||
if problemUpdate.Scoring != nil {
|
||||
statement.Scoring = *problemUpdate.Scoring
|
||||
}
|
||||
|
||||
builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
if builtStatement.LegendHtml != problem.LegendHtml {
|
||||
problemUpdate.LegendHtml = &builtStatement.LegendHtml
|
||||
}
|
||||
if builtStatement.InputFormatHtml != problem.InputFormatHtml {
|
||||
problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml
|
||||
}
|
||||
if builtStatement.OutputFormatHtml != problem.OutputFormatHtml {
|
||||
problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml
|
||||
}
|
||||
if builtStatement.NotesHtml != problem.NotesHtml {
|
||||
problemUpdate.NotesHtml = &builtStatement.NotesHtml
|
||||
}
|
||||
if builtStatement.ScoringHtml != problem.ScoringHtml {
|
||||
problemUpdate.ScoringHtml = &builtStatement.ScoringHtml
|
||||
}
|
||||
|
||||
err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ProblemProperties struct {
|
||||
Title string `json:"name"`
|
||||
TimeLimit int32 `json:"timeLimit"`
|
||||
MemoryLimit int32 `json:"memoryLimit"`
|
||||
}
|
||||
|
||||
func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
|
||||
locale := "russian"
|
||||
defaultLocale := "english"
|
||||
var localeProblem, defaultProblem string
|
||||
var localeProperties, defaultProperties ProblemProperties
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
rc, err := zip.NewReader(r, int64(r.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testsZipBuf := new(bytes.Buffer)
|
||||
w := zip.NewWriter(testsZipBuf)
|
||||
|
||||
for _, f := range rc.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem.tex", locale) {
|
||||
localeProblem, err = readProblem(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem.tex", defaultLocale) {
|
||||
defaultProblem, err = readProblem(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", locale) {
|
||||
localeProperties, err = readProperties(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", defaultLocale) {
|
||||
defaultProperties, err = readProperties(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(f.Name, "tests/") {
|
||||
if err := w.Copy(f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
// testsZipBuf contains test files; this is for s3
|
||||
|
||||
localeProperties.MemoryLimit /= 1024 * 1024
|
||||
defaultProperties.MemoryLimit /= 1024 * 1024
|
||||
|
||||
problemUpdate := &models.ProblemUpdate{}
|
||||
if localeProblem != "" {
|
||||
problemUpdate.Legend = &localeProblem
|
||||
problemUpdate.Title = &localeProperties.Title
|
||||
problemUpdate.TimeLimit = &localeProperties.TimeLimit
|
||||
problemUpdate.MemoryLimit = &localeProperties.MemoryLimit
|
||||
} else {
|
||||
problemUpdate.Legend = &defaultProblem
|
||||
problemUpdate.Title = &defaultProperties.Title
|
||||
problemUpdate.TimeLimit = &defaultProperties.TimeLimit
|
||||
problemUpdate.MemoryLimit = &defaultProperties.MemoryLimit
|
||||
}
|
||||
if err := u.UpdateProblem(ctx, id, problemUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readProblem(f *zip.File) (string, error) {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rc.Close()
|
||||
problemData, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(problemData), nil
|
||||
}
|
||||
|
||||
func readProperties(f *zip.File) (ProblemProperties, error) {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return ProblemProperties{}, err
|
||||
}
|
||||
defer rc.Close()
|
||||
var properties ProblemProperties
|
||||
if err := json.NewDecoder(rc).Decode(&properties); err != nil {
|
||||
return ProblemProperties{}, err
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func isEmpty(p models.ProblemUpdate) bool {
|
||||
return p.Title == nil &&
|
||||
p.Legend == nil &&
|
||||
p.InputFormat == nil &&
|
||||
p.OutputFormat == nil &&
|
||||
p.Notes == nil &&
|
||||
p.Scoring == nil &&
|
||||
p.MemoryLimit == nil &&
|
||||
p.TimeLimit == nil
|
||||
}
|
||||
|
||||
func wrap(s string) string {
|
||||
return fmt.Sprintf("\\begin{document}\n%s\n\\end{document}\n", s)
|
||||
}
|
||||
|
||||
func trimSpaces(statement models.ProblemStatement) models.ProblemStatement {
|
||||
return models.ProblemStatement{
|
||||
Legend: strings.TrimSpace(statement.Legend),
|
||||
InputFormat: strings.TrimSpace(statement.InputFormat),
|
||||
OutputFormat: strings.TrimSpace(statement.OutputFormat),
|
||||
Notes: strings.TrimSpace(statement.Notes),
|
||||
Scoring: strings.TrimSpace(statement.Scoring),
|
||||
}
|
||||
}
|
||||
|
||||
func sanitize(statement models.Html5ProblemStatement) models.Html5ProblemStatement {
|
||||
p := bluemonday.UGCPolicy()
|
||||
|
||||
p.AllowAttrs("class").Globally()
|
||||
p.AllowAttrs("style").Globally()
|
||||
p.AllowStyles("text-align").MatchingEnum("center", "left", "right").Globally()
|
||||
p.AllowStyles("display").MatchingEnum("block", "inline", "inline-block").Globally()
|
||||
|
||||
p.AllowStandardURLs()
|
||||
p.AllowAttrs("cite").OnElements("blockquote", "q")
|
||||
p.AllowAttrs("href").OnElements("a", "area")
|
||||
p.AllowAttrs("src").OnElements("img")
|
||||
|
||||
if statement.LegendHtml != "" {
|
||||
statement.LegendHtml = p.Sanitize(statement.LegendHtml)
|
||||
}
|
||||
if statement.InputFormatHtml != "" {
|
||||
statement.InputFormatHtml = p.Sanitize(statement.InputFormatHtml)
|
||||
}
|
||||
if statement.OutputFormatHtml != "" {
|
||||
statement.OutputFormatHtml = p.Sanitize(statement.OutputFormatHtml)
|
||||
}
|
||||
if statement.NotesHtml != "" {
|
||||
statement.NotesHtml = p.Sanitize(statement.NotesHtml)
|
||||
}
|
||||
if statement.ScoringHtml != "" {
|
||||
statement.ScoringHtml = p.Sanitize(statement.ScoringHtml)
|
||||
}
|
||||
|
||||
return statement
|
||||
}
|
||||
|
||||
func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) {
|
||||
p = trimSpaces(p)
|
||||
|
||||
latex := models.ProblemStatement{}
|
||||
|
||||
if p.Legend != "" {
|
||||
latex.Legend = wrap(p.Legend)
|
||||
}
|
||||
if p.InputFormat != "" {
|
||||
latex.InputFormat = wrap(p.InputFormat)
|
||||
}
|
||||
if p.OutputFormat != "" {
|
||||
latex.OutputFormat = wrap(p.OutputFormat)
|
||||
}
|
||||
if p.Notes != "" {
|
||||
latex.Notes = wrap(p.Notes)
|
||||
}
|
||||
if p.Scoring != "" {
|
||||
latex.Scoring = wrap(p.Scoring)
|
||||
}
|
||||
|
||||
req := []string{
|
||||
latex.Legend,
|
||||
latex.InputFormat,
|
||||
latex.OutputFormat,
|
||||
latex.Notes,
|
||||
latex.Scoring,
|
||||
}
|
||||
|
||||
res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req)
|
||||
if err != nil {
|
||||
return models.Html5ProblemStatement{}, err
|
||||
}
|
||||
|
||||
if len(res) != len(req) {
|
||||
return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res))
|
||||
}
|
||||
|
||||
sanitizedStatement := sanitize(models.Html5ProblemStatement{
|
||||
LegendHtml: res[0],
|
||||
InputFormatHtml: res[1],
|
||||
OutputFormatHtml: res[2],
|
||||
NotesHtml: res[3],
|
||||
ScoringHtml: res[4],
|
||||
})
|
||||
|
||||
return sanitizedStatement, nil
|
||||
}
|
57
internal/services/contest.go
Normal file
57
internal/services/contest.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
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
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewContestService(
|
||||
contestStorage ContestStorage,
|
||||
permissionService IPermissionService,
|
||||
) *ContestService {
|
||||
return &ContestService{
|
||||
contestStorage: contestStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *ContestService) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.CreateContest(ctx, contest)
|
||||
}
|
||||
|
||||
func (service *ContestService) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.ReadContestById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *ContestService) UpdateContest(ctx context.Context, contest *models.Contest) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.UpdateContest(ctx, contest)
|
||||
}
|
||||
|
||||
func (service *ContestService) DeleteContest(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.contestStorage.DeleteContest(ctx, id)
|
||||
}
|
28
internal/services/language.go
Normal file
28
internal/services/language.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type LanguageStorage interface {
|
||||
ReadLanguageById(ctx context.Context, id int32) (*models.Language, error)
|
||||
}
|
||||
|
||||
type LanguageService struct {
|
||||
languageStorage LanguageStorage
|
||||
}
|
||||
|
||||
func NewLanguageService(
|
||||
languageStorage LanguageStorage,
|
||||
) *LanguageService {
|
||||
return &LanguageService{
|
||||
languageStorage: languageStorage,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *LanguageService) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) {
|
||||
//userId := ctx.Value("user_id").(int32)
|
||||
panic("access control is not implemented yet")
|
||||
return service.languageStorage.ReadLanguageById(ctx, id)
|
||||
}
|
57
internal/services/participants.go
Normal file
57
internal/services/participants.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type ParticipantStorage interface {
|
||||
CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error)
|
||||
ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error)
|
||||
UpdateParticipant(ctx context.Context, participant *models.Participant) error
|
||||
DeleteParticipant(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type ParticipantService struct {
|
||||
participantStorage ParticipantStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewParticipantService(
|
||||
participantStorage ParticipantStorage,
|
||||
permissionService IPermissionService,
|
||||
) *ParticipantService {
|
||||
return &ParticipantService{
|
||||
participantStorage: participantStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *ParticipantService) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.CreateParticipant(ctx, participant)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.ReadParticipantById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) UpdateParticipant(ctx context.Context, participant *models.Participant) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.UpdateParticipant(ctx, participant)
|
||||
}
|
||||
|
||||
func (service *ParticipantService) DeleteParticipant(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.participantStorage.DeleteParticipant(ctx, id)
|
||||
}
|
39
internal/services/permission.go
Normal file
39
internal/services/permission.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/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)
|
||||
}
|
104
internal/services/problem.go
Normal file
104
internal/services/problem.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
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 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 {
|
||||
problemStorage ProblemStorage
|
||||
pandocClient PandocClient
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewProblemService(
|
||||
problemStorage ProblemStorage,
|
||||
pandocClient PandocClient,
|
||||
permissionService IPermissionService,
|
||||
) *ProblemService {
|
||||
return &ProblemService{
|
||||
problemStorage: problemStorage,
|
||||
pandocClient: pandocClient,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func extractUser(ctx context.Context) *models.User {
|
||||
return ctx.Value("user").(*models.User)
|
||||
}
|
||||
|
||||
func (service *ProblemService) 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 {
|
||||
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 {
|
||||
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 {
|
||||
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) {
|
||||
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 *ProblemService) 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 {
|
||||
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 {
|
||||
if err := service.CanDeleteProblem(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.problemStorage.DeleteProblem(ctx, id)
|
||||
}
|
57
internal/services/solution.go
Normal file
57
internal/services/solution.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type SolutionStorage interface {
|
||||
CreateSolution(ctx context.Context, solution models.Solution) (int32, error)
|
||||
ReadSolutionById(ctx context.Context, id int32) (models.Solution, error)
|
||||
RejudgeSolution(ctx context.Context, id int32) error
|
||||
DeleteSolution(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type SolutionService struct {
|
||||
solutionStorage SolutionStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewSolutionService(
|
||||
solutionStorage SolutionStorage,
|
||||
permissionService IPermissionService,
|
||||
) *SolutionService {
|
||||
return &SolutionService{
|
||||
solutionStorage: solutionStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *SolutionService) CreateSolution(ctx context.Context, solution models.Solution) (int32, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
|
||||
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.CreateSolution(ctx, solution)
|
||||
}
|
||||
|
||||
func (service *SolutionService) ReadSolutionById(ctx context.Context, id int32) (models.Solution, error) {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
|
||||
return models.Solution{}, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.ReadSolutionById(ctx, id)
|
||||
}
|
||||
|
||||
func (service *SolutionService) RejudgeSolution(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.RejudgeSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (service *SolutionService) DeleteSolution(ctx context.Context, id int32) error {
|
||||
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
|
||||
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
|
||||
}
|
||||
return service.solutionStorage.DeleteSolution(ctx, id)
|
||||
}
|
41
internal/services/task.go
Normal file
41
internal/services/task.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
)
|
||||
|
||||
type TaskStorage interface {
|
||||
CreateTask(ctx context.Context, task models.Task) (int32, error)
|
||||
DeleteTask(ctx context.Context, id int32) error
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
taskStorage TaskStorage
|
||||
permissionService IPermissionService
|
||||
}
|
||||
|
||||
func NewTaskService(
|
||||
taskStorage TaskStorage,
|
||||
permissionService IPermissionService,
|
||||
) *TaskService {
|
||||
return &TaskService{
|
||||
taskStorage: taskStorage,
|
||||
permissionService: permissionService,
|
||||
}
|
||||
}
|
||||
|
||||
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 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 service.taskStorage.DeleteTask(ctx, id)
|
||||
}
|
29
internal/services/user.go
Normal file
29
internal/services/user.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/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)
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ValkeyRepository struct {
|
||||
db valkey.Client
|
||||
}
|
||||
|
||||
func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
|
||||
return &ValkeyRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const SessionLifetime = time.Minute * 40
|
||||
|
||||
func (r *ValkeyRepository) CreateSession(ctx context.Context, session *models.Session) error {
|
||||
const op = "ValkeyRepository.CreateSession"
|
||||
|
||||
data, err := session.JSON()
|
||||
if err != nil {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "cannot marshal session")
|
||||
}
|
||||
|
||||
resp := r.db.Do(ctx, r.db.
|
||||
B().Set().
|
||||
Key(session.Key()).
|
||||
Value(string(data)).
|
||||
Exat(session.ExpiresAt).
|
||||
Build(),
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
if #result[2] == 0 then
|
||||
return nil
|
||||
else
|
||||
return redis.call('GET', result[2][1])
|
||||
end`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "ValkeyRepository.ReadSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(readSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
if err := resp.Error(); err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return nil, pkg.Wrap(pkg.ErrNotFound, err, op, "reading session")
|
||||
}
|
||||
return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
session := &models.Session{}
|
||||
|
||||
err := resp.DecodeJSON(session)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('EXPIRE', result[2][1], ARGV[2]) == 1`
|
||||
)
|
||||
|
||||
var (
|
||||
sessionLifetimeString = strconv.Itoa(int(SessionLifetime.Seconds()))
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.UpdateSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(updateSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const deleteSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('DEL', result[2][1]) == 1`
|
||||
|
||||
func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.DeleteSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
deleteUserSessionsScript = `local cursor = 0
|
||||
local dels = 0
|
||||
repeat
|
||||
local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1])
|
||||
for _,key in ipairs(result[2]) do
|
||||
redis.call('DEL', key)
|
||||
dels = dels + 1
|
||||
end
|
||||
cursor = tonumber(result[1])
|
||||
until cursor == 0
|
||||
return dels`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "ValkeyRepository.DeleteAllSessions"
|
||||
|
||||
userIdHash := (&models.Session{UserId: userId}).UserIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"github.com/valkey-io/valkey-go/mock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValkeyRepository_CreateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "SET" {
|
||||
return false
|
||||
}
|
||||
if cmd[1] != session.Key() {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != "EXAT" {
|
||||
return false
|
||||
}
|
||||
if cmd[4] != strconv.FormatInt(session.ExpiresAt.Unix(), 10) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.CreateSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_ReadSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
d, err := session.JSON()
|
||||
require.NoError(t, err)
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(string(d))))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(res.CreatedAt.Unix(), res.ExpiresAt.UnixNano())
|
||||
fmt.Println(session.CreatedAt.Unix(), session.ExpiresAt.UnixNano())
|
||||
require.EqualExportedValues(t, session, res)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
require.Empty(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_UpdateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteAllSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
type SessionsUC struct {
|
||||
sessionsRepo sessions.ValkeyRepository
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func NewUseCase(
|
||||
sessionRepo sessions.ValkeyRepository,
|
||||
cfg config.Config,
|
||||
) *SessionsUC {
|
||||
return &SessionsUC{
|
||||
sessionsRepo: sessionRepo,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) CreateSession(ctx context.Context, creation *models.Session) error {
|
||||
const op = "UseCase.CreateSession"
|
||||
|
||||
err := u.sessionsRepo.CreateSession(ctx, creation)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot create session")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "UseCase.ReadSession"
|
||||
|
||||
session, err := u.sessionsRepo.ReadSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "cannot read session")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.UpdateSession"
|
||||
|
||||
err := u.sessionsRepo.UpdateSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot update session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.DeleteSession"
|
||||
|
||||
err := u.sessionsRepo.DeleteSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "UseCase.DeleteAllSessions"
|
||||
|
||||
err := u.sessionsRepo.DeleteAllSessions(ctx, userId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete all sessions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ValkeyRepository interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
78
internal/storage/contests.go
Normal file
78
internal/storage/contests.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/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
|
||||
}
|
23
internal/storage/errhandling.go
Normal file
23
internal/storage/errhandling.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
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")
|
||||
}
|
28
internal/storage/language.go
Normal file
28
internal/storage/language.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/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
|
||||
}
|
79
internal/storage/participants.go
Normal file
79
internal/storage/participants.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ParticipantStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewParticipantStorage(db *sqlx.DB, logger *zap.Logger) *ParticipantStorage {
|
||||
return &ParticipantStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO participants
|
||||
(user_id,contest_id,name)
|
||||
VALUES (?,?,?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
participant.UserId,
|
||||
participant.ContestId,
|
||||
participant.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 *ParticipantStorage) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
|
||||
var participant models.Participant
|
||||
query := storage.db.Rebind("SELECT * from participants WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &participant, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &participant, nil
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) UpdateParticipant(ctx context.Context, id int32, participant models.Participant) error {
|
||||
query := storage.db.Rebind("UPDATE participants SET name=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, participant.Name)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *ParticipantStorage) DeleteParticipant(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM participants WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
110
internal/storage/problem.go
Normal file
110
internal/storage/problem.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ProblemStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewProblemStorage(db *sqlx.DB, logger *zap.Logger) *ProblemStorage {
|
||||
return &ProblemStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *ProblemStorage) 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 *ProblemStorage) 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 *ProblemStorage) 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 *ProblemStorage) 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
|
||||
}
|
114
internal/storage/solution.go
Normal file
114
internal/storage/solution.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SolutionStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewSolutionStorage(db *sqlx.DB, logger *zap.Logger) *SolutionStorage {
|
||||
return &SolutionStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: testing graph
|
||||
|
||||
func (storage *SolutionStorage) updateResult(ctx context.Context, participantId int32, taskId int32) error {
|
||||
tx, err := storage.db.Beginx()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
query := tx.Rebind("UPDATE participant_subtask AS ps SET best_score = (SELECT COALESCE(max(score),0) FROM subtaskruns WHERE subtask_id = ps.subtask_id AND solution_id IN (SELECT id FROM solutions WHERE participant_id=ps.participant_id)) WHERE participant_id = 2 AND subtask_id IN (SELECT id FROM subtasks WHERE task_id = 2)")
|
||||
tx.QueryxContext(ctx, query, participantId, taskId)
|
||||
query = tx.Rebind("UPDATE participant_task SET best_score = (select max(best_score) from participant_subtask WHERE participant_id = ? AND subtask_id IN (SELECT id FROM subtask WHERE task_id = ?)) WHERE participant_id = ? AND task_id = ?")
|
||||
tx.QueryxContext(ctx, query, participantId, taskId, participantId, taskId)
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
//TODO: add testing tree
|
||||
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 *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, handlePgErr(err)
|
||||
}
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) RejudgeSolution(ctx context.Context, id int32) error {
|
||||
tx, err := storage.db.Beginx()
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
query := tx.Rebind("UPDATE solutions SET result = ? WHERE id = ?")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
query = tx.Rebind("UPDATE subtaskruns SET result = ?,score = 0 WHERE solution_id = ?")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
query = tx.Rebind("UPDATE testruns SET result = ?, score = 0 WHERE testgrouprun_id IN (SELECT id FROM tesgrouprun WHERE solution_id = ?)")
|
||||
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
|
||||
err = tx.Commit()
|
||||
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 handlePgErr(err)
|
||||
}
|
||||
storage.updateResult(ctx, *solution.ParticipantId, *solution.TaskId) // FIXME
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *SolutionStorage) DeleteSolution(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM solutions WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
80
internal/storage/task.go
Normal file
80
internal/storage/task.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TaskStorage struct {
|
||||
db *sqlx.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTaskStorage(db *sqlx.DB, logger *zap.Logger) *TaskStorage {
|
||||
return &TaskStorage{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) CreateTask(ctx context.Context, task *models.Task) (int32, error) {
|
||||
query := storage.db.Rebind(`
|
||||
INSERT INTO tasks
|
||||
(contest_id,problem_id,position,position_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`)
|
||||
|
||||
rows, err := storage.db.QueryxContext(
|
||||
ctx,
|
||||
query,
|
||||
task.ContestId,
|
||||
task.ProblemId,
|
||||
task.Position,
|
||||
task.PositionName,
|
||||
)
|
||||
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 *TaskStorage) ReadTaskById(ctx context.Context, id int32) (*models.Task, error) {
|
||||
var task models.Task
|
||||
query := storage.db.Rebind("SELECT * from tasks WHERE id=? LIMIT 1")
|
||||
err := storage.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err)
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) UpdateTask(ctx context.Context, id int32, task models.Task) error {
|
||||
query := storage.db.Rebind("UPDATE tasks SET position=?,position_name=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, task.Position, task.PositionName)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *TaskStorage) DeleteTask(ctx context.Context, id int32) error {
|
||||
query := storage.db.Rebind("DELETE FROM tasks WHERE id=?")
|
||||
_, err := storage.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
42
internal/storage/user.go
Normal file
42
internal/storage/user.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/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
|
||||
}
|
155
internal/transport/interceptors.go
Normal file
155
internal/transport/interceptors.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
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, lib.ErrNotFound) {
|
||||
user = &models.User{
|
||||
UserId: lib.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, lib.ErrValidationFailed):
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, lib.ErrInternal):
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
case errors.Is(err, lib.ErrExternal):
|
||||
return status.Error(codes.Unavailable, err.Error())
|
||||
case errors.Is(err, lib.ErrNoPermission):
|
||||
return status.Error(codes.PermissionDenied, err.Error())
|
||||
case errors.Is(err, lib.ErrUnknown):
|
||||
return status.Error(codes.Unknown, err.Error())
|
||||
case errors.Is(err, lib.ErrDeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, err.Error())
|
||||
case errors.Is(err, lib.ErrNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, lib.ErrAlreadyExists):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Is(err, lib.ErrConflict):
|
||||
return status.Error(codes.Unimplemented, err.Error())
|
||||
case errors.Is(err, lib.ErrUnimplemented):
|
||||
return status.Error(codes.Unimplemented, err.Error())
|
||||
case errors.Is(err, lib.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)
|
||||
}
|
||||
}
|
162
internal/transport/problem.go
Normal file
162
internal/transport/problem.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (s *TesterServer) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error {
|
||||
ctx := server.Context()
|
||||
|
||||
if err := s.problemService.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 := s.problemService.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 (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) {
|
||||
problem, err := s.problemService.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: AsTimestampP(problem.CreatedAt),
|
||||
UpdatedAt: AsTimestampP(problem.UpdatedAt),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
//func (s *TesterServer) 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 (s *TesterServer) DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) {
|
||||
err := s.problemService.DeleteProblem(ctx, req.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
95
internal/transport/server.go
Normal file
95
internal/transport/server.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg/models"
|
||||
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type ProblemService interface {
|
||||
CanCreateProblem(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
|
||||
}
|
||||
|
||||
type SessionClient interface {
|
||||
Read(ctx context.Context,
|
||||
in *sessionv1.ReadSessionRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*sessionv1.ReadSessionResponse, error)
|
||||
}
|
||||
|
||||
type UserService interface {
|
||||
CreateUser(ctx context.Context, user *models.User) error
|
||||
ReadUserById(ctx context.Context, userId int32) (*models.User, error)
|
||||
}
|
||||
|
||||
type TesterServer struct {
|
||||
problemv1.UnimplementedProblemServiceServer
|
||||
problemService ProblemService
|
||||
|
||||
sessionClient SessionClient
|
||||
userService UserService
|
||||
|
||||
grpcServer *grpc.Server
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTesterServer(
|
||||
problemService ProblemService,
|
||||
sessionClient SessionClient,
|
||||
userService UserService,
|
||||
logger *zap.Logger,
|
||||
) *TesterServer {
|
||||
server := &TesterServer{
|
||||
problemService: problemService,
|
||||
sessionClient: sessionClient,
|
||||
userService: userService,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(server.ErrUnwrappingUnaryInterceptor()),
|
||||
grpc.StreamInterceptor(server.ErrUnwrappingStreamInterceptor()),
|
||||
grpc.UnaryInterceptor(server.AuthUnaryInterceptor()),
|
||||
grpc.StreamInterceptor(server.AuthStreamInterceptor()),
|
||||
)
|
||||
|
||||
problemv1.RegisterProblemServiceServer(grpcServer, server)
|
||||
|
||||
server.grpcServer = grpcServer
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *TesterServer) Start(lis net.Listener) error {
|
||||
return s.grpcServer.Serve(lis)
|
||||
}
|
||||
|
||||
func (s *TesterServer) Stop() {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
|
||||
func AsTimeP(t *timestamppb.Timestamp) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
tt := t.AsTime()
|
||||
return &tt
|
||||
}
|
||||
|
||||
func AsTimestampP(t *time.Time) *timestamppb.Timestamp {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(*t)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UsersHandlers interface {
|
||||
ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error
|
||||
CreateUser(c *fiber.Ctx) error
|
||||
DeleteUser(c *fiber.Ctx, id int32) error
|
||||
GetUser(c *fiber.Ctx, id int32) error
|
||||
UpdateUser(c *fiber.Ctx, id int32) error
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
usersUC users.UseCase
|
||||
}
|
||||
|
||||
func NewHandlers(usersUC users.UseCase) *Handlers {
|
||||
return &Handlers{
|
||||
usersUC: usersUC,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
sessionKey = "session"
|
||||
)
|
||||
|
||||
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
|
||||
const op = "sessionFromCtx"
|
||||
|
||||
session, ok := ctx.Value(sessionKey).(*models.Session)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateUser(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req = &testerv1.CreateUserRequest{}
|
||||
err := c.BodyParser(req)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
id, err := h.usersUC.CreateUser(ctx,
|
||||
&models.UserCreation{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Role: models.RoleStudent,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateUserResponse{Id: id})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetUser(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
|
||||
user, err := h.usersUC.ReadUserById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetUserResponse{
|
||||
User: UserDTO(*user),
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateUser(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin:
|
||||
var req = &testerv1.UpdateUserRequest{}
|
||||
err := c.BodyParser(req)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = h.usersUC.UpdateUser(c.Context(), id, &models.UserUpdate{
|
||||
Username: req.Username,
|
||||
Role: RoleDTO(req.Role),
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteUser(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin:
|
||||
ctx := c.Context()
|
||||
|
||||
err := h.usersUC.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
usersList, err := h.usersUC.ListUsers(c.Context(), models.UsersListFilters{
|
||||
PageSize: params.PageSize,
|
||||
Page: params.Page,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListUsersResponse{
|
||||
Users: make([]testerv1.User, len(usersList.Users)),
|
||||
Pagination: PaginationDTO(usersList.Pagination),
|
||||
}
|
||||
|
||||
for i, user := range usersList.Users {
|
||||
resp.Users[i] = UserDTO(*user)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func RoleDTO(i *int32) *models.Role {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
ii := models.Role(*i)
|
||||
return &ii
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
// UserDTO sanitizes password
|
||||
func UserDTO(u models.User) testerv1.User {
|
||||
return testerv1.User{
|
||||
Id: u.Id,
|
||||
Username: u.Username,
|
||||
Role: int32(u.Role),
|
||||
CreatedAt: u.CreatedAt,
|
||||
ModifiedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
Rebind(query string) string
|
||||
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
|
||||
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
}
|
||||
|
||||
type Tx interface {
|
||||
Querier
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
BeginTx(ctx context.Context) (Tx, error)
|
||||
DB() Querier
|
||||
CreateUser(ctx context.Context, q Querier, user *models.UserCreation) (int32, error)
|
||||
ReadUserByUsername(ctx context.Context, q Querier, username string) (*models.User, error)
|
||||
ReadUserById(ctx context.Context, q Querier, id int32) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, q Querier, id int32, update *models.UserUpdate) error
|
||||
DeleteUser(ctx context.Context, q Querier, id int32) error
|
||||
ListUsers(ctx context.Context, q Querier, filters models.UsersListFilters) (*models.UsersList, error)
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
_db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sqlx.DB) *Repository {
|
||||
return &Repository{
|
||||
_db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) BeginTx(ctx context.Context) (users.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DB() users.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const CreateUserQuery = `
|
||||
INSERT INTO users
|
||||
(username, hashed_pwd, role)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *Repository) CreateUser(ctx context.Context, q users.Querier, user *models.UserCreation) (int32, error) {
|
||||
const op = "Caller.CreateUser"
|
||||
|
||||
rows, err := q.QueryxContext(
|
||||
ctx,
|
||||
CreateUserQuery,
|
||||
user.Username,
|
||||
user.Password,
|
||||
user.Role,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const ReadUserByUsernameQuery = "SELECT * from users WHERE username=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) ReadUserByUsername(ctx context.Context, q users.Querier, username string) (*models.User, error) {
|
||||
const op = "Caller.ReadUserByUsername"
|
||||
|
||||
var user models.User
|
||||
err := q.GetContext(ctx, &user, ReadUserByUsernameQuery, username)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
const ReadUserByIdQuery = "SELECT * from users WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) ReadUserById(ctx context.Context, q users.Querier, id int32) (*models.User, error) {
|
||||
const op = "Caller.ReadUserById"
|
||||
|
||||
var user models.User
|
||||
err := q.GetContext(ctx, &user, ReadUserByIdQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
const UpdateUserQuery = `
|
||||
UPDATE users
|
||||
SET username = COALESCE($1, username),
|
||||
role = COALESCE($2, role)
|
||||
WHERE id = $3
|
||||
`
|
||||
|
||||
func (r *Repository) UpdateUser(ctx context.Context, q users.Querier, id int32, update *models.UserUpdate) error {
|
||||
const op = "Caller.UpdateUser"
|
||||
|
||||
_, err := q.ExecContext(
|
||||
ctx,
|
||||
UpdateUserQuery,
|
||||
update.Username,
|
||||
update.Role,
|
||||
id,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const DeleteUserQuery = "DELETE FROM users WHERE id = $1"
|
||||
|
||||
func (r *Repository) DeleteUser(ctx context.Context, q users.Querier, id int32) error {
|
||||
const op = "Caller.DeleteUser"
|
||||
|
||||
_, err := q.ExecContext(ctx, DeleteUserQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListUsersQuery = "SELECT * FROM users LIMIT $1 OFFSET $2"
|
||||
CountUsersQuery = "SELECT COUNT(*) FROM users"
|
||||
)
|
||||
|
||||
func (r *Repository) ListUsers(ctx context.Context, q users.Querier, filters models.UsersListFilters) (*models.UsersList, error) {
|
||||
const op = "Caller.ListUsers"
|
||||
|
||||
list := make([]*models.User, 0)
|
||||
err := q.SelectContext(ctx, &list, ListUsersQuery, filters.PageSize, filters.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, CountUsersQuery)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.UsersList{
|
||||
Users: list,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filters.PageSize),
|
||||
Page: filters.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
|
||||
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
assert.NoError(t, err)
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
return sqlxDB, mock
|
||||
}
|
||||
|
||||
func TestRepository_CreateUser(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var expectedId int32 = 1
|
||||
user := &models.UserCreation{
|
||||
Username: "testuser",
|
||||
Password: "hashed-password",
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateUserQuery).
|
||||
WithArgs(user.Username, sqlmock.AnyArg(), user.Role).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateUser(ctx, db, user)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ReadUserByUsername(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := &models.User{
|
||||
Id: 1,
|
||||
Username: "testuser",
|
||||
HashedPassword: "hashed-password",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"username",
|
||||
"hashed_pwd",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"role",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).AddRow(
|
||||
expected.Id,
|
||||
expected.Username,
|
||||
expected.HashedPassword,
|
||||
expected.CreatedAt,
|
||||
expected.UpdatedAt,
|
||||
expected.Role,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(expected.Username).WillReturnRows(rows)
|
||||
|
||||
user, err := repo.ReadUserByUsername(ctx, db, expected.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, user)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
username := "testuser"
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(username).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
user, err := repo.ReadUserByUsername(ctx, db, username)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ReadUserById(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := &models.User{
|
||||
Id: 1,
|
||||
Username: "testuser",
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByIdQuery).
|
||||
WithArgs(expected.Id).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
|
||||
AddRow(expected.Id, expected.Username, expected.Role))
|
||||
|
||||
user, err := repo.ReadUserById(ctx, db, expected.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, user)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
userID := int32(1)
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByIdQuery).WithArgs(userID).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
user, err := repo.ReadUserById(ctx, db, userID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateUser(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
userID := int32(1)
|
||||
username := "testuser"
|
||||
role := models.RoleStudent
|
||||
update := &models.UserUpdate{
|
||||
Username: &username,
|
||||
Role: &role,
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateUserQuery).
|
||||
WithArgs(update.Username, update.Role, userID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.UpdateUser(ctx, db, userID, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteUser(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteUserQuery).
|
||||
WithArgs(userID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteUser(ctx, db, userID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ListUsers(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
filters := models.UsersListFilters{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
expectedUsers := []*models.User{
|
||||
{Id: 1, Username: "user1", Role: models.RoleAdmin},
|
||||
{Id: 2, Username: "user2", Role: models.RoleStudent},
|
||||
}
|
||||
totalCount := int32(2)
|
||||
|
||||
mock.ExpectQuery(repository.ListUsersQuery).
|
||||
WithArgs(filters.PageSize, filters.Offset()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
|
||||
AddRow(expectedUsers[0].Id, expectedUsers[0].Username, expectedUsers[0].Role).
|
||||
AddRow(expectedUsers[1].Id, expectedUsers[1].Username, expectedUsers[1].Role))
|
||||
|
||||
mock.ExpectQuery(repository.CountUsersQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
|
||||
|
||||
result, err := repo.ListUsers(ctx, db, filters)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedUsers, result.Users)
|
||||
assert.Equal(t, models.Pagination{Total: 1, Page: 1}, result.Pagination)
|
||||
})
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateUser(ctx context.Context, user *models.UserCreation) (int32, error)
|
||||
ReadUserById(ctx context.Context, id int32) (*models.User, error)
|
||||
ReadUserByUsername(ctx context.Context, username string) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error
|
||||
DeleteUser(ctx context.Context, id int32) error
|
||||
ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error)
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
type UsersUC struct {
|
||||
sessionRepo sessions.ValkeyRepository
|
||||
usersRepo users.Repository
|
||||
}
|
||||
|
||||
func NewUseCase(sessionRepo sessions.ValkeyRepository, usersRepo users.Repository) *UsersUC {
|
||||
return &UsersUC{
|
||||
sessionRepo: sessionRepo,
|
||||
usersRepo: usersRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsersUC) CreateUser(ctx context.Context, user *models.UserCreation) (int32, error) {
|
||||
const op = "UseCase.CreateUser"
|
||||
|
||||
err := user.HashPassword()
|
||||
if err != nil {
|
||||
return 0, pkg.Wrap(pkg.ErrBadInput, err, op, "bad password")
|
||||
}
|
||||
|
||||
id, err := u.usersRepo.CreateUser(ctx, u.usersRepo.DB(), user)
|
||||
if err != nil {
|
||||
return 0, pkg.Wrap(nil, err, op, "can't create user")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error) {
|
||||
const op = "UseCase.ListUsers"
|
||||
|
||||
usersList, err := u.usersRepo.ListUsers(ctx, u.usersRepo.DB(), filters)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't list users")
|
||||
}
|
||||
|
||||
return usersList, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error {
|
||||
const op = "UseCase.UpdateUser"
|
||||
|
||||
tx, err := u.usersRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot start transaction")
|
||||
}
|
||||
|
||||
err = u.usersRepo.UpdateUser(ctx, tx, id, update)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot update user")
|
||||
}
|
||||
err = u.sessionRepo.DeleteAllSessions(ctx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *UsersUC) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserByUsername"
|
||||
|
||||
user, err := u.usersRepo.ReadUserByUsername(ctx, u.usersRepo.DB(), username)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't read user by username")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) ReadUserById(ctx context.Context, id int32) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserById"
|
||||
|
||||
user, err := u.usersRepo.ReadUserById(ctx, u.usersRepo.DB(), id)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) DeleteUser(ctx context.Context, id int32) error {
|
||||
const op = "UseCase.DeleteUser"
|
||||
|
||||
tx, err := u.usersRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot start transaction")
|
||||
}
|
||||
|
||||
err = u.usersRepo.DeleteUser(ctx, tx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete user")
|
||||
}
|
||||
|
||||
err = u.sessionRepo.DeleteAllSessions(ctx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func ValidEmail(str string) error {
|
||||
emailAddress, err := mail.ParseAddress(str)
|
||||
if err != nil || emailAddress.Address != str {
|
||||
return errors.New("invalid email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidUsername(str string) error {
|
||||
if len(str) < 5 {
|
||||
return errors.New("too short username")
|
||||
}
|
||||
if len(str) > 70 {
|
||||
return errors.New("too long username")
|
||||
}
|
||||
if err := ValidEmail(str); err == nil {
|
||||
return errors.New("username cannot be an email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidPassword(str string) error {
|
||||
if len(str) < 5 {
|
||||
return errors.New("too short password")
|
||||
}
|
||||
if len(str) > 70 {
|
||||
return errors.New("too long password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidRole(role models.Role) error {
|
||||
switch role {
|
||||
case models.RoleAdmin:
|
||||
return nil
|
||||
case models.RoleTeacher:
|
||||
return nil
|
||||
case models.RoleStudent:
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid role")
|
||||
}
|
||||
*/
|
124
main.go
124
main.go
|
@ -1,34 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/auth"
|
||||
authHandlers "git.sch9.ru/new_gate/ms-tester/internal/auth/delivery/rest"
|
||||
authUseCase "git.sch9.ru/new_gate/ms-tester/internal/auth/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests"
|
||||
contestsHandlers "git.sch9.ru/new_gate/ms-tester/internal/contests/delivery/rest"
|
||||
contestsRepository "git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
contestsUseCase "git.sch9.ru/new_gate/ms-tester/internal/contests/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/middleware"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
problemsHandlers "git.sch9.ru/new_gate/ms-tester/internal/problems/delivery/rest"
|
||||
problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
|
||||
problemsUseCase "git.sch9.ru/new_gate/ms-tester/internal/problems/usecase"
|
||||
sessionsRepository "git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
|
||||
sessionsUseCase "git.sch9.ru/new_gate/ms-tester/internal/sessions/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
usersHandlers "git.sch9.ru/new_gate/ms-tester/internal/users/delivery/rest"
|
||||
usersRepository "git.sch9.ru/new_gate/ms-tester/internal/users/repository"
|
||||
usersUseCase "git.sch9.ru/new_gate/ms-tester/internal/users/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
"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"
|
||||
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -36,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
var cfg config.Config
|
||||
var cfg lib.Config
|
||||
err := cleanenv.ReadConfig(".env", &cfg)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error reading config: %s", err.Error()))
|
||||
|
@ -51,87 +36,44 @@ func main() {
|
|||
panic(fmt.Sprintf(`error reading config: env expected "prod" or "dev", got "%s"`, cfg.Env))
|
||||
}
|
||||
|
||||
logger.Info("connecting to postgres")
|
||||
db, err := pkg.NewPostgresDB(cfg.PostgresDSN)
|
||||
db, err := sqlx.Connect("pgx", cfg.PostgresDSN)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
logger.Info("successfully connected to postgres")
|
||||
|
||||
logger.Info("connecting to redis")
|
||||
vk, err := pkg.NewValkeyClient(cfg.RedisDSN)
|
||||
//contestStorage := storage.NewContestStorage(db, logger)
|
||||
//contestService := services.NewContestService(contestStorage)
|
||||
|
||||
pandocClient := lib.NewPandocClient(&http.Client{}, cfg.Pandoc)
|
||||
|
||||
grpcSessionClient, err := grpc.NewClient(cfg.Auth, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("error connecting to redis: %s", err.Error()))
|
||||
panic(err)
|
||||
}
|
||||
logger.Info("successfully connected to redis")
|
||||
sessionClient := sessionv1.NewSessionServiceClient(grpcSessionClient)
|
||||
|
||||
usersRepo := usersRepository.NewRepository(db)
|
||||
permissionService := services.NewPermissionService()
|
||||
|
||||
_, err = usersRepo.CreateUser(context.Background(),
|
||||
usersRepo.DB(), &models.UserCreation{
|
||||
Username: cfg.AdminUsername,
|
||||
Password: cfg.AdminPassword,
|
||||
Role: models.RoleAdmin,
|
||||
})
|
||||
problemStorage := storage.NewProblemStorage(db, logger)
|
||||
problemService := services.NewProblemService(problemStorage, pandocClient, permissionService)
|
||||
|
||||
userStorage := storage.NewUserStorage(db)
|
||||
userService := services.NewUserService(userStorage)
|
||||
|
||||
server := transport.NewTesterServer(problemService, sessionClient, userService, logger)
|
||||
|
||||
lis, err := net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("error creating admin user: %s", err.Error()))
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sessionsRepo := sessionsRepository.NewValkeyRepository(vk)
|
||||
sessionsUC := sessionsUseCase.NewUseCase(sessionsRepo, cfg)
|
||||
|
||||
usersUC := usersUseCase.NewUseCase(sessionsRepo, usersRepo)
|
||||
|
||||
authUC := authUseCase.NewUseCase(usersUC, sessionsUC)
|
||||
|
||||
pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc)
|
||||
|
||||
problemsRepo := problemsRepository.NewRepository(db)
|
||||
problemsUC := problemsUseCase.NewUseCase(problemsRepo, pandocClient)
|
||||
|
||||
contestsRepo := contestsRepository.NewRepository(db)
|
||||
contestsUC := contestsUseCase.NewContestUseCase(contestsRepo)
|
||||
|
||||
server := fiber.New()
|
||||
|
||||
type MergedHandlers struct {
|
||||
users.UsersHandlers
|
||||
auth.AuthHandlers
|
||||
contests.ContestsHandlers
|
||||
problems.ProblemsHandlers
|
||||
}
|
||||
|
||||
merged := MergedHandlers{
|
||||
usersHandlers.NewHandlers(usersUC),
|
||||
authHandlers.NewHandlers(authUC, cfg.JWTSecret),
|
||||
contestsHandlers.NewHandlers(problemsUC, contestsUC, cfg.JWTSecret),
|
||||
problemsHandlers.NewHandlers(problemsUC),
|
||||
}
|
||||
|
||||
testerv1.RegisterHandlersWithOptions(server, merged, testerv1.FiberServerOptions{
|
||||
Middlewares: []testerv1.MiddlewareFunc{
|
||||
fiberlogger.New(),
|
||||
middleware.AuthMiddleware(cfg.JWTSecret, sessionsUC),
|
||||
//rest.AuthMiddleware(cfg.JWTSecret, userUC),
|
||||
//cors.New(cors.Config{
|
||||
// AllowOrigins: "http://localhost:3000",
|
||||
// AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
// AllowHeaders: "Content-Type,Set-Cookie,Credentials",
|
||||
// AllowCredentials: true,
|
||||
//}),
|
||||
},
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := server.Listen(cfg.Address)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error()))
|
||||
if err := server.Start(lis); err != nil {
|
||||
logger.Fatal("error starting server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info(fmt.Sprintf("server started on %s", cfg.Address))
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
|
|
|
@ -1,5 +1,280 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
--CREATE TABLE IF NOT EXISTS languages
|
||||
--(
|
||||
-- id serial NOT NULL,
|
||||
-- name VARCHAR(60) NOT NULL,
|
||||
-- build_file_hash CHAR(128) NULL,
|
||||
-- execute_file_hash CHAR(128) NULL,
|
||||
-- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
-- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
--
|
||||
-- PRIMARY KEY (id)
|
||||
--);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problems
|
||||
(
|
||||
id serial NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
time_limit INT NOT NULL,
|
||||
memory_limit INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON problems USING BTREE (id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON contests USING BTREE (id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks
|
||||
(
|
||||
id SERIAL NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
problem_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
position INT NOT NULL,
|
||||
position_name VARCHAR(10) NOT NULL, -- problem name like: A,B,A1,etc
|
||||
UNIQUE (contest_id,problem_id),
|
||||
UNIQUE (contest_id,position),
|
||||
UNIQUE (contest_id,position_name),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON tasks USING BTREE (id);
|
||||
CREATE INDEX ON tasks USING BTREE (contest_id);
|
||||
CREATE INDEX ON tasks USING BTREE (problem_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS testgroups
|
||||
(
|
||||
id serial NOT NULL,
|
||||
problem_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
testing_strategy INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON testgroups USING BTREE (id);
|
||||
CREATE INDEX ON testgroups USING BTREE (problem_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subtasks
|
||||
(
|
||||
id SERIAL NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
|
||||
task_id INT REFERENCES tasks ON DELETE CASCADE,
|
||||
UNIQUE (contest_id,testgroup_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON subtasks USING BTREE (id);
|
||||
CREATE INDEX ON subtasks USING BTREE (contest_id);
|
||||
CREATE INDEX ON subtasks USING BTREE (testgroup_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participants
|
||||
(
|
||||
id serial NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
contest_id INT REFERENCES contests ON DELETE CASCADE,
|
||||
name varchar(200) NOT NULL,
|
||||
|
||||
UNIQUE (user_id,contest_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON participants USING BTREE (id);
|
||||
CREATE INDEX ON participants USING BTREE (user_id);
|
||||
CREATE INDEX ON participants USING BTREE (contest_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
task_id INT REFERENCES problems ON DELETE CASCADE,
|
||||
language_id INT NOT NULL,REFERENCES languages ON DELETE CASCADE,
|
||||
solution_hash CHAR(128) NOT NULL,
|
||||
result INT NOT NULL,
|
||||
score INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON solutions USING BTREE (id);
|
||||
CREATE INDEX ON solutions USING BTREE (id,participant_id,task_id,language_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
--problem_id INT NOT NULL,
|
||||
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON tests USING BTREE (id);
|
||||
CREATE INDEX ON tests USING BTREE (testgroup_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subtaskruns
|
||||
(
|
||||
id serial NOT NULL,
|
||||
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
|
||||
solution_id INT REFERENCES solutions ON DELETE CASCADE,
|
||||
result INT NOT NULL,
|
||||
score INT NOT NULL,
|
||||
|
||||
UNIQUE (subtask_id,solution_id),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON subtaskruns USING BTREE (id);
|
||||
CREATE INDEX ON subtaskruns USING BTREE (result);
|
||||
CREATE INDEX ON subtaskruns USING BTREE (solution_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS testruns
|
||||
(
|
||||
id serial NOT NULL,
|
||||
test_id INT REFERENCES tests ON DELETE CASCADE,
|
||||
--solution_id INT REFERENCES solutions ON DELETE CASCADE,
|
||||
subtaskrun_id INT REFERENCES subtaskruns ON DELETE CASCADE,
|
||||
result INT NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ON testruns USING BTREE (id);
|
||||
CREATE INDEX ON testruns USING BTREE (result);
|
||||
CREATE INDEX ON testruns USING BTREE (subtaskrun_id);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participant_subtask
|
||||
(
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
|
||||
--result INT NOT NULL,
|
||||
best_score INT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participant_task
|
||||
(
|
||||
participant_id INT REFERENCES participants ON DELETE CASCADE,
|
||||
task_id INT REFERENCES tasks ON DELETE CASCADE,
|
||||
--result INT NOT NULL,
|
||||
best_score INT NOT NULL,
|
||||
penalty INT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT NEW.id,id,0,0 FROM tasks WHERE contest_id=NEW.contest_id;
|
||||
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT NEW.id,id,0 FROM subtasks WHERE contest_id=NEW.contest_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_participant_trg AFTER INSERT ON participants FOR EACH ROW EXECUTE FUNCTION on_new_participant();
|
||||
|
||||
CREATE FUNCTION on_new_task() RETURNS TRIGGER AS
|
||||
$$ BEGIN
|
||||
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT id,NEW.id,0,0 FROM participants WHERE contest_id=NEW.contest_id;
|
||||
INSERT INTO subtasks(contest_id,testgroup_id,task_id) SELECT NEW.contest_id,id,NEW.id FROM testgroups WHERE problem_id = NEW.problem_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_task_trg AFTER INSERT ON tasks FOR EACH ROW EXECUTE FUNCTION on_new_task();
|
||||
|
||||
CREATE FUNCTION on_new_subtask() RETURNS TRIGGER AS
|
||||
$$ BEGIN
|
||||
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT id,NEW.id,0 FROM participants WHERE contest_id=NEW.contest_id;
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER new_subtask_trg AFTER INSERT ON subtasks FOR EACH ROW EXECUTE FUNCTION on_new_subtask();
|
||||
|
||||
|
||||
CREATE FUNCTION on_new_solution() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO subtaskruns (subtask_id,solution_id,result,score) (SELECT id,NEW.id,0,0 FROM subtasks WHERE task_id = NEW.task_id);
|
||||
--INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,str.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id IN (SELECT subtask_id FROM subtaskruns AS str WHERE solution_id=NEW.id)));
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_new_solution_trg AFTER INSERT ON solutions FOR EACH ROW EXECUTE FUNCTION on_new_solution();
|
||||
|
||||
CREATE FUNCTION on_new_subtaskrun() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,NEW.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id=NEW.subtask_id));
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_new_subtaskrun_trg AFTER INSERT ON subtaskruns FOR EACH ROW EXECUTE FUNCTION on_new_subtaskrun();
|
||||
|
||||
CREATE FUNCTION updated_at_update() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS
|
||||
$$
|
||||
|
@ -9,178 +284,33 @@ BEGIN
|
|||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION check_max_tasks() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS
|
||||
$$
|
||||
DECLARE
|
||||
max_on_contest_tasks_amount integer := 50;
|
||||
BEGIN
|
||||
IF (SELECT count(*)
|
||||
FROM tasks
|
||||
WHERE contest_id = NEW.contest_id) >= (
|
||||
max_on_contest_tasks_amount
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Exceeded max tasks for this contest';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id serial NOT NULL,
|
||||
username varchar(70) UNIQUE NOT NULL,
|
||||
hashed_pwd varchar(60) NOT NULL,
|
||||
role integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
CHECK (length(username) != 0 AND username = lower(username) AND username = trim(username)),
|
||||
CHECK (length(hashed_pwd) != 0),
|
||||
CHECK (role BETWEEN 0 AND 2)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_users_update
|
||||
BEFORE UPDATE
|
||||
ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problems
|
||||
(
|
||||
id serial NOT NULL,
|
||||
title varchar(64) NOT NULL,
|
||||
time_limit integer NOT NULL DEFAULT 1000,
|
||||
memory_limit integer NOT NULL DEFAULT 64,
|
||||
|
||||
legend varchar(10240) NOT NULL DEFAULT '',
|
||||
input_format varchar(10240) NOT NULL DEFAULT '',
|
||||
output_format varchar(10240) NOT NULL DEFAULT '',
|
||||
notes varchar(10240) NOT NULL DEFAULT '',
|
||||
scoring varchar(10240) NOT NULL DEFAULT '',
|
||||
|
||||
legend_html varchar(10240) NOT NULL DEFAULT '',
|
||||
input_format_html varchar(10240) NOT NULL DEFAULT '',
|
||||
output_format_html varchar(10240) NOT NULL DEFAULT '',
|
||||
notes_html varchar(10240) NOT NULL DEFAULT '',
|
||||
scoring_html varchar(10240) NOT NULL DEFAULT '',
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
CHECK (length(title) != 0),
|
||||
CHECK (memory_limit BETWEEN 4 and 1024),
|
||||
CHECK (time_limit BETWEEN 250 and 5000)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_problems_update
|
||||
BEFORE UPDATE
|
||||
ON problems
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests
|
||||
(
|
||||
id serial NOT NULL,
|
||||
title varchar(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
CHECK (length(title) != 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_contests_update
|
||||
BEFORE UPDATE
|
||||
ON contests
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks
|
||||
(
|
||||
id serial NOT NULL,
|
||||
problem_id integer REFERENCES problems (id) ON DELETE SET NULL,
|
||||
contest_id integer REFERENCES contests (id) ON DELETE SET NULL,
|
||||
position integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (problem_id, contest_id),
|
||||
UNIQUE (contest_id, position),
|
||||
CHECK (position >= 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER max_tasks_on_contest_check
|
||||
BEFORE INSERT
|
||||
ON tasks
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION check_max_tasks();
|
||||
|
||||
CREATE TRIGGER on_tasks_update
|
||||
BEFORE UPDATE
|
||||
ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS participants
|
||||
(
|
||||
id serial NOT NULL,
|
||||
user_id integer NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
contest_id integer NOT NULL REFERENCES contests (id) ON DELETE CASCADE,
|
||||
name varchar(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (user_id, contest_id),
|
||||
CHECK (length(name) != 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_participants_update
|
||||
BEFORE UPDATE
|
||||
ON participants
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
task_id integer REFERENCES tasks (id) ON DELETE SET NULL,
|
||||
participant_id integer REFERENCES participants (id) ON DELETE SET NULL,
|
||||
solution varchar(1048576) NOT NULL,
|
||||
state integer NOT NULL DEFAULT 1,
|
||||
score integer NOT NULL DEFAULT 0,
|
||||
penalty integer NOT NULL,
|
||||
time_stat integer NOT NULL DEFAULT 0,
|
||||
memory_stat integer NOT NULL DEFAULT 0,
|
||||
language integer NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER on_solutions_update
|
||||
BEFORE UPDATE
|
||||
ON solutions
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
--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
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS on_solutions_update ON solutions;
|
||||
DROP TABLE IF EXISTS solutions;
|
||||
DROP TRIGGER IF EXISTS on_participants_update ON participants;
|
||||
DROP TABLE IF EXISTS participants;
|
||||
DROP TRIGGER IF EXISTS on_tasks_update ON tasks;
|
||||
DROP TRIGGER IF EXISTS max_tasks_on_contest_check ON tasks;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
DROP TRIGGER IF EXISTS on_problems_update ON problems;
|
||||
DROP TABLE IF EXISTS problems;
|
||||
DROP TRIGGER IF EXISTS on_contests_update ON contests;
|
||||
DROP TABLE IF EXISTS contests;
|
||||
DROP TRIGGER IF EXISTS on_users_update ON users;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP FUNCTION IF EXISTS updated_at_update();
|
||||
DROP FUNCTION IF EXISTS check_max_tasks();
|
||||
DROP FUNCTION IF EXISTS updated_at_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_participant CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_task CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_subtask CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_solution CASCADE;
|
||||
DROP FUNCTION IF EXISTS on_new_subtaskrun CASCADE;
|
||||
DROP TABLE IF EXISTS tests CASCADE;
|
||||
DROP TABLE IF EXISTS solutions CASCADE;
|
||||
--DROP TABLE IF EXISTS languages CASCADE;
|
||||
DROP TABLE IF EXISTS testgroups CASCADE;
|
||||
DROP TABLE IF EXISTS testruns CASCADE;
|
||||
DROP TABLE IF EXISTS subtaskruns CASCADE;
|
||||
DROP TABLE IF EXISTS problems CASCADE;
|
||||
DROP TABLE IF EXISTS contests CASCADE;
|
||||
DROP TABLE IF EXISTS tasks CASCADE;
|
||||
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
|
44
opa/all.rego
Normal file
44
opa/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
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package pkg
|
|
@ -1,58 +0,0 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
NoPermission = errors.New("no permission")
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrUnhandled = errors.New("unhandled")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrBadInput = errors.New("bad input")
|
||||
ErrInternal = errors.New("internal")
|
||||
)
|
||||
|
||||
func Wrap(basic error, err error, op string, msg string) error {
|
||||
return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg))
|
||||
}
|
||||
|
||||
func ToREST(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrUnauthenticated):
|
||||
return http.StatusUnauthorized
|
||||
case errors.Is(err, ErrBadInput):
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, ErrNotFound):
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, ErrInternal):
|
||||
return http.StatusInternalServerError
|
||||
case errors.Is(err, NoPermission):
|
||||
return http.StatusForbidden
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func HandlePgErr(err error, op string) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
|
||||
return Wrap(ErrBadInput, err, op, pgErr.Message)
|
||||
}
|
||||
if pgerrcode.IsNoData(pgErr.Code) {
|
||||
return Wrap(ErrNotFound, err, op, pgErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Wrap(ErrNotFound, err, op, "no rows found")
|
||||
}
|
||||
|
||||
return Wrap(ErrUnhandled, err, op, "unexpected error")
|
||||
}
|
10
pkg/models/contest.go
Normal file
10
pkg/models/contest.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Contest struct {
|
||||
Id *int `db:"id"`
|
||||
Name *string `db:"name"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at"`
|
||||
}
|
14
pkg/models/language.go
Normal file
14
pkg/models/language.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
type Language struct {
|
||||
Name string
|
||||
CompileCmd []string //source: src;result:executable
|
||||
RunCmd []string //source: executable
|
||||
}
|
||||
|
||||
var Languages = [...]Language {
|
||||
{Name : "gcc std=c90",
|
||||
CompileCmd : []string{"gcc", "src","-std=c90","-o","executable"},
|
||||
RunCmd : []string{"executable"}},
|
||||
}
|
||||
|
8
pkg/models/participant.go
Normal file
8
pkg/models/participant.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
type Participant struct {
|
||||
Id *int32 `db:"id"`
|
||||
UserId *int32 `db:"user_id"`
|
||||
ContestId *int32 `db:"contest_id"`
|
||||
Name *string `db:"name"`
|
||||
}
|
13
pkg/models/problem.go
Normal file
13
pkg/models/problem.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Problem struct {
|
||||
Id *int32 `db:"id"`
|
||||
Name *string `db:"name"`
|
||||
Description *string `db:"description"`
|
||||
TimeLimit *int32 `db:"time_limit"`
|
||||
MemoryLimit *int32 `db:"memory_limit"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at"`
|
||||
}
|
28
pkg/models/result.go
Normal file
28
pkg/models/result.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
)
|
||||
|
||||
type Result int32
|
||||
|
||||
const (
|
||||
NotTested Result = 0 // change only with schema change
|
||||
Accepted Result = 1
|
||||
CompilationError Result = 2
|
||||
MemoryLimitExceeded Result = 3
|
||||
TimeLimitExceeded Result = 4
|
||||
SystemFailDuringTesting Result = 5
|
||||
Testing Result = 6
|
||||
)
|
||||
|
||||
var ErrBadResult = errors.New("bad result")
|
||||
|
||||
func (result Result) Valid() error {
|
||||
switch result {
|
||||
case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
|
||||
return nil
|
||||
}
|
||||
return lib.ServiceError(ErrBadResult, lib.ErrValidationFailed, "bad result")
|
||||
}
|
53
pkg/models/role.go
Normal file
53
pkg/models/role.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/lib"
|
||||
)
|
||||
|
||||
type Role int32
|
||||
|
||||
const (
|
||||
RoleSpectator Role = 0
|
||||
RoleParticipant Role = 1
|
||||
RoleModerator Role = 2
|
||||
RoleAdmin Role = 3
|
||||
)
|
||||
|
||||
func (role Role) IsAdmin() bool {
|
||||
return role == RoleAdmin
|
||||
}
|
||||
|
||||
func (role Role) IsModerator() bool {
|
||||
return role == RoleModerator
|
||||
}
|
||||
|
||||
func (role Role) IsParticipant() bool {
|
||||
return role == RoleParticipant
|
||||
}
|
||||
|
||||
func (role Role) IsSpectator() bool {
|
||||
return role == RoleSpectator
|
||||
}
|
||||
|
||||
func (role Role) AtLeast(other Role) bool {
|
||||
return role >= other
|
||||
}
|
||||
|
||||
func (role Role) AtMost(other Role) bool {
|
||||
return role <= other
|
||||
}
|
||||
|
||||
var ErrBadRole = errors.New("bad role")
|
||||
|
||||
func (role Role) Valid() error {
|
||||
switch role {
|
||||
case RoleSpectator, RoleParticipant, RoleModerator, RoleAdmin:
|
||||
return nil
|
||||
}
|
||||
return lib.ServiceError(ErrBadRole, lib.ErrValidationFailed, "bad role")
|
||||
}
|
||||
|
||||
func (role Role) AsPointer() *Role {
|
||||
return &role
|
||||
}
|
14
pkg/models/solution.go
Normal file
14
pkg/models/solution.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Solution struct {
|
||||
Id *int32 `db:"id"`
|
||||
ParticipantId *int32 `db:"participant_id"`
|
||||
TaskId *int32 `db:"task_id"`
|
||||
LanguageId *int32 `db:"language_id"`
|
||||
SolutionHash *string `db:"solution_hash"`
|
||||
Result *int32 `db:"result"`
|
||||
Score *int32 `db:"score"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue