first adding

This commit is contained in:
Pavel 2024-02-19 20:48:04 +03:00
parent 7e0ebc99f6
commit d78fd38511
21 changed files with 2096 additions and 0 deletions

21
.gitignore vendored Normal file

@ -0,0 +1,21 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
squiz
.idea/
gen
worker/worker
storer/storer
answerer/answerer

158
app/app.go Normal file

@ -0,0 +1,158 @@
package app
import (
"context"
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/skeris/appInit"
"github.com/themakers/hlog"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/penahub_common/privilege"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/healthchecks"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/clients/auth"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/service"
)
type App struct {
logger *zap.Logger
err chan error
}
func (a App) GetLogger() *zap.Logger {
return a.logger
}
func (a App) GetErr() chan error {
return a.err
}
var (
errInvalidOptions = errors.New("invalid options")
)
var zapOptions = []zap.Option{
zap.AddCaller(),
zap.AddCallerSkip(2),
zap.AddStacktrace(zap.ErrorLevel),
}
var _ appInit.CommonApp = (*App)(nil)
type Options struct {
LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
IsProd bool `env:"IS_PROD" default:"false"`
NumberPort string `env:"PORT" default:"1488"`
CrtFile string `env:"CRT" default:"server.crt"`
KeyFile string `env:"KEY" default:"server.key"`
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
HubAdminUrl string `env:"HUB_ADMIN_URL"`
ServiceName string `env:"SERVICE_NAME" default:"squiz"`
AuthServiceURL string `env:"AUTH_URL"`
}
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
var (
err, workerErr error
zapLogger *zap.Logger
errChan = make(chan error)
options Options
ok bool
)
if options, ok = opts.(Options); !ok {
return App{}, errInvalidOptions
}
if options.LoggerProdMode {
zapLogger, err = zap.NewProduction(zapOptions...)
if err != nil {
return nil, err
}
} else {
zapLogger, err = zap.NewDevelopment(zapOptions...)
if err != nil {
return nil, err
}
}
zapLogger = zapLogger.With(
zap.String("SvcCommit", ver.Commit),
zap.String("SvcVersion", ver.Release),
zap.String("SvcBuildTime", ver.BuildTime),
)
logger := hlog.New(zapLogger)
logger.Emit(InfoSvcStarted{})
authClient := auth.NewAuthClient(options.AuthServiceURL)
pgdal, err := dal.New(ctx, options.PostgresCredentials, authClient)
if err != nil {
fmt.Println("NEW", err)
return nil, err
}
if err := pgdal.Init(); err != nil {
fmt.Println("INIT", err)
return nil, err
}
clientData := privilege.Client{
URL: options.HubAdminUrl,
ServiceName: options.ServiceName,
Privileges: model.Privileges,
}
fiberClient := &fiber.Client{}
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
err = privilegeController.PublishPrivileges()
if err != nil {
logger.Module("Failed to publish privileges")
}
app := fiber.New()
app.Use(middleware.JWTAuth())
app.Get("/liveness", healthchecks.Liveness)
app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
svc := service.New(pgdal)
svc.Register(app)
logger.Emit(InfoSvcReady{})
go func() {
defer func() {
if pgdal != nil {
pgdal.Close()
}
err := app.Shutdown()
logger.Emit(InfoSvcShutdown{Signal: err.Error()})
}()
if options.IsProd {
if err := app.ListenTLS(fmt.Sprintf(":%s", options.NumberPort), options.CrtFile, options.KeyFile); err != nil {
logger.Emit(ErrorCanNotServe{
Err: err,
})
errChan <- err
}
} else {
if err := app.Listen(fmt.Sprintf(":%s", options.NumberPort)); err != nil {
logger.Emit(ErrorCanNotServe{
Err: err,
})
errChan <- err
}
}
errChan <- nil
}()
// todo implement helper func for service app type. such as server preparing, logger preparing, healthchecks and etc.
return &App{
logger: zapLogger,
err: errChan,
}, err
}

10
app/logrecords.go Normal file

@ -0,0 +1,10 @@
package app
type InfoSvcStarted struct{}
type InfoSvcReady struct{}
type InfoSvcShutdown struct {
Signal string
}
type ErrorCanNotServe struct {
Err error
}

44
clients/auth/auth.go Normal file

@ -0,0 +1,44 @@
package auth
import (
"fmt"
"github.com/gofiber/fiber/v2"
"log"
)
type AuthClient struct {
URL string
}
type User struct {
Email string `json:"login"`
}
func NewAuthClient(url string) *AuthClient {
if url == "" {
log.Panicln("url is nil on <NewAuthClient>")
}
return &AuthClient{
URL: url,
}
}
func (client *AuthClient) GetUserEmail(userID string) (string, error) {
fiberClient := fiber.Client{}
userURL := fmt.Sprintf("%s/%s", client.URL, userID)
var user User
status, resp, errs := fiberClient.Get(userURL).Struct(&user)
if len(errs) > 0 {
return "", errs[0]
}
if status != fiber.StatusOK {
return "", fmt.Errorf("unexpected status code: %d, user: %s, reesponse: %s", status, userID, string(resp))
}
return user.Email, nil
}

@ -0,0 +1,12 @@
services:
postgres:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: Redalert2
POSTGRES_USER: squiz
POSTGRES_DB: squiz
app:
image: penahub.gitlab.yandexcloud.net:5050/backend/squiz:latest
ports:
- 1488:1488

@ -0,0 +1,77 @@
services:
core:
hostname: squiz-core
container_name: squiz-core
image: $CI_REGISTRY_IMAGE/main-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
HUB_ADMIN_URL: 'http://10.8.0.8:59303'
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PORT: 1488
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
AUTH_URL: 'http://10.8.0.8:59300/user'
ports:
- 10.8.0.9:1488:1488
storer:
hostname: squiz-storer
container_name: squiz-storer
image: $CI_REGISTRY_IMAGE/main-storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1489
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
ports:
- 10.8.0.9:1489:1489
worker:
hostname: squiz-worker
container_name: squiz-worker
image: $CI_REGISTRY_IMAGE/main-worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
KAFKA_BROKER: '10.8.0.8:9092'
KAFKA_TOPIC: 'tariffs'
QUIZ_ID: quizCnt
AMOUNT: 10
UNLIM_ID: quizUnlimTime
REDIS_HOST: '10.8.0.9:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
SMTP_API_URL: 'https://api.smtp.bz/v1/smtp/send'
SMTP_HOST: 'connect.smtp.bz'
SMTP_PORT: '587'
SMTP_UNAME: 'team@pena.digital'
SMTP_PASS: 'AyMfwqA9LkQH'
SMTP_API_KEY: '8tv2xcsfCMBX3TCQxzgeeEwAEYyQrPUp0ggw'
SMTP_SENDER: 'recovery@noreply.pena.digital'
CUSTOMER_SERVICE_ADDRESS: 'http://10.8.0.8:8065/'
answerer:
hostname: squiz-answerer
container_name: squiz-answerer
image: $CI_REGISTRY_IMAGE/main-answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1490
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.8.0.9 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
REDIS_HOST: '10.8.0.9:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
ports:
- 10.8.0.9:1490:1490

@ -0,0 +1,77 @@
services:
core:
hostname: squiz-core
container_name: squiz-core
image: $CI_REGISTRY_IMAGE/core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
HUB_ADMIN_URL: 'http://10.6.0.11:59303'
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PORT: 1488
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
AUTH_URL: 'http://10.6.0.11:59300/user'
ports:
- 1488:1488
storer:
hostname: squiz-storer
container_name: squiz-storer
image: $CI_REGISTRY_IMAGE/storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1489
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
ports:
- 1489:1489
worker:
hostname: squiz-worker
container_name: squiz-worker
image: $CI_REGISTRY_IMAGE/worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
KAFKA_BROKER: '10.6.0.11:9092'
KAFKA_TOPIC: 'tariffs'
QUIZ_ID: quizCnt
AMOUNT: 10
UNLIM_ID: quizUnlimTime
REDIS_HOST: '10.6.0.23:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
SMTP_HOST: 'connect.mailclient.bz'
SMTP_PORT: '587'
SMTP_SENDER: 'noreply@mailing.pena.digital'
SMTP_IDENTITY: ''
SMTP_USERNAME: 'kotilion.95@gmail.com'
SMTP_PASSWORD: 'vWwbCSg4bf0p'
SMTP_API_KEY: 'P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev'
CUSTOMER_SERVICE_ADDRESS: 'http://10.6.0.11:8065/'
answerer:
hostname: squiz-answerer
container_name: squiz-answerer
image: $CI_REGISTRY_IMAGE/answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1490
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
REDIS_HOST: '10.6.0.23:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
ports:
- 1490:1490

@ -0,0 +1,77 @@
services:
core:
hostname: squiz-core
container_name: squiz-core
image: $CI_REGISTRY_IMAGE/staging-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
HUB_ADMIN_URL: 'http://10.6.0.11:59303'
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PORT: 1488
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
AUTH_URL: 'http://10.6.0.11:59300/user'
ports:
- 1488:1488
storer:
hostname: squiz-storer
container_name: squiz-storer
image: $CI_REGISTRY_IMAGE/staging-storer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1489
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
ports:
- 1489:1489
worker:
hostname: squiz-worker
container_name: squiz-worker
image: $CI_REGISTRY_IMAGE/staging-worker:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
KAFKA_BROKER: '10.6.0.11:9092'
KAFKA_TOPIC: 'tariffs'
QUIZ_ID: quizCnt
AMOUNT: 10
UNLIM_ID: quizUnlimTime
REDIS_HOST: '10.6.0.23:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
SMTP_HOST: 'connect.mailclient.bz'
SMTP_PORT: '587'
SMTP_SENDER: 'noreply@mailing.pena.digital'
SMTP_IDENTITY: ''
SMTP_USERNAME: 'kotilion.95@gmail.com'
SMTP_PASSWORD: 'vWwbCSg4bf0p'
SMTP_API_KEY: 'P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev'
CUSTOMER_SERVICE_ADDRESS: 'http://10.6.0.11:8065/'
answerer:
hostname: squiz-answerer
container_name: squiz-answerer
image: $CI_REGISTRY_IMAGE/staging-answerer:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
tty: true
environment:
IS_PROD_LOG: 'false'
IS_PROD: 'false'
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
PORT: 1490
MINIO_EP: 'storage.yandexcloud.net'
MINIO_AK: 'YCAJEOcqqTHpiwL4qFwLfHPNA'
MINIO_SK: 'YCNIAIat0XqdDzycWsYKX3OU7mPor6S0WmMoG4Ry'
PG_CRED: 'host=10.6.0.23 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
REDIS_HOST: '10.6.0.23:6379'
REDIS_PASSWORD: 'Redalert2'
REDIS_DB: 2
ports:
- 1490:1490

@ -0,0 +1,102 @@
version: '3'
services:
test-postgres:
image: postgres
environment:
POSTGRES_PASSWORD: Redalert2
POSTGRES_USER: squiz
POSTGRES_DB: squiz
volumes:
- test-postgres:/var/lib/postgresql/data
ports:
- 35432:5432
networks:
- penatest
healthcheck:
test: pg_isready -U squiz
interval: 2s
timeout: 2s
retries: 10
# need update!
# test-pena-auth-service:
# image: penahub.gitlab.yandexcloud.net:5050/pena-services/pena-auth-service:staging.872
# container_name: test-pena-auth-service
# init: true
# env_file: auth.env.test
# healthcheck:
# test: wget -T1 --spider http://localhost:8000/user
# interval: 2s
# timeout: 2s
# retries: 5
# environment:
# - DB_HOST=test-pena-auth-db
# - DB_PORT=27017
# - ENVIRONMENT=staging
# - HTTP_HOST=0.0.0.0
# - HTTP_PORT=8000
# - DB_USERNAME=test
# - DB_PASSWORD=test
# - DB_NAME=admin
# - DB_AUTH=admin
# # ports:
# # - 8000:8000
# depends_on:
# - test-pena-auth-db
# # - pena-auth-migration
# networks:
# - penatest
#
# test-pena-auth-db:
# container_name: test-pena-auth-db
# init: true
# image: "mongo:6.0.3"
# command: mongod --quiet --logpath /dev/null
# volumes:
# - test-mongodb:/data/db
# - test-mongoconfdb:/data/configdb
# environment:
# MONGO_INITDB_ROOT_USERNAME: test
# MONGO_INITDB_ROOT_PASSWORD: test
# # ports:
# # - 27017:27017
# networks:
# - penatest
test-minio:
container_name: test-minio
init: true
image: quay.io/minio/minio
volumes:
- test-minio:/data
command: [ "minio", "--quiet", "server", "/data" ]
networks:
- penatest
test-squiz:
container_name: test-squiz
init: true
build:
context: ../..
dockerfile: TestsDockerfile
depends_on:
test-postgres:
condition: service_healthy
# test-pena-auth-service:
# condition: service_healthy
# volumes:
# - ./../..:/app:ro
# command: [ "go", "test", "./tests", "-run", "TestFoo" ]
command: [ "go", "test", "-parallel", "1", "./tests" ]
networks:
- penatest
networks:
penatest:
volumes:
test-minio:
test-postgres:
test-mongodb:
test-mongoconfdb:

@ -0,0 +1,23 @@
version: '3'
services:
test-postgres:
image: postgres
environment:
POSTGRES_PASSWORD: Redalert2
POSTGRES_USER: squiz
POSTGRES_DB: squiz
ports:
- 35432:5432
networks:
- penatest
healthcheck:
test: pg_isready -U squiz
interval: 2s
timeout: 2s
retries: 10
networks:
penatest:
# просто чтоб тестануть мигрировала ли бд
# в app/app.go pgdal, err := dal.New(ctx, "host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable")

30
go.mod Normal file

@ -0,0 +1,30 @@
module penahub.gitlab.yandexcloud.net/backend/quiz/core.git
go 1.21.4
require (
github.com/gofiber/fiber/v2 v2.52.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/lib/pq v1.10.9
github.com/skeris/appInit v1.0.2
github.com/tealeg/xlsx v1.0.5
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf
go.uber.org/zap v1.26.0
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.0 // 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.15 // indirect
github.com/rivo/uniseg v0.2.0 // 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
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.15.0 // indirect
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219174347-aa1b1a89378e // indirect
)

100
go.sum Normal file

@ -0,0 +1,100 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pkg/errors v0.8.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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/skeris/appInit v1.0.2 h1:Hr4KbXYd6kolTVq4cXGqDpgnpmaauiOiKizA1+Ep4KQ=
github.com/skeris/appInit v1.0.2/go.mod h1:4ElEeXWVGzU3dlYq/eMWJ/U5hd+LKisc1z3+ySh1XmY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf h1:TJJm6KcBssmbWzplF5lzixXl1RBAi/ViPs1GaSOkhwo=
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf/go.mod h1:1FsorU3vnXO9xS9SrhUp8fRb/6H/Zfll0rPt1i4GWaA=
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=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d h1:gbaDt35HMDqOK84WYmDIlXMI7rstUcRqNttaT6Kx1do=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219173525-b14fbf58477f h1:4i2MBe+UZeboeWTOW37x0cAMml+ZIzXO0DIQPhm5rLo=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219173525-b14fbf58477f/go.mod h1:rcY5DQK14XW+/kYNOujQXPf79oZE5eI74sJntAKY7ek=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219174347-aa1b1a89378e h1:/LG1jEu8gPL6K4vLBuHpm6/0kIua+J+KnbJL1yd8CC8=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240219174347-aa1b1a89378e/go.mod h1:rcY5DQK14XW+/kYNOujQXPf79oZE5eI74sJntAKY7ek=

10
main.go Normal file

@ -0,0 +1,10 @@
package main
import (
"github.com/skeris/appInit"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/app"
)
func main() {
appInit.Initialize(app.New, app.Options{})
}

80
middleware/middleware.go Normal file

@ -0,0 +1,80 @@
package middleware
import (
"github.com/gofiber/fiber/v2"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
AccountId = "id"
)
func JWTAuth() fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
c.Status(fiber.StatusUnauthorized).SendString("no JWT found")
return nil
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.Status(fiber.StatusUnauthorized).SendString("invalid JWT Header: missing Bearer")
return nil
}
publicKey := os.Getenv("PUBLIC_ACCESS_SECRET_KEY")
if publicKey == "" {
// TODO log
c.Status(fiber.StatusInternalServerError).SendString("public key not found")
return nil
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwt.ParseRSAPublicKeyFromPEM([]byte(publicKey))
})
if err != nil {
c.Status(fiber.StatusUnauthorized).SendString("invalid JWT")
return nil
}
if token.Valid {
expirationTime, err := token.Claims.GetExpirationTime()
if err != nil {
c.Status(fiber.StatusUnauthorized).SendString("no expiration time in JWT")
return nil
}
if time.Now().Unix() >= expirationTime.Unix() {
c.Status(fiber.StatusUnauthorized).SendString("expired JWT")
return nil
}
} else {
c.Status(fiber.StatusUnauthorized).SendString("invalid JWT")
return nil
}
m, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.Status(fiber.StatusInternalServerError).SendString("broken token claims")
return nil
}
id, ok := m["id"].(string)
if !ok || id == "" {
c.Status(fiber.StatusUnauthorized).SendString("missing id claim in JWT")
return nil
}
c.Context().SetUserValue(AccountId, id)
return c.Next()
}
}
func GetAccountId(c *fiber.Ctx) (string, bool) {
id, ok := c.Context().UserValue(AccountId).(string)
return id, ok
}

0
openapi.yaml Normal file

99
pkg/excel_export.go Normal file

@ -0,0 +1,99 @@
package pkg
import (
"github.com/tealeg/xlsx"
"io"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"sort"
)
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer) error {
file := xlsx.NewFile()
sheet, err := file.AddSheet("Results")
if err != nil {
return err
}
headers := []string{"Данные респондента"}
mapQueRes := make(map[uint64]string)
for _, q := range questions {
if !q.Deleted {
if q.Type == model.TypeResult {
mapQueRes[q.Id] = q.Title + "\n" + q.Description
} else {
headers = append(headers, q.Title)
}
}
}
headers = append(headers, "Результат")
// добавляем заголовки в первую строку
row := sheet.AddRow()
for _, header := range headers {
cell := row.AddCell()
cell.Value = header
}
// мапа для хранения обычных ответов респондентов
standart := make(map[string][]model.Answer)
// мапа для хранения данных респондентов
results := make(map[string]model.Answer)
// заполняем мапу ответами и данными респондентов
for _, answer := range answers {
if answer.Result {
// если это результат то данные респондента берутся из контента ответа по сессии
results[answer.Session] = answer
} else {
// если это обычный ответ то добавляем его в соответствующий список ответов респондента
standart[answer.Session] = append(standart[answer.Session], answer)
}
}
// записываем данные в файл
for session, _ := range results {
response := standart[session]
row := sheet.AddRow()
row.AddCell().Value = results[session].Content // данные респондента
for _, q := range questions {
if !q.Deleted && q.Type != model.TypeResult {
sort.Slice(response, func(i, j int) bool {
return response[i].QuestionId < response[j].QuestionId
})
index := binarySearch(response, q.Id)
if index != -1 {
row.AddCell().Value = response[index].Content
} else {
row.AddCell().Value = "-"
}
}
}
row.AddCell().Value = mapQueRes[results[session].QuestionId]
}
// cохраняем данные в буфер
err = file.Write(buffer)
if err != nil {
return err
}
return nil
}
func binarySearch(answers []model.Answer, questionID uint64) int {
left := 0
right := len(answers) - 1
for left <= right {
mid := left + (right-left)/2
if answers[mid].QuestionId == questionID {
return mid
} else if answers[mid].QuestionId < questionID {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}

187
service/account_svc.go Normal file

@ -0,0 +1,187 @@
package service
import (
"database/sql"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/middleware"
"time"
)
type CreateAccountReq struct {
UserID string `json:"userId"`
}
type CreateAccountResp struct {
CreatedAccount model.Account `json:"created_account"`
}
type DeleteAccountResp struct {
DeletedAccountID string `json:"account_Id"`
}
type GetPrivilegeByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDReq struct {
UserID string `json:"userId"`
}
type DeleteAccountByUserIDResp struct {
DeletedAccountUserID string `json:"userId"`
}
type GetAccountsReq struct {
Limit uint64 `json:"limit"`
Page uint64 `json:"page"`
}
type GetAccountsResp struct {
Count uint64 `json:"count"`
Items []model.Account `json:"items"`
}
// getCurrentAccount обработчик для получения текущего аккаунта
func (s *Service) getCurrentAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
//TODO: fix this later
if account.ID == "" {
return ctx.Status(fiber.StatusNotFound).SendString("no account")
}
return ctx.Status(fiber.StatusOK).JSON(account)
}
// createAccount обработчик для создания нового аккаунта
func (s *Service) createAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil && err != sql.ErrNoRows {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID != "" {
return ctx.Status(fiber.StatusConflict).SendString("user with this ID already exists")
}
newAccount := model.Account{
UserID: accountID,
CreatedAt: time.Now(),
Deleted: false,
Privileges: map[string]model.ShortPrivilege{
"quizUnlimTime": {
PrivilegeID: "quizUnlimTime",
PrivilegeName: "Безлимит Опросов",
Amount: 14,
CreatedAt: time.Now(),
},
},
}
if err := s.dal.AccountRepo.CreateAccount(ctx.Context(), &newAccount); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(CreateAccountResp{
CreatedAccount: newAccount,
})
}
// deleteAccount обработчик для удаления текущего аккаунта
func (s *Service) deleteAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), account.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountResp{
DeletedAccountID: accountID,
})
}
// getPrivilegeByUserID обработчик для получения привилегий аккаунта по ID пользователя
func (s *Service) getPrivilegeByUserID(ctx *fiber.Ctx) error {
var req GetPrivilegeByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
privilege, err := s.dal.AccountRepo.GetPrivilegesByAccountID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(privilege)
}
// deleteAccountByUserID обработчик для удаления аккаунта по ID пользователя
func (s *Service) deleteAccountByUserID(ctx *fiber.Ctx) error {
var req DeleteAccountByUserIDReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), req.UserID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if existingAccount.ID == "" {
return ctx.Status(fiber.StatusInternalServerError).SendString("user with this ID not found")
}
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), existingAccount.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeleteAccountByUserIDResp{
DeletedAccountUserID: req.UserID,
})
}
// getAccounts обработчик для получения списка аккаунтов с пагинацией
func (s *Service) getAccounts(ctx *fiber.Ctx) error {
var req GetAccountsReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
_, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
accounts, totalCount, err := s.dal.AccountRepo.GetAccounts(ctx.Context(), req.Limit, req.Page)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
response := GetAccountsResp{
Count: totalCount,
Items: accounts,
}
return ctx.Status(fiber.StatusOK).JSON(response)
}

294
service/question_svc.go Normal file

@ -0,0 +1,294 @@
package service
import (
"github.com/gofiber/fiber/v2"
"github.com/lib/pq"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"unicode/utf8"
)
// QuestionCreateReq request structure for creating Question
type QuestionCreateReq struct {
QuizId uint64 `json:"quiz_id"` // relation to quiz
Title string `json:"title"` // title of question
Description string `json:"description"` // additional content in question such as pics, html markup or plain text
Type string `json:"type"` // button/select/file/checkbox/text
Required bool `json:"required"` // set true if question must be answered for valid quiz passing
Page int `json:"page"` // set page of question
Content string `json:"content"` // json serialized config of question
}
// CreateQuestion service handler for creating question for quiz
func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
var req QuestionCreateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if utf8.RuneCountInString(req.Title) >= 512 {
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("title field should have less then 512 chars")
}
if req.Type != model.TypeText &&
req.Type != model.TypeVariant &&
req.Type != model.TypeImages &&
req.Type != model.TypeSelect &&
req.Type != model.TypeVarImages &&
req.Type != model.TypeEmoji &&
req.Type != model.TypeDate &&
req.Type != model.TypeNumber &&
req.Type != model.TypePage &&
req.Type != model.TypeRating &&
req.Type != model.TypeResult &&
req.Type != model.TypeFile {
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none")
}
result := model.Question{
QuizId: req.QuizId,
Title: req.Title,
Description: req.Description,
Type: req.Type,
Required: req.Required,
Deleted: false,
Page: req.Page,
Content: req.Content,
}
if err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result); err != nil {
if e, ok := err.(pq.Error); ok {
if e.Constraint == "quiz_relation" {
return ctx.Status(fiber.StatusFailedDependency).SendString(e.Error())
}
}
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
// GetQuestionListReq request structure for get question page
type GetQuestionListReq struct {
Limit uint64 `json:"limit"` // page size
Page uint64 `json:"page"` // page number
From int64 `json:"from"` // start of time period
To int64 `json:"to"` // end of time period
QuizId uint64 `json:"quiz_id"` // relation to quiz
Search string `json:"search"` // search string to search in files
Type string `json:"type"` // type of questions. check types in model
Deleted bool `json:"deleted"` // true to get only deleted questions
Required bool `json:"required"`
}
// GetQuestionListResp response to get page questions with count of all filtered items
type GetQuestionListResp struct {
Count uint64 `json:"count"`
Items []model.Question `json:"items"`
}
// GetQuestionList handler for paginated list question
func (s *Service) GetQuestionList(ctx *fiber.Ctx) error {
var req GetQuestionListReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Type != "" &&
req.Type != model.TypeText &&
req.Type != model.TypeVariant &&
req.Type != model.TypeImages &&
req.Type != model.TypeSelect &&
req.Type != model.TypeVarImages &&
req.Type != model.TypeEmoji &&
req.Type != model.TypeDate &&
req.Type != model.TypeNumber &&
req.Type != model.TypePage &&
req.Type != model.TypeRating &&
req.Type != model.TypeResult &&
req.Type != model.TypeFile {
return ctx.Status(fiber.StatusNotAcceptable).SendString("inappropriate type, allowed only '', " +
"'test','none','file', 'button','select','checkbox'")
}
res, cnt, err := s.dal.QuestionRepo.GetQuestionList(ctx.Context(),
req.Limit,
req.Page*req.Limit,
uint64(req.From),
uint64(req.To),
req.QuizId,
req.Deleted,
req.Required,
req.Search,
req.Type,
)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(GetQuestionListResp{
Items: res,
Count: cnt,
})
}
// UpdateQuestionReq struct for request to update question
type UpdateQuestionReq struct {
Id uint64 `json:"id"`
Title string `json:"title"`
Description string `json:"desc"`
Type string `json:"type"`
Required bool `json:"required"`
Content string `json:"content"`
Page int `json:"page"`
}
// UpdateResp id you change question that you need only new question id
type UpdateResp struct {
Updated uint64 `json:"updated"`
}
// UpdateQuestion handler for update question
func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
var req UpdateQuestionReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("need id of question for update")
}
if utf8.RuneCountInString(req.Title) >= 512 {
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("title field should have less then 512 chars")
}
if req.Type != model.TypeText &&
req.Type != model.TypeVariant &&
req.Type != model.TypeImages &&
req.Type != model.TypeSelect &&
req.Type != model.TypeVarImages &&
req.Type != model.TypeEmoji &&
req.Type != model.TypeDate &&
req.Type != model.TypeNumber &&
req.Type != model.TypePage &&
req.Type != model.TypeRating &&
req.Type != model.TypeFile &&
req.Type != model.TypeResult &&
req.Type != "" {
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none or empty string")
}
question, err := s.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
question.ParentIds = append(question.ParentIds, int32(question.Id))
question.Id = req.Id
question.Version += 1
if req.Title != "" {
question.Title = req.Title
}
if req.Description != "" {
question.Description = req.Description
}
if req.Page != 0 {
question.Page = req.Page
}
if req.Type != "" {
question.Type = req.Type
}
if req.Required != question.Required {
question.Required = req.Required
}
if req.Content != question.Content {
question.Content = req.Content
}
if err := s.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(UpdateResp{
Updated: question.Id,
})
}
// CopyQuestionReq request struct for copy or duplicate question
type CopyQuestionReq struct {
Id uint64 `json:"id"`
QuizId uint64 `json:"quiz_id"`
}
// CopyQuestion handler for copy question
func (s *Service) CopyQuestion(ctx *fiber.Ctx) error {
var req CopyQuestionReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
question, err := s.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(UpdateResp{
Updated: question.Id,
})
}
// GetQuestionHistoryReq struct of get history request
type GetQuestionHistoryReq struct {
Id uint64 `json:"id"`
Limit uint64 `json:"l"`
Page uint64 `json:"p"`
}
// GetQuestionHistory handler for history of quiz
func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
var req GetQuizHistoryReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
history, err := s.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(history)
}
// DeleteQuestion handler for fake delete question
func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
var req DeactivateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting question is required")
}
deleted, err := s.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeactivateResp{
Deactivated: deleted.Id,
})
}

432
service/quiz_svc.go Normal file

@ -0,0 +1,432 @@
package service
import (
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/quiz"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/middleware"
"time"
"unicode/utf8"
)
type CreateQuizReq struct {
Fingerprinting bool `json:"fingerprinting"` // true if you need to store device id
Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting
NotePrevented bool `json:"note_prevented"` // note answers even if the quiz was aborted
MailNotifications bool `json:"mail_notifications"` // set true if you want get an email with every quiz passing
UniqueAnswers bool `json:"unique_answers"` // set true if we you mention only last quiz passing
Name string `json:"name"` // max 280 chars
Description string `json:"description"`
Config string `json:"config"` // serialize json with config for page rules. fill it up only if implement one form scenario
Status string `json:"status"` // status of quiz as enum. see Status const. fill it up only if implement one form scenario
Limit uint64 `json:"limit"` // max count of quiz passing
DueTo uint64 `json:"due_to"` // time when quiz is end
QuestionCnt uint64 `json:"question_cnt"` // for creating at one request
TimeOfPassing uint64 `json:"time_of_passing"` // amount of seconds for give all appropriate answers for quiz
Pausable bool `json:"pausable"` // true allows to pause the quiz taking
Super bool `json:"super"` // set true if you want to create group
GroupId uint64 `json:"group_id"` // if you create quiz in group provide there the id of super quiz
}
// CreateQuiz handler for quiz creating request
func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
var req CreateQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
// check that we can store name
if utf8.RuneCountInString(req.Name) >= 280 {
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("name field should have less then 280 chars")
}
// status should be empty or equal one of status enum strings
// I mean not draft, template, stop, start statuses
if req.Status != "" &&
req.Status != model.StatusDraft &&
req.Status != model.StatusTemplate &&
req.Status != model.StatusStop &&
req.Status != model.StatusStart {
return ctx.Status(fiber.StatusNotAcceptable).SendString("status on creating must be only draft,template,stop,start")
}
// DueTo should be bigger then now
if req.DueTo != 0 && req.DueTo <= uint64(time.Now().Unix()) {
return ctx.Status(fiber.StatusNotAcceptable).SendString("due to time must be lesser then now")
}
// you can pause quiz only if it has deadline for passing
if req.Pausable && req.TimeOfPassing == 0 {
return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing")
}
record := model.Quiz{
AccountId: accountId,
Fingerprinting: req.Fingerprinting,
Repeatable: req.Repeatable,
NotePrevented: req.NotePrevented,
MailNotifications: req.MailNotifications,
UniqueAnswers: req.UniqueAnswers,
Name: req.Name,
Description: req.Description,
Config: req.Config,
Status: req.Status,
Limit: req.Limit,
DueTo: req.DueTo,
TimeOfPassing: req.TimeOfPassing,
Pausable: req.Pausable,
QuestionsCount: req.QuestionCnt,
ParentIds: []int32{},
Super: req.Super,
GroupId: req.GroupId,
}
if err := s.dal.QuizRepo.CreateQuiz(ctx.Context(), &record); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusCreated).JSON(record)
}
// GetQuizListReq request struct for paginated quiz table
type GetQuizListReq struct {
Limit uint64 `json:"limit"`
Page uint64 `json:"page"`
From int64 `json:"from"`
To int64 `json:"to"`
Search string `json:"search"`
Status string `json:"status"`
Deleted bool `json:"deleted"`
Archived bool `json:"archived"`
Super bool `json:"super"`
GroupId uint64 `json:"group_id"`
}
type GetQuizListResp struct {
Count uint64 `json:"count"`
Items []model.Quiz `json:"items"`
}
// GetQuizList handler for paginated list quiz
func (s *Service) GetQuizList(ctx *fiber.Ctx) error {
var req GetQuizListReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Status != "" &&
req.Status != model.StatusStop &&
req.Status != model.StatusStart &&
req.Status != model.StatusDraft &&
req.Status != model.StatusTemplate &&
req.Status != model.StatusTimeout &&
req.Status != model.StatusOffLimit {
return ctx.Status(fiber.StatusNotAcceptable).SendString("inappropriate status, allowed only '', " +
"'stop','start','draft', 'template','timeout','offlimit'")
}
res, cnt, err := s.dal.QuizRepo.GetQuizList(ctx.Context(),
quiz.GetQuizListDeps{
Limit: req.Limit,
Offset: req.Limit * req.Page,
From: uint64(req.From),
To: uint64(req.To),
Group: req.GroupId,
Deleted: req.Deleted,
Archived: req.Archived,
Super: req.Super,
Search: req.Search,
Status: req.Status,
AccountId: accountId,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(GetQuizListResp{
Items: res,
Count: cnt,
})
}
type UpdateQuizReq struct {
Id uint64 `json:"id"`
Fingerprinting bool `json:"fp"`
Repeatable bool `json:"rep"`
NotePrevented bool `json:"note_prevented"`
MailNotifications bool `json:"mailing"`
UniqueAnswers bool `json:"uniq"`
Name string `json:"name"`
Description string `json:"desc"`
Config string `json:"conf"`
Status string `json:"status"`
Limit uint64 `json:"limit"`
DueTo uint64 `json:"due_to"`
TimeOfPassing uint64 `json:"time_of_passing"`
Pausable bool `json:"pausable"`
QuestionCnt uint64 `json:"question_cnt"` // for creating at one request
Super bool `json:"super"`
GroupId uint64 `json:"group_id"`
}
func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
var req UpdateQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("need id of question for update")
}
if utf8.RuneCountInString(req.Name) >= 280 {
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("name field should have less then 280 chars")
}
// status should be empty or equal one of status enum strings
// I mean not draft, template, stop, start statuses
if req.Status != "" &&
req.Status != model.StatusDraft &&
req.Status != model.StatusTemplate &&
req.Status != model.StatusStop &&
req.Status != model.StatusStart {
return ctx.Status(fiber.StatusNotAcceptable).SendString("status on creating must be only draft,template,stop,start")
}
// DueTo should be bigger then now
if req.DueTo != 0 && req.DueTo <= uint64(time.Now().Unix()) {
return ctx.Status(fiber.StatusNotAcceptable).SendString("due to time must be lesser then now")
}
// you can pause quiz only if it has deadline for passing
if req.Pausable && req.TimeOfPassing == 0 {
return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing")
}
quiz, err := s.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id))
quiz.Id = req.Id
quiz.Version += 1
if req.Fingerprinting != quiz.Fingerprinting {
quiz.Fingerprinting = req.Fingerprinting
}
if req.Repeatable != quiz.Repeatable {
quiz.Repeatable = req.Repeatable
}
if req.MailNotifications != quiz.MailNotifications {
quiz.MailNotifications = req.MailNotifications
}
if req.NotePrevented != quiz.NotePrevented {
quiz.NotePrevented = req.NotePrevented
}
if req.UniqueAnswers != quiz.UniqueAnswers {
quiz.UniqueAnswers = req.UniqueAnswers
}
if req.Pausable != quiz.Pausable {
quiz.Pausable = req.Pausable
}
if req.Name != "" && req.Name != quiz.Name {
quiz.Name = req.Name
}
if req.Description != "" && req.Description != quiz.Description {
quiz.Description = req.Description
}
if req.Status != "" && req.Status != quiz.Status {
quiz.Status = req.Status
}
if req.TimeOfPassing != quiz.TimeOfPassing {
quiz.TimeOfPassing = req.TimeOfPassing
}
if req.DueTo != quiz.DueTo {
quiz.DueTo = req.DueTo
}
if req.Limit != quiz.Limit {
quiz.Limit = req.Limit
}
if req.Config != "" && req.Config != quiz.Config {
quiz.Config = req.Config
}
if req.Super != quiz.Super {
quiz.Super = req.Super
}
if req.GroupId != quiz.GroupId {
quiz.GroupId = req.GroupId
}
quiz.QuestionsCount = req.QuestionCnt
quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id))
if err := s.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(UpdateResp{
Updated: quiz.Id,
})
}
// CopyQuizReq request struct for copy quiz
type CopyQuizReq struct {
Id uint64 `json:"id"`
}
// CopyQuiz request handler for copy quiz
func (s *Service) CopyQuiz(ctx *fiber.Ctx) error {
var req CopyQuizReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
quiz, err := s.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(UpdateResp{
Updated: quiz.Id,
})
}
// GetQuizHistoryReq struct of get history request
type GetQuizHistoryReq struct {
Id uint64 `json:"id"`
Limit uint64 `json:"l"`
Page uint64 `json:"p"`
}
// GetQuizHistory handler for history of quiz
func (s *Service) GetQuizHistory(ctx *fiber.Ctx) error {
var req GetQuizHistoryReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
}
history, err := s.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{
Id: req.Id,
Limit: req.Limit,
Offset: req.Page * req.Limit,
AccountId: accountId,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusCreated).JSON(history)
}
// DeactivateReq request structure for archiving and deleting
type DeactivateReq struct {
Id uint64 `json:"id"`
}
type DeactivateResp struct {
Deactivated uint64 `json:"deactivated"`
}
// DeleteQuiz handler for fake delete quiz
func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
var req DeactivateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting is required")
}
deleted, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeactivateResp{
Deactivated: deleted.Id,
})
}
// ArchiveQuiz handler for archiving quiz
func (s *Service) ArchiveQuiz(ctx *fiber.Ctx) error {
var req DeactivateReq
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountId, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
if req.Id == 0 {
return ctx.Status(fiber.StatusFailedDependency).SendString("id for archive quiz is required")
}
archived, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.JSON(DeactivateResp{
Deactivated: archived.Id,
})
}

213
service/result_svc.go Normal file

@ -0,0 +1,213 @@
package service
import (
"bytes"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/result"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/core.git/pkg"
"strconv"
"time"
)
type ReqExport struct {
To, From time.Time
New bool
Page uint64
Limit uint64
}
type ReqExportResponse struct {
TotalCount uint64 `json:"total_count"`
Results []model.AnswerExport `json:"results"`
}
func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
payment := true // параметр для определения существования текущих привилегий юзера
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
var req ReqExport
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
quizIDStr := ctx.Params("quizId")
quizID, err := strconv.ParseUint(quizIDStr, 10, 64)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if len(account.Privileges) == 0 {
payment = false
}
results, totalCount, err := s.dal.ResultRepo.GetQuizResults(ctx.Context(), quizID, result.GetQuizResDeps{
To: req.To,
From: req.From,
New: req.New,
Page: req.Page,
Limit: req.Limit,
}, payment)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
resp := &ReqExportResponse{
TotalCount: totalCount,
Results: results,
}
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
}
resultIDStr := ctx.Params("resultId")
resultID, err := strconv.ParseUint(resultIDStr, 10, 64)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid result ID format")
}
isOwner, err := s.dal.ResultRepo.CheckResultOwner(ctx.Context(), resultID, accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if !isOwner {
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner of the result")
}
if err := s.dal.ResultRepo.SoftDeleteResultByID(ctx.Context(), resultID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
ctx.Status(fiber.StatusOK)
return nil
}
type ReqSeen struct {
Answers []int64
}
func (s *Service) SetStatus(ctx *fiber.Ctx) error {
var req ReqSeen
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
}
answers, err := s.dal.ResultRepo.CheckResultsOwner(ctx.Context(), req.Answers, accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if len(answers) != len(req.Answers) {
return ctx.Status(fiber.StatusNotAcceptable).SendString("could not update some answers because you don't have rights")
}
if err := s.dal.ResultRepo.UpdateAnswersStatus(ctx.Context(), accountID, answers); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return ctx.Status(fiber.StatusOK).JSON(nil)
}
func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
quizIDStr := ctx.Params("quizID")
quizID, err := strconv.ParseUint(quizIDStr, 10, 64)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID")
}
req := ReqExport{}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("invalid request body")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if len(account.Privileges) == 0 {
return ctx.Status(fiber.StatusPaymentRequired).SendString("payment required")
}
questions, err := s.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
}
answers, err := s.dal.ResultRepo.GetQuizResultsCSV(ctx.Context(), quizID, result.GetQuizResDeps{
To: req.To,
From: req.From,
New: req.New,
Page: req.Page,
Limit: req.Limit,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get quiz results")
}
buffer := new(bytes.Buffer)
if err := pkg.WriteDataToExcel(buffer, questions, answers); err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
}
ctx.Set(fiber.HeaderContentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
ctx.Set(fiber.HeaderContentDisposition, `attachment; filename="results.xlsx"`)
return ctx.Send(buffer.Bytes())
}
func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
resultID, err := strconv.ParseUint(ctx.Params("resultID"), 10, 64)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID")
}
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if len(account.Privileges) == 0 {
return ctx.Status(fiber.StatusPaymentRequired).SendString("payment required")
}
answers, err := s.dal.ResultRepo.GetResultAnswers(ctx.Context(), resultID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get result answers")
}
return ctx.JSON(answers)
}

50
service/service.go Normal file

@ -0,0 +1,50 @@
package service
import (
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
)
// Service is an entity for http requests handling
type Service struct {
dal *dal.DAL
}
func New(d *dal.DAL) *Service {
return &Service{dal: d}
}
// Register is a function for add handlers of service to external multiplexer
func (s *Service) Register(app *fiber.App) {
// quiz manipulating handlers
app.Post("/quiz/create", s.CreateQuiz)
app.Post("/quiz/getList", s.GetQuizList)
app.Patch("/quiz/edit", s.UpdateQuiz)
app.Post("/quiz/copy", s.CopyQuiz)
app.Post("/quiz/history", s.GetQuizHistory)
app.Delete("/quiz/delete", s.DeleteQuiz)
app.Patch("/quiz/archive", s.ArchiveQuiz)
// question manipulating handlers
app.Post("/question/create", s.CreateQuestion)
app.Post("/question/getList", s.GetQuestionList)
app.Patch("/question/edit", s.UpdateQuestion)
app.Post("/question/copy", s.CopyQuestion)
app.Post("/question/history", s.GetQuestionHistory)
app.Delete("/question/delete", s.DeleteQuestion)
// account handlers
app.Get("/account/get", s.getCurrentAccount)
app.Post("/account/create", s.createAccount)
app.Delete("/account/delete", s.deleteAccount)
app.Get("/accounts", s.getAccounts)
app.Get("/privilege/:userId", s.getPrivilegeByUserID)
app.Delete("/account/:userId", s.deleteAccountByUserID)
// result handlers
app.Post("/results/getResults/:quizId", s.GetResultsByQuizID)
app.Delete("/results/delete/:resultId", s.DelResultByID)
app.Patch("/result/seen", s.SetStatus)
app.Post("/results/:quizID/export", s.ExportResultsToCSV)
app.Get("/result/:resultID", s.GetResultAnswers)
}