Начальный коммит

This commit is contained in:
Александр Кирюхин 2023-11-01 23:21:12 +03:00
commit e3b935d1c6
No known key found for this signature in database
GPG key ID: 7E27ABF5BF09B487
60 changed files with 6150 additions and 0 deletions

7
.env Normal file
View file

@ -0,0 +1,7 @@
PG_HOST=localhost
PG_NAME=nquest
PG_USER=nquest
PG_PASS=nquest
PG_PORT=5432
SECRET=s3cr3t
LISTEN=:8000

8
Makefile Normal file
View file

@ -0,0 +1,8 @@
.PHONY: db
db:
docker run --name nquest-db -e POSTGRES_DB=nquest -e POSTGRES_USER=nquest -e POSTGRES_PASSWORD=nquest -p 5432:5432 -d postgres
.PHONY: gen-api
gen-api:
oapi-codegen -generate server,spec -package api -o api/server.go api/openapi.yaml
oapi-codegen -generate types -package api -o api/types.go api/openapi.yaml

20
api/mapper.go Normal file
View file

@ -0,0 +1,20 @@
package api
import "gitrepo.ru/neonxp/nquest/pkg/models"
var MapRole = map[models.Role]UserTeamRole{
models.Captain: Captain,
models.Member: Member,
}
func MapUserTeam(team *models.TeamMember) *UserTeam {
if team == nil || team.Team == nil {
return nil
}
return &UserTeam{
Id: int(team.Team.ID),
Name: team.Team.Name,
Role: MapRole[team.Role],
}
}

254
api/openapi.yaml Normal file
View file

@ -0,0 +1,254 @@
openapi: "3.1.0"
info:
version: 1.0.0
title: nQuest
servers:
- url: /api
paths:
# User routes
/user:
get:
responses:
200:
$ref: '#/components/responses/userResponse'
403:
$ref: '#/components/responses/errorResponse'
/user/login:
post:
security: []
requestBody:
$ref: '#/components/requestBodies/login'
responses:
200:
$ref: '#/components/responses/userResponse'
400:
$ref: '#/components/responses/errorResponse'
/user/register:
post:
security: []
requestBody:
$ref: '#/components/requestBodies/register'
responses:
200:
$ref: '#/components/responses/userResponse'
400:
$ref: '#/components/responses/errorResponse'
/user/logout:
post:
security: []
responses:
204:
description: "success logout"
400:
$ref: '#/components/responses/errorResponse'
# Team routes
/teams:
get:
responses:
200:
$ref: '#/components/responses/teamsListResponse'
403:
$ref: '#/components/responses/errorResponse'
/teams/{teamID}:
get:
parameters:
- in: path
name: teamID
schema:
type: integer
required: true
responses:
200:
$ref: '#/components/responses/teamResponse'
404:
$ref: '#/components/responses/errorResponse'
/teams/{teamID}/members:
post:
parameters:
- in: path
name: teamID
schema:
type: integer
required: true
responses:
200:
$ref: '#/components/responses/teamResponse'
404:
$ref: '#/components/responses/errorResponse'
delete:
parameters:
- in: path
name: teamID
schema:
type: integer
required: true
requestBody:
content:
'application/json':
schema:
userID:
type: integer
responses:
200:
$ref: '#/components/responses/teamResponse'
404:
$ref: '#/components/responses/errorResponse'
components:
schemas:
userTeam:
type: object
properties:
id:
type: integer
name:
type: string
role:
$ref: "#/components/schemas/userTeamRole"
required: [ id, name, role ]
userTeamRole:
type: string
enum:
- member
- captain
teamListItem:
type: object
properties:
id:
type: integer
name:
type: string
members:
type: integer
currentTeam:
type: boolean
createdAt:
type: string
required: [ id, name, members, currentTeam, createdAt ]
teamMember:
type: object
properties:
user:
$ref: "#/components/schemas/userView"
role:
$ref: "#/components/schemas/userTeamRole"
createdAt:
type: string
required: [ user, role, createdAt ]
teamRequest:
type: object
properties:
user:
$ref: "#/components/schemas/userView"
createdAt:
type: string
required: [ user, role, createdAt ]
userView:
type: object
properties:
id:
type: integer
username:
type: string
required: [ id, username ]
requestBodies:
login:
required: true
content:
'application/json':
schema:
type: object
properties:
email:
type: string
password:
type: string
required: [ email, password ]
register:
required: true
content:
'application/json':
schema:
type: object
properties:
username:
type: string
email:
type: string
password:
type: string
password2:
type: string
required: [ username, email, password, password2 ]
responses:
userResponse:
description: ''
content:
'application/json':
schema:
type: object
properties:
id:
type: integer
username:
type: string
email:
type: string
team:
$ref: "#/components/schemas/userTeam"
required:
- id
- username
- email
errorResponse:
description: ''
content:
'application/json':
schema:
type: object
properties:
code:
type: integer
message:
type: string
required: [ code, message ]
teamsListResponse:
description: ''
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/teamListItem'
teamResponse:
description: ''
content:
'application/json':
schema:
type: object
properties:
id:
type: integer
name:
type: string
members:
type: array
items:
$ref: "#/components/schemas/teamMember"
requests:
type: array
items:
$ref: "#/components/schemas/teamRequest"
createdAt:
type: string
required: [ id, name, members, requests, createdAt ]
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: session
security:
- cookieAuth: []

286
api/server.go Normal file
View file

@ -0,0 +1,286 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT.
package api
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
"github.com/oapi-codegen/runtime"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /teams)
GetTeams(ctx echo.Context) error
// (GET /teams/{teamID})
GetTeamsTeamID(ctx echo.Context, teamID int) error
// (DELETE /teams/{teamID}/members)
DeleteTeamsTeamIDMembers(ctx echo.Context, teamID int) error
// (POST /teams/{teamID}/members)
PostTeamsTeamIDMembers(ctx echo.Context, teamID int) error
// (GET /user)
GetUser(ctx echo.Context) error
// (POST /user/login)
PostUserLogin(ctx echo.Context) error
// (POST /user/logout)
PostUserLogout(ctx echo.Context) error
// (POST /user/register)
PostUserRegister(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// GetTeams converts echo context to params.
func (w *ServerInterfaceWrapper) GetTeams(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetTeams(ctx)
return err
}
// GetTeamsTeamID converts echo context to params.
func (w *ServerInterfaceWrapper) GetTeamsTeamID(ctx echo.Context) error {
var err error
// ------------- Path parameter "teamID" -------------
var teamID int
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
}
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetTeamsTeamID(ctx, teamID)
return err
}
// DeleteTeamsTeamIDMembers converts echo context to params.
func (w *ServerInterfaceWrapper) DeleteTeamsTeamIDMembers(ctx echo.Context) error {
var err error
// ------------- Path parameter "teamID" -------------
var teamID int
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
}
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.DeleteTeamsTeamIDMembers(ctx, teamID)
return err
}
// PostTeamsTeamIDMembers converts echo context to params.
func (w *ServerInterfaceWrapper) PostTeamsTeamIDMembers(ctx echo.Context) error {
var err error
// ------------- Path parameter "teamID" -------------
var teamID int
err = runtime.BindStyledParameterWithLocation("simple", false, "teamID", runtime.ParamLocationPath, ctx.Param("teamID"), &teamID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter teamID: %s", err))
}
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.PostTeamsTeamIDMembers(ctx, teamID)
return err
}
// GetUser converts echo context to params.
func (w *ServerInterfaceWrapper) GetUser(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetUser(ctx)
return err
}
// PostUserLogin converts echo context to params.
func (w *ServerInterfaceWrapper) PostUserLogin(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.PostUserLogin(ctx)
return err
}
// PostUserLogout converts echo context to params.
func (w *ServerInterfaceWrapper) PostUserLogout(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.PostUserLogout(ctx)
return err
}
// PostUserRegister converts echo context to params.
func (w *ServerInterfaceWrapper) PostUserRegister(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.PostUserRegister(ctx)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}
// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.GET(baseURL+"/teams", wrapper.GetTeams)
router.GET(baseURL+"/teams/:teamID", wrapper.GetTeamsTeamID)
router.DELETE(baseURL+"/teams/:teamID/members", wrapper.DeleteTeamsTeamIDMembers)
router.POST(baseURL+"/teams/:teamID/members", wrapper.PostTeamsTeamIDMembers)
router.GET(baseURL+"/user", wrapper.GetUser)
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
router.POST(baseURL+"/user/register", wrapper.PostUserRegister)
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/8xYTW/jNhD9KwXbo2A5HyfdUgQojCZA66Z7MXxgpInDrEVqyVECI9B/X8yIsqVIsiXH",
"691TGH4M33szfKT8LmKTZkaDRieid2HhWw4O/zSJAu5Ym5XS1IiNRtBITZllaxVLVEaHL87wsIufIZXU",
"yqzJwKJfD6lUa2rgJgMRCYdW6ZUoApFJ596MTToGi4CBKAuJiBY+Rm3FMqhWmMcXiFEUzSVoc+COlXII",
"9tzwd4OXnaO5A6tlCoeZb2cGbRHquwwShHtcZrTz3Kw1du57PqFRbJI6FaURVmCJaArOydUAnhxiN7+b",
"TgIutiojTCISFB9BpqcgYEEiJDfYmS2V9JFLH8FyAIWQcuMPC08iEr+Hu1MVlru6kMDe8xqGXsaT1soN",
"/d9TD0F1IsftMy8XtTf6oLuiMvIFVvGpbRnUpBmTE3enHB6VmMEMaYcZQtpBsRMWnaQTlEq/H/SVCYE9",
"RIjAPdC8UebAyWs5xLA8FYGnyKwago48HnFuLWh88DT9+KMxa5B64PlpD45QoFW+dUT7K7jMjj+UI3lb",
"s4aheZ3TXJ/bIWu+KHjrvAuE33cIrcoDRvI6G8Zt0bcA9lVMv0eOzsWeQuJg+wDP/W6g85QWl5VHfGWG",
"Uuna4qasLNlgtscaQYcBBMJBnFuFm/9IjeraNl8V3OT4zCjIF8quSopIOHCOHGNnsZn6G7zHKv1kGJtC",
"0kPof7ncAvEK1pU+czGZTqbExWSgZaZEJK4mF5Mpv13wmWGwm3NrBVyRpA2b8SwRkfgL+CCX11Lt8XI5",
"nfZlfDsvbN9FRSCup1eHVzbfRgUTLsOF7/RndlschPzA85irlSkgm93CS038d0JjNbX5ZAtq99DHAimW",
"xyrSFOP6JGKENTdPYA0IbV1uub8mzf3Wsn+gQtX3zGbUrU+naXbbGbVoPaTPqHwgMuM6au4f4/Dcyv68",
"2qvuqL7j97+/hkZDbLwSP+UVFCncfjz3J42g3vG0VrF271r7Pvfxi9MwnY5nurtXRLRYNnibHAcRp3kt",
"/NeljdSfrC6PY3DuNx/61IjrPxXsxzyvZh6Rr+0uv07Kmh3NV8FiSQfdgX2t7CO3axGJkK7yYll8DwAA",
"//8BW4TRPRIAAA==",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

118
api/types.go Normal file
View file

@ -0,0 +1,118 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT.
package api
const (
CookieAuthScopes = "cookieAuth.Scopes"
)
// Defines values for UserTeamRole.
const (
Captain UserTeamRole = "captain"
Member UserTeamRole = "member"
)
// TeamListItem defines model for teamListItem.
type TeamListItem struct {
CreatedAt string `json:"createdAt"`
CurrentTeam bool `json:"currentTeam"`
Id int `json:"id"`
Members int `json:"members"`
Name string `json:"name"`
}
// TeamMember defines model for teamMember.
type TeamMember struct {
CreatedAt string `json:"createdAt"`
Role UserTeamRole `json:"role"`
User UserView `json:"user"`
}
// TeamRequest defines model for teamRequest.
type TeamRequest struct {
CreatedAt string `json:"createdAt"`
User UserView `json:"user"`
}
// UserTeam defines model for userTeam.
type UserTeam struct {
Id int `json:"id"`
Name string `json:"name"`
Role UserTeamRole `json:"role"`
}
// UserTeamRole defines model for userTeamRole.
type UserTeamRole string
// UserView defines model for userView.
type UserView struct {
Id int `json:"id"`
Username string `json:"username"`
}
// ErrorResponse defines model for errorResponse.
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// TeamResponse defines model for teamResponse.
type TeamResponse struct {
CreatedAt string `json:"createdAt"`
Id int `json:"id"`
Members []TeamMember `json:"members"`
Name string `json:"name"`
Requests []TeamRequest `json:"requests"`
}
// TeamsListResponse defines model for teamsListResponse.
type TeamsListResponse = []TeamListItem
// UserResponse defines model for userResponse.
type UserResponse struct {
Email string `json:"email"`
Id int `json:"id"`
Team *UserTeam `json:"team,omitempty"`
Username string `json:"username"`
}
// Login defines model for login.
type Login struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Register defines model for register.
type Register struct {
Email string `json:"email"`
Password string `json:"password"`
Password2 string `json:"password2"`
Username string `json:"username"`
}
// DeleteTeamsTeamIDMembersJSONBody defines parameters for DeleteTeamsTeamIDMembers.
type DeleteTeamsTeamIDMembersJSONBody = interface{}
// PostUserLoginJSONBody defines parameters for PostUserLogin.
type PostUserLoginJSONBody struct {
Email string `json:"email"`
Password string `json:"password"`
}
// PostUserRegisterJSONBody defines parameters for PostUserRegister.
type PostUserRegisterJSONBody struct {
Email string `json:"email"`
Password string `json:"password"`
Password2 string `json:"password2"`
Username string `json:"username"`
}
// DeleteTeamsTeamIDMembersJSONRequestBody defines body for DeleteTeamsTeamIDMembers for application/json ContentType.
type DeleteTeamsTeamIDMembersJSONRequestBody = DeleteTeamsTeamIDMembersJSONBody
// PostUserLoginJSONRequestBody defines body for PostUserLogin for application/json ContentType.
type PostUserLoginJSONRequestBody PostUserLoginJSONBody
// PostUserRegisterJSONRequestBody defines body for PostUserRegister for application/json ContentType.
type PostUserRegisterJSONRequestBody PostUserRegisterJSONBody

41
config.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"fmt"
"log"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
)
type Config struct {
PgHost string `envconfig:"PG_HOST"`
PgName string `envconfig:"PG_NAME"`
PgUser string `envconfig:"PG_USER"`
PgPass string `envconfig:"PG_PASS"`
PgPort int `envconfig:"PG_PORT"`
Listen string `envconfig:"LISTEN"`
Secret string `envconfig:"SECRET"`
}
func (c *Config) DSN() string {
return fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Europe/Moscow",
c.PgHost,
c.PgUser,
c.PgPass,
c.PgName,
c.PgPort,
)
}
func GetConfig() (*Config, error) {
c := new(Config)
err := godotenv.Load()
if err != nil {
log.Println("not .env")
}
return c, envconfig.Process("", c)
}

17
embeds.go Normal file
View file

@ -0,0 +1,17 @@
package main
import (
"embed"
"github.com/labstack/echo/v4"
)
var (
//go:embed all:frontend/dist
dist embed.FS
//go:embed frontend/dist/index.html
indexHTML embed.FS
distDirFS = echo.MustSubFS(dist, "frontend/dist")
distIndexHtml = echo.MustSubFS(indexHTML, "frontend/dist")
)

1
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

8
frontend/README.md Normal file
View file

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

67
frontend/dist/assets/index-7c896ee4.js vendored Normal file

File diff suppressed because one or more lines are too long

11
frontend/dist/assets/index-dca32c5e.css vendored Normal file

File diff suppressed because one or more lines are too long

14
frontend/dist/index.html vendored Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nQuest</title>
<script type="module" crossorigin src="/assets/index-7c896ee4.js"></script>
<link rel="stylesheet" href="/assets/index-dca32c5e.css">
</head>
<body data-bs-theme="dark">
<div id="root"></div>
</body>
</html>

1
frontend/dist/vite.svg vendored Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
frontend/index.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nQuest</title>
</head>
<body data-bs-theme="dark">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2821
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
frontend/package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "nquest",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"provider-compose": "0.0.5",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.17.0",
"unstated-next": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.4.5"
}
}

1
frontend/public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

60
frontend/src/App.jsx Normal file
View file

@ -0,0 +1,60 @@
import { Navigate, Route, RouterProvider, createBrowserRouter, createRoutesFromElements, useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import Layout from './components/Layout'
import Index from './pages/Index'
import Login from './pages/Login'
import Register from './pages/Register'
import NoMatch from './pages/NoMatch'
import Team from './pages/Team'
import Teams from './pages/Teams'
import { UserProvider } from './store/user'
import { ajax } from './utils/fetch'
import TeamNew from './pages/TeamNew'
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="/"
id="root"
element={<Layout />}
loader={async () => ajax("/api/user")}
>
<Route index element={<Index />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route
path="teams"
element={<Auth><Teams /></Auth>}
loader={() => ajax("/api/teams")}
/>
<Route
path="teams/new"
element={<Auth><TeamNew /></Auth>}
/>
<Route
path="teams/:teamId"
element={<Auth><Team /></Auth>}
loader={({ params }) => ajax(`/api/teams/${params.teamId}`)}
/>
<Route path="*" element={<NoMatch />} />
</Route>
)
);
function App() {
return (
<RouterProvider router={router} />
)
}
function Auth({ children }) {
const baseUser = useRouteLoaderData("root")
const {user} = UserProvider.useContainer();
const location = useLocation();
if (!user && !baseUser) {
return <Navigate to="/login" state={{from: location}} replace />;
}
return children;
}
export default App

Binary file not shown.

12
frontend/src/assets/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,30 @@
/* navbar */
.navbar-dark {
border-bottom: 0.1px solid #333333 !important;
}
@font-face {
font-family: 'TiltNeon';
src: url('TiltNeon-Regular.ttf');
}
.navbar-brand {
font-family: TiltNeon, var(--bs-font-sans-serif);
text-shadow:
0 0 1px rgb(255, 255, 255, 1),
0 0 2px rgb(255, 255, 255, 1),
0 0 3px rgb(255, 255, 255, 1),
0 0 4px rgb(255, 255, 255, 1),
-1px 0 #198754,
0 1px #198754,
1px 0 #198754,
0 -1px #198754;
-webkit-text-stroke: 1px white;
font-size: 26px;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
th.thin {
width: 1px;
white-space: nowrap;
}

View file

@ -0,0 +1,67 @@
import { Link, Outlet, useLoaderData } from "react-router-dom";
import { Button } from "react-bootstrap";
import { UserProvider } from "../store/user";
import { useEffect } from "react";
import { ajax } from "../utils/fetch";
export default () => {
const params = useLoaderData();
useEffect(() => {
setUser(params)
}, [params])
const { user, setUser } = UserProvider.useContainer();
const logout = () => {
ajax("/api/user/logout", {
method: "POST",
}).
then(() => setUser(null))
}
return (<>
<nav className="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div className="container">
<a className="navbar-brand" href="https://nquest.ru/">nQuest</a>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" to="/">Игры</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/teams">Команды</Link>
</li>
</ul>
<div className="d-flex">
{user ? (
<>
<span className="navbar-text me-2">
{user.username}&nbsp;
{user.team ? (
<>(<Link to={`teams/${user.team.id}`}>{user.team.name}</Link>)</>
) : (
<>(без команды)</>
)}
</span>
<Button type="button" variant="outline-success" onClick={logout}>Выход</Button>
</>
) : (
<div className="btn-group">
<Link className="btn btn-success" to="login">Вход</Link>
<Link className="btn btn-outline-success" to="register">Регистрация</Link>
</div>
)}
</div>
</div>
</div>
</nav>
<div className="container my-5">
<Outlet />
</div>
</>);
}

17
frontend/src/main.jsx Normal file
View file

@ -0,0 +1,17 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Compose } from 'provider-compose';
import "./assets/bootstrap.min.css"
import "./assets/styles.css"
import App from './App.jsx'
import { store } from './store/provider.js';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Compose providers={store}>
<App />
</Compose>
</React.StrictMode>,
);

View file

@ -0,0 +1,3 @@
export default () => (<>
Index
</>);

View file

@ -0,0 +1,65 @@
import { useEffect, useState } from "react";
import { Form, Button, Row, Col } from "react-bootstrap";
import { UserProvider } from "../store/user";
import { useLocation, useNavigate } from "react-router-dom";
import { ajax } from "../utils/fetch";
export default () => {
const {user, setUser} = UserProvider.useContainer();
const {state} = useLocation()
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const navigate = useNavigate();
useEffect(() => {
if (user) {
navigate(state && state.from ? state.from : "/");
}
}, [user])
const onLogin = (e) => {
e.preventDefault();
ajax("/api/user/login", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
}).
then(setUser).
catch(({ message }) => setError(message));
}
return (<>
<h1>Вход</h1>
<Form onSubmit={onLogin}>
<div className="col-lg-8 px-0">
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
<Form.Group as={Row} className="mb-3" controlId="email">
<Form.Label column sm="4">Email</Form.Label>
<Col sm="8">
<Form.Control
type="email"
placeholder="name@mail.ru"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" controlId="password">
<Form.Label column sm="4">Пароль</Form.Label>
<Col sm="8">
<Form.Control
type="password"
placeholder="********"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</Col>
</Form.Group>
<Button type="submit" size="lg">Вход</Button>
</div>
</Form>
</>)
};

View file

@ -0,0 +1,3 @@
export default () => (<>
<h1>404!</h1>
</>);

View file

@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
import { Form, Button, Row, Col } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
import { UserProvider } from "../store/user";
import { ajax } from "../utils/fetch";
export default () => {
const { user, setUser } = UserProvider.useContainer();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [password2, setPassword2] = useState("");
const [error, setError] = useState(null);
const navigate = useNavigate();
useEffect(() => {
user ? navigate("/") : null;
}, [user])
const onRegister = (e) => {
e.preventDefault();
ajax("/api/user/register", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, email, password, password2 })
}).
then(setUser).
catch(({ message }) => setError(message));
}
return (<>
<h1>Регистрация</h1>
<Form onSubmit={onRegister}>
<div className="col-lg-8 px-0">
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
<Form.Group as={Row} className="mb-3" controlId="username">
<Form.Label column sm="4">Имя пользователя</Form.Label>
<Col sm="8">
<Form.Control
type="text"
placeholder="Имя пользователя"
value={username}
onChange={e => setUsername(e.target.value)}
/>
<Form.Text>Имя пользователя для отображения другим игрокам</Form.Text>
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" controlId="email">
<Form.Label column sm="4">Email</Form.Label>
<Col sm="8">
<Form.Control
type="email"
placeholder="name@mail.ru"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<Form.Text>E-mail не виден другим игрокам</Form.Text>
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" controlId="password">
<Form.Label column sm="4">Пароль</Form.Label>
<Col sm="8">
<Form.Control
type="password"
placeholder="********"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<Form.Text>Пароль должен быть от 8 до 16 символов</Form.Text>
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" controlId="password2">
<Form.Label column sm="4">Повторите пароль</Form.Label>
<Col sm="8">
<Form.Control
type="password"
placeholder="********"
value={password2}
onChange={e => setPassword2(e.target.value)}
/>
<Form.Text>Введите пароль заново, чтобы избежать опечаток</Form.Text>
</Col>
</Form.Group>
<Button type="submit" size="lg">Регистрация</Button>
</div>
</Form>
</>);
}

View file

@ -0,0 +1,50 @@
import { useLoaderData, useRouteLoaderData } from "react-router-dom";
import { Button, Table } from "react-bootstrap";
import { UserProvider } from "../store/user";
const userRoles = { captain: "Командир", member: "Участник" };
export default () => {
const team = useLoaderData();
if (!team) {
return null;
}
const { user } = UserProvider.useContainer();
if (!user) {
return null;
}
const member = team.members.find(tm => tm.user.id === user.id);
const inOtherTeam = user.team && user.team.td != team.id;
return (<>
<h1>{team.name}</h1>
<p>Создана: {team.createdAt}</p>
{!member && !inOtherTeam ? (<Button>Отправить заявку в команду</Button>) : null}
{member && member.role != "captain" ? (<Button>Выйти из команды</Button>) : null}
<h2>Участники</h2>
<Table>
<thead>
<tr>
<th>Имя пользователя</th>
<th>Роль</th>
<th>Присоединился</th>
</tr>
</thead>
<tbody>
{team.members.map(tm => (
<tr key={tm.user.id}>
<td>{tm.user.username}</td>
<td>{userRoles[tm.role]}</td>
<td>{tm.createdAt}</td>
</tr>
))}
</tbody>
</Table>
</>);
}

View file

@ -0,0 +1,47 @@
import { useState } from "react";
import { Form, Button, Row, Col } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
import { ajax } from "../utils/fetch";
export default () => {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const navigate = useNavigate();
const onCreate = (e) => {
e.preventDefault();
ajax("/api/teams/", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
}).then(({team}) => {
if (!team) {
setError("Ошибка создания команды")
return
}
navigate(`/teams/${team.ID}`)
})
}
return (<>
<h1>Создание команды</h1>
<Form onSubmit={onCreate}>
<div className="col-lg-8 px-0">
{error ? (<div className="alert alert-danger" role="alert">{error}</div>) : null}
<Form.Group as={Row} className="mb-3" controlId="name">
<Form.Label column sm="4">Название команды</Form.Label>
<Col sm="8">
<Form.Control
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</Col>
</Form.Group>
<Button type="submit" size="lg">Создать</Button>
</div>
</Form>
</>)
};

View file

@ -0,0 +1,31 @@
import { Link, useLoaderData } from "react-router-dom";
import { Table } from 'react-bootstrap';
import { UserProvider } from "../store/user";
export default () => {
const teams = useLoaderData();
if (!teams) {
return null
}
return (<>
<h1>Команды</h1>
<Table>
<thead>
<tr>
<th>Команда</th>
<th>Участников</th>
<th>Создана</th>
</tr>
</thead>
<tbody>
{teams.map(team => (
<tr key={team.id} className={team.currentTeam?"table-active":null}>
<td><Link to={`/teams/${team.id}`}>{team.name}</Link></td>
<td>{team.members}</td>
<td>{team.createdAt}</td>
</tr>
))}
</tbody>
</Table>
</>);
}

View file

@ -0,0 +1,5 @@
import { UserProvider } from "./user";
export const store = [
UserProvider.Provider,
];

View file

@ -0,0 +1,9 @@
import { useState } from 'react';
import { createContainer } from 'unstated-next';
const useUser = () => {
const [user, setUser] = useState(null);
return { user, setUser }
};
export const UserProvider = createContainer(useUser);

View file

@ -0,0 +1,11 @@
export const ajax = async (path, params) => {
return fetch(path, params)
.then(r => {
if (r.status < 200 || r.status >= 300) {
throw Error(r.statusText)
}
return r
})
.then(r => r.json())
.catch(() => null)
}

17
frontend/vite.config.js Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
ws: false,
}
}
}
})

63
go.mod Normal file
View file

@ -0,0 +1,63 @@
module gitrepo.ru/neonxp/nquest
go 1.21.3
require (
github.com/dimuska139/go-email-normalizer v1.2.1
github.com/getkin/kin-openapi v0.120.0
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo-contrib v0.15.0
github.com/labstack/echo/v4 v4.11.2
github.com/oapi-codegen/runtime v1.0.0
golang.org/x/crypto v0.14.0
gorm.io/driver/postgres v1.5.3
gorm.io/gorm v1.25.5
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/sessions v1.2.1
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/oapi-codegen/echo-middleware v1.0.1
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wader/gormstore/v2 v2.0.3
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

323
go.sum Normal file
View file

@ -0,0 +1,323 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/dimuska139/go-email-normalizer v1.2.1 h1:pJNZnU7uS9MRoYqpoir05B+bCYXrS9sPGE4G1o9EDA8=
github.com/dimuska139/go-email-normalizer v1.2.1/go.mod h1:fGPWcd/7PSz9aOHusKVYmDk+oKahH/fZTCQ7tTU7e0Y=
github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg=
github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
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/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oapi-codegen/echo-middleware v1.0.1 h1:edYGScq1phCcuDoz9AqA9eHX+tEI1LNL5PL1lkkQh1k=
github.com/oapi-codegen/echo-middleware v1.0.1/go.mod h1:DBQKRn+D/vfXOFbaX5GRwFttoJY64JH6yu+pdt7wU3o=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wader/gormstore/v2 v2.0.3 h1:/29GWPauY8xZkpLnB8hsp+dZfP3ivA9fiDw1YVNTp6U=
github.com/wader/gormstore/v2 v2.0.3/go.mod h1:sr3N3a8F1+PBc3fHoKaphFqDXLRJ9Oe6Yow0HxKFbbg=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/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=
gorm.io/driver/mysql v1.4.0 h1:P+gpa0QGyNma39khn1vZMS/eXEJxTwHz4Q26NR4C8fw=
gorm.io/driver/mysql v1.4.0/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.4.1/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
gorm.io/driver/postgres v1.5.3 h1:qKGY5CPHOuj47K/VxbCXJfFvIUeqMSXXadqdCY+MbBU=
gorm.io/driver/postgres v1.5.3/go.mod h1:F+LtvlFhZT7UBiA81mC9W6Su3D4WUhSboc/36QZU0gk=
gorm.io/driver/sqlite v1.4.1 h1:ThZ3dRIbTbWGvaMHSVjgf0sb6SRJMNRyQAwfLo25+cM=
gorm.io/driver/sqlite v1.4.1/go.mod h1:AKZZCAoFfOWHF7Nd685Iq8Uywc0i9sWJlzpoE/INzsw=
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

169
main.go Normal file
View file

@ -0,0 +1,169 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
oapiMiddleware "github.com/oapi-codegen/echo-middleware"
"github.com/wader/gormstore/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gitrepo.ru/neonxp/nquest/api"
appmiddleware "gitrepo.ru/neonxp/nquest/pkg/contextlib"
"gitrepo.ru/neonxp/nquest/pkg/controller"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
func main() {
cfg, err := GetConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config\n: %s", err)
os.Exit(1)
}
db, err := gorm.Open(postgres.Open(cfg.DSN()), &gorm.Config{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error DB connection\n: %s", err)
os.Exit(1)
}
// db.Use(prometheus.New(prometheus.Config{
// DBName: "db1", // use `DBName` as metrics label
// RefreshInterval: 15, // Refresh metrics interval (default 15 seconds)
// MetricsCollector: []prometheus.MetricsCollector{
// &prometheus.MySQL{
// VariableNames: []string{"Threads_running"},
// },
// }, // user defined metrics
// }))
if err := db.AutoMigrate(
&models.User{},
&models.Team{},
&models.TeamMember{},
&models.TeamRequest{},
&models.Game{},
&models.GamePassing{},
&models.Team{},
&models.TeamAtGame{},
&models.Task{},
&models.Solution{},
&models.Code{},
); err != nil {
fmt.Fprintf(os.Stderr, "Error DB migration\n: %s", err)
os.Exit(1)
}
// --[ Services ]--
userService := service.NewUser(db)
teamService := service.NewTeam(db)
gameService := service.NewGame(db)
// --[ HTTP server ]--
e := echo.New()
e.Debug = true
store := gormstore.New(db, []byte(cfg.Secret))
quit := make(chan struct{})
defer func() {
close(quit)
}()
go store.PeriodicCleanup(12*time.Hour, quit)
// userMW := appmiddleware.User(models.RoleUser, userService)
authFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
echoCtx := ctx.Value(oapiMiddleware.EchoContextKey).(echo.Context)
user := appmiddleware.GetUser(echoCtx)
if user != nil {
return nil
}
return echo.ErrForbidden
}
swagger, err := api.GetSwagger()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
os.Exit(1)
}
swagger.Servers = []*openapi3.Server{{URL: "/api", Description: "Needed to match path"}}
e.Use(
middleware.Recover(),
middleware.RequestID(),
session.Middleware(store),
middleware.Logger(),
middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)),
middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "cookie:_csrf",
CookiePath: "/",
// CookieDomain: "nquest.ru",
// CookieSecure: true,
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteStrictMode,
}),
middleware.Gzip(),
echoprometheus.NewMiddleware("nquest"),
appmiddleware.User(userService),
)
// --[ Router ]--
handler := serverRouter{
Game: &controller.Game{
GameService: gameService,
},
User: &controller.User{
UserService: userService,
},
Team: &controller.Team{
UserService: userService,
TeamService: teamService,
},
Engine: &controller.Engine{
GameService: gameService,
},
}
codegen := e.Group("")
codegen.Use(
oapiMiddleware.OapiRequestValidatorWithOptions(
swagger,
&oapiMiddleware.Options{
Options: openapi3filter.Options{
AuthenticationFunc: authFunc,
},
},
),
)
api.RegisterHandlersWithBaseURL(codegen, handler, "/api")
e.FileFS("/", "index.html", distIndexHtml)
e.StaticFS("/", distDirFS)
// --[ System ]--
e.GET("/metrics", echoprometheus.NewHandler())
e.Logger.Fatal(e.Start(cfg.Listen))
}
type serverRouter struct {
*controller.Game
*controller.User
*controller.Team
*controller.Engine
}

39
pkg/contextlib/user.go Normal file
View file

@ -0,0 +1,39 @@
package contextlib
import (
"context"
"github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
type userKeyCtx struct{}
func GetUser(c echo.Context) *models.User {
user, ok := c.Request().Context().Value(userKeyCtx{}).(*models.User)
if !ok {
return nil
}
return user
}
func User(userService *service.User) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := userService.GetUser(c)
if user == nil {
return next(c)
}
ctx := c.Request().Context()
ctx = context.WithValue(ctx, userKeyCtx{}, user)
c.SetRequest(c.Request().WithContext(ctx))
return next(c)
}
}
}

55
pkg/controller/engine.go Normal file
View file

@ -0,0 +1,55 @@
package controller
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
type Engine struct {
GameService *service.Game
}
func (ec *Engine) Get(c echo.Context) error {
user := contextlib.GetUser(c)
team := user.Team.Team
gameID, err := strconv.Atoi(c.Param("ID"))
if err != nil {
return err
}
game, err := ec.GameService.GetByID(c.Request().Context(), uint(gameID))
if err != nil {
return err
}
state, err := ec.GameService.GetState(c.Request().Context(), game, team)
if err != nil {
return err
}
history, err := ec.GameService.GetHistory(c.Request().Context(), game, team)
if err != nil {
return err
}
return c.Render(
http.StatusOK,
"engine_view",
&GetReponse{
Game: game,
State: state,
History: history,
},
)
}
type GetReponse struct {
Game *models.Game
State *models.GamePassing
History []*models.GamePassing
}

27
pkg/controller/game.go Normal file
View file

@ -0,0 +1,27 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
type Game struct {
GameService *service.Game
}
func (index *Game) Index(c echo.Context) error {
games, err := index.GameService.List(c.Request().Context())
if err != nil {
return err
}
return c.Render(http.StatusOK, "index", &GamesResponse{Games: games})
}
type GamesResponse struct {
Games []*models.Game
}

99
pkg/controller/team.go Normal file
View file

@ -0,0 +1,99 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/api"
"gitrepo.ru/neonxp/nquest/pkg/contextlib"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
type Team struct {
UserService *service.User
TeamService *service.Team
}
func (t *Team) GetTeams(ctx echo.Context) error {
currentTeamID := uint(0)
user := contextlib.GetUser(ctx)
if user.Team != nil {
currentTeamID = user.Team.TeamID
}
teams, err := t.TeamService.List(ctx.Request().Context())
if err != nil {
return err
}
resp := make([]api.TeamListItem, 0, len(teams))
for _, t := range teams {
resp = append(resp, api.TeamListItem{
Id: int(t.ID),
Members: len(t.Members),
Name: t.Name,
CurrentTeam: currentTeamID == t.ID,
CreatedAt: t.CreatedAt.Format("02.01.06"),
})
}
return ctx.JSON(http.StatusOK, api.TeamsListResponse(resp))
}
func (t *Team) GetTeamsTeamID(ctx echo.Context, teamID int) error {
return t.getTeamResponse(ctx, teamID)
}
func (t *Team) DeleteTeamsTeamIDMembers(ctx echo.Context, teamID int) error {
return t.getTeamResponse(ctx, teamID)
}
func (t *Team) PostTeamsTeamIDMembers(ctx echo.Context, teamID int) error {
team, err := t.TeamService.GetByID(ctx.Request().Context(), uint(teamID))
if err != nil {
return ctx.JSON(http.StatusNotFound, api.ErrorResponse{
Code: http.StatusNotFound,
Message: err.Error(),
})
}
return t.getTeamResponse(ctx, teamID)
}
func (t *Team) getTeamResponse(ctx echo.Context, teamID int) error {
team, err := t.TeamService.GetByID(ctx.Request().Context(), uint(teamID))
if err != nil {
return ctx.JSON(http.StatusNotFound, api.ErrorResponse{
Code: http.StatusNotFound,
Message: err.Error(),
})
}
members := make([]api.TeamMember, 0, len(team.Members))
for _, tm := range team.Members {
members = append(members, api.TeamMember{
Role: api.MapRole[tm.Role],
User: api.UserView{
Id: int(tm.User.ID),
Username: tm.User.Username,
},
CreatedAt: tm.CreatedAt.Format("02.01.06"),
})
}
requests := make([]api.TeamRequest, 0, len(team.Requests))
for _, tm := range team.Requests {
requests = append(requests, api.TeamRequest{
User: api.UserView{
Id: int(tm.User.ID),
Username: tm.User.Username,
},
CreatedAt: tm.CreatedAt.Format("02.01.06"),
})
}
return ctx.JSON(http.StatusOK, api.TeamResponse(api.TeamResponse{
Id: teamID,
Name: team.Name,
Members: members,
Requests: requests,
CreatedAt: team.CreatedAt.Format("02.01.06"),
}))
}

143
pkg/controller/user.go Normal file
View file

@ -0,0 +1,143 @@
package controller
import (
"net/http"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/api"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gitrepo.ru/neonxp/nquest/pkg/service"
)
type User struct {
UserService *service.User
}
func (u *User) PostUserLogin(c echo.Context) error {
req := new(api.PostUserLoginJSONRequestBody)
if err := c.Bind(req); err != nil {
return err
}
user, err := u.UserService.Login(
c.Request().Context(),
req.Email,
req.Password,
)
if err != nil {
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
if err := setUser(c, user); err != nil {
if err != nil {
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
}
return c.JSON(http.StatusOK, &api.UserResponse{
Id: int(user.ID),
Username: user.Username,
Email: user.Email,
Team: api.MapUserTeam(user.Team),
})
}
func (u *User) PostUserRegister(c echo.Context) error {
req := new(api.PostUserRegisterJSONRequestBody)
if err := c.Bind(req); err != nil {
return err
}
user, err := u.UserService.Register(
c.Request().Context(),
req.Username,
req.Email,
req.Password,
req.Password2,
)
if err != nil {
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
if err := setUser(c, user); err != nil {
return c.JSON(http.StatusBadRequest, &api.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
return c.JSON(http.StatusOK, &api.UserResponse{
Id: int(user.ID),
Username: user.Username,
Email: user.Email,
Team: api.MapUserTeam(user.Team),
})
}
func (u *User) PostUserLogout(c echo.Context) error {
if err := setUser(c, nil); err != nil {
return err
}
return c.NoContent(http.StatusOK)
}
func (u *User) GetUser(c echo.Context) error {
user := u.UserService.GetUser(c)
if user == nil {
return c.JSON(http.StatusNotFound, &api.ErrorResponse{
Code: http.StatusNotFound,
Message: "User not found",
})
}
return c.JSON(http.StatusOK, &api.UserResponse{
Id: int(user.ID),
Username: user.Username,
Email: user.Email,
Team: api.MapUserTeam(user.Team),
})
}
func setUser(c echo.Context, user *models.User) error {
sess, err := session.Get("session", c)
if err != nil {
return err
}
if user == nil {
sess.Options = &sessions.Options{
Path: "/",
MaxAge: -86400 * 7,
HttpOnly: true,
}
if err := sess.Save(c.Request(), c.Response()); err != nil {
return err
}
return nil
}
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
}
sess.Values["userID"] = user.ID
if err := sess.Save(c.Request(), c.Response()); err != nil {
return err
}
return nil
}

54
pkg/models/game.go Normal file
View file

@ -0,0 +1,54 @@
package models
import (
"time"
)
type Game struct {
Model
Visible bool `gorm:"index"`
Title string
Description string
StartAt time.Time
Teams []*TeamAtGame `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Tasks []*Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
FirstTask *Task `gorm:"foreignKey:ID"`
FirstTaskID uint
}
type TeamAtGame struct {
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
TeamID uint `gorm:"primaryKey"`
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
GameID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
}
type GamePassing struct {
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
TeamID uint `gorm:"primaryKey"`
Game *Game `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
GameID uint `gorm:"primaryKey"`
Task *Task `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
TaskID uint `gorm:"primaryKey"`
CreatedAt time.Time
FinishedAt *time.Time
Deadline time.Time
Status Passing
Codes []*Code `gorm:"many2many:passing_codes;"`
}
func (g *GamePassing) Timeouted() bool {
return g.Deadline.Before(time.Now())
}
type Passing int
const (
PassStarted Passing = iota
PassFinished
PassCanceled
PassFailed
)

14
pkg/models/model.go Normal file
View file

@ -0,0 +1,14 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Model struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

31
pkg/models/task.go Normal file
View file

@ -0,0 +1,31 @@
package models
type Task struct {
Model
Title string
Text string
MaxTime int
Game *Game
GameID uint
Solutions []*Solution `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Codes []*Code `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Next *Task `gorm:"foreignKey:NextID"`
NextID *uint
}
type Solution struct {
Model
TaskID uint
After int
Text string
}
type Code struct {
Model
TaskID uint
Code string `gorm:"index"`
Description string
}

37
pkg/models/team.go Normal file
View file

@ -0,0 +1,37 @@
package models
import (
"time"
)
type Team struct {
Model
Name string `gorm:"unique,not_null" json:"name"`
Members []*TeamMember `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"members"`
Requests []*TeamRequest `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
Games []*TeamAtGame `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-"`
}
type TeamMember struct {
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"team"`
TeamID uint `gorm:"primaryKey" json:"-"`
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-"`
UserID uint `gorm:"primaryKey" json:"-"`
Role Role `json:"role"`
CreatedAt time.Time `json:"-"`
}
type TeamRequest struct {
Team *Team `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
TeamID uint `gorm:"primaryKey"`
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
UserID uint `gorm:"primaryKey"`
CreatedAt time.Time
}
type Role int
const (
Captain Role = iota
Member
)

27
pkg/models/user.go Normal file
View file

@ -0,0 +1,27 @@
package models
import (
"errors"
)
var (
ErrEmptyPassword = errors.New("empty password")
)
type User struct {
Model
Username string `gorm:"unique" json:"username"`
Email string `gorm:"unique" json:"email"`
Password string `json:"-"`
Team *TeamMember `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"team"`
Role UserRole `json:"role"`
}
type UserRole int
const (
RoleNotVerified UserRole = iota
RoleUser
RoleCreator
RoleAdmin
)

131
pkg/service/game.go Normal file
View file

@ -0,0 +1,131 @@
package service
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5/pgconn"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gorm.io/gorm"
)
var (
ErrGameNotStarted = errors.New("game not started")
)
type Game struct {
DB *gorm.DB
}
// NewGame returns new Game.
func NewGame(db *gorm.DB) *Game {
return &Game{
DB: db,
}
}
func (gs *Game) GetByID(ctx context.Context, id uint) (*models.Game, error) {
g := &models.Game{}
return g, gs.DB.
WithContext(ctx).
Preload("FirstTask").
First(g, id).
Error
}
func (gs *Game) List(ctx context.Context) ([]*models.Game, error) {
games := make([]*models.Game, 0)
return games, gs.DB.
WithContext(ctx).
Preload("Teams").
Preload("Teams.Team").
Order("start_at ASC").
Find(&games, "visible = true").
Limit(20).
Error
}
func (gs *Game) GetTaskID(ctx context.Context, id uint) (*models.Task, error) {
t := &models.Task{}
return t, gs.DB.WithContext(ctx).Preload("Next").First(t, id).Error
}
func (gs *Game) GetHistory(ctx context.Context, game *models.Game, team *models.Team) ([]*models.GamePassing, error) {
db := gs.DB.WithContext(ctx)
history := []*models.GamePassing{}
return history, db.Where(`team_id = ? and game_id = ?`, team.ID, game.ID).Find(&history).Error
}
func (gs *Game) GetState(ctx context.Context, game *models.Game, team *models.Team) (*models.GamePassing, error) {
db := gs.DB.WithContext(ctx)
if !game.StartAt.Before(time.Now()) {
return nil, ErrGameNotStarted
}
// Пытаемся получить GamePassing
gamepass := &models.GamePassing{
Team: team,
Game: game,
Task: game.FirstTask,
Status: models.PassStarted,
Codes: []*models.Code{},
CreatedAt: game.StartAt,
Deadline: game.StartAt.Add(time.Minute * time.Duration(game.FirstTask.MaxTime)),
}
err := db.
Where(`team_id = ? and game_id = ? and status = ?`, team.ID, game.ID, models.PassStarted).
Preload("Task").
FirstOrCreate(gamepass).
Error
if err != nil {
if err, ok := err.(*pgconn.PgError); ok {
if err.Code == "23505" {
return nil, nil
}
}
return nil, err
}
for {
if !gamepass.Timeouted() {
break
}
gamepass.Status = models.PassFailed
gamepass.FinishedAt = &gamepass.Deadline
if err := db.Save(gamepass).Error; err != nil {
return nil, err
}
taskID := gamepass.TaskID
task, err := gs.GetTaskID(ctx, taskID)
if err != nil {
return nil, err
}
if task.Next == nil {
return nil, nil
}
gamepass = &models.GamePassing{
Team: team,
Game: game,
Task: task.Next,
CreatedAt: gamepass.Deadline,
Deadline: gamepass.Deadline.Add(time.Minute * time.Duration(task.Next.MaxTime)),
Status: models.PassStarted,
Codes: []*models.Code{},
}
if err := db.Create(gamepass).Error; err != nil {
return nil, err
}
}
return gamepass, nil
}

118
pkg/service/team.go Normal file
View file

@ -0,0 +1,118 @@
package service
import (
"context"
"errors"
"slices"
"gitrepo.ru/neonxp/nquest/pkg/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrTeamNotFound = errors.New("team not found")
)
type Team struct {
DB *gorm.DB
}
// NewTeam returns new Team.
func NewTeam(db *gorm.DB) *Team {
return &Team{
DB: db,
}
}
func (ts *Team) List(ctx context.Context) ([]*models.Team, error) {
teams := []*models.Team{}
return teams, ts.DB.WithContext(ctx).Preload("Members").Find(&teams).Error
}
func (ts *Team) Create(ctx context.Context, name string, user *models.User) (*models.Team, error) {
t := &models.Team{
Name: name,
Members: []*models.TeamMember{{
User: user,
Role: models.Captain,
}},
}
db := ts.DB.WithContext(ctx)
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
return t, err
}
return t, db.Create(t).Error
}
func (ts *Team) GetByID(ctx context.Context, id uint) (*models.Team, error) {
t := new(models.Team)
err := ts.DB.
WithContext(ctx).
Preload("Members").
Preload("Members.User").
Preload("Requests").
Preload("Requests.User").
First(t, id).
Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTeamNotFound
}
return nil, err
}
return t, nil
}
func (ts *Team) Request(ctx context.Context, team *models.Team, user *models.User) error {
return ts.DB.
WithContext(ctx).
Clauses(clause.OnConflict{DoNothing: true}).
Create(&models.TeamRequest{
Team: team,
User: user,
}).
Error
}
func (ts *Team) ApproveMember(ctx context.Context, team *models.Team, user *models.User) error {
team.Requests = slices.DeleteFunc(team.Requests, func(tr *models.TeamRequest) bool {
return tr.UserID == user.ID
})
team.Members = append(team.Members, &models.TeamMember{
Team: team,
User: user,
Role: models.Member,
})
db := ts.DB.WithContext(ctx)
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
return err
}
return db.Save(team).Error
}
func (ts *Team) DeclineMember(ctx context.Context, team *models.Team, user *models.User) error {
team.Requests = slices.DeleteFunc(team.Requests, func(tr *models.TeamRequest) bool {
return tr.UserID == user.ID
})
db := ts.DB.WithContext(ctx)
if err := db.Delete(&models.TeamRequest{}, `user_id = ?`, user.ID).Error; err != nil {
return err
}
return db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(team).Error
}
func (ts *Team) RemoveMember(ctx context.Context, team *models.Team, user *models.User) error {
return ts.DB.WithContext(ctx).Delete(&models.TeamMember{}, `user_id = ? and team_id = ?`, user.ID, team.ID).Error
}

136
pkg/service/user.go Normal file
View file

@ -0,0 +1,136 @@
package service
import (
"context"
"encoding/hex"
"errors"
"net/mail"
normalizer "github.com/dimuska139/go-email-normalizer"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gitrepo.ru/neonxp/nquest/pkg/models"
)
var (
ErrInvalidUsername = errors.New("invalid username")
ErrInvalidPassword = errors.New("invalid password")
ErrInvalidEmail = errors.New("invalid email")
ErrDiffferentPassword = errors.New("different password")
ErrDuplicateUser = errors.New("duplicate user")
ErrIvalidUserOrPassword = errors.New("invalid user or password")
)
type User struct {
DB *gorm.DB
}
// NewUser returns new User.
func NewUser(db *gorm.DB) *User {
return &User{
DB: db,
}
}
func (s *User) Register(ctx context.Context, username, email, password, password2 string) (*models.User, error) {
if len(username) < 3 || len(username) > 36 {
return nil, ErrInvalidUsername
}
if len(password) < 8 {
return nil, ErrInvalidPassword
}
if password != password2 {
return nil, ErrDiffferentPassword
}
if !isValidEmail(email) {
return nil, ErrInvalidEmail
}
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u := &models.User{
Username: username,
Email: normalizer.NewNormalizer().Normalize(email),
Password: hex.EncodeToString(hashed),
Role: models.RoleUser,
}
err = s.DB.WithContext(ctx).Create(u).Error
switch {
case errors.Is(err, models.ErrEmptyPassword):
return nil, ErrInvalidPassword
case errors.Is(err, gorm.ErrDuplicatedKey):
return nil, ErrDuplicateUser
case err != nil:
return nil, err
}
return u, nil
}
func (s *User) Login(ctx context.Context, email, password string) (*models.User, error) {
u := new(models.User)
nemail := normalizer.NewNormalizer().Normalize(email)
err := s.DB.
WithContext(ctx).
Where("email = ?", nemail).
Preload("Team").Preload("Team.Team").
First(u).
Error
if err != nil {
return nil, ErrIvalidUserOrPassword
}
b, err := hex.DecodeString(u.Password)
if err != nil {
return nil, ErrIvalidUserOrPassword
}
if err := bcrypt.CompareHashAndPassword(b, []byte(password)); err != nil {
return nil, ErrIvalidUserOrPassword
}
return u, nil
}
func (s *User) GetUserByID(ctx context.Context, userID uint) (*models.User, error) {
u := new(models.User)
return u, s.DB.WithContext(ctx).Preload("Team").Preload("Team.Team").First(u, userID).Error
}
func (s *User) GetUser(c echo.Context) *models.User {
sess, err := session.Get("session", c)
if err != nil {
return nil
}
userID, ok := sess.Values["userID"].(uint)
if !ok {
return nil
}
user, err := s.GetUserByID(c.Request().Context(), userID)
if err != nil {
return nil
}
return user
}
func (s *User) Update(ctx context.Context, user *models.User) error {
return s.DB.WithContext(ctx).Session(&gorm.Session{FullSaveAssociations: true}).Save(user).Error
}
func isValidEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}

83
views/engine/view.gotmpl Normal file
View file

@ -0,0 +1,83 @@
{{ template "header" . }}
{{ if .State }}
<h5 class="card-title mb-2">Уровень</h5>
<table class="table table-bordered mb-4">
<thead class="table-primary">
<th colspan="4">
{{.State.Task.Title}}
</th>
</thead>
<tbody>
<tr>
<td>Выдано:</td>
<td>{{ .State.CreatedAt.Format "15:04 02.01.2006" }}</td>
<td>Автопереход:</td>
<td>{{ .State.Deadline.Format "15:04 02.01.2006" }} (через {{ (.State.Deadline.Sub now) | toTime }})</td>
</tr>
<tr>
<td colspan="4">{{ .State.Task.Text | markDown }}</td>
</tr>
</tbody>
</table>
<table class="table table-bordered mb-4">
<thead class="table-primary">
<th colspan="1">
Ввод кода
</th>
</thead>
<tbody>
<tr>
<td>
<form method="post" action="/go/{{ .Game.ID }}/code">
<div class="col-lg-8 px-0">
<div class="mb-3 row">
<div class="col-sm-10">
<input type="text" class="form-control" id="code" name="code">
</div>
<input type="submit" class="col-sm-2 btn btn-outline-primary" value="Ввод" />
</div>
</div>
</form>
</td>
</tr>
</tbody>
</table>
<h5 class="card-title">История игры</h5>
<table class="table table-bordered">
<thead class="table-primary">
<tr>
<th scope="col" class="thin">Уровень</th>
<th scope="col" class="thin">Время начала</th>
<th scope="col" class="thin">Время окончания</th>
<th scope="col">Статус</th>
</tr>
</thead>
<tbody>
{{ range $i, $a := .History }}
<tr>
<td>{{ inc $i }}</td>
<td>{{ $a.CreatedAt.Format "15:04 02.01.2006" }}</td>
<td>{{ $a.Deadline.Format "15:04 02.01.2006" }}</td>
<td>
{{ if eq $a.Status 0 }}
Текущее
{{ else if eq $a.Status 1 }}
Пройден
{{ else if eq $a.Status 2 }}
Снят
{{ else if eq $a.Status 3 }}
Автопереход
{{ end }}
</td>
</tr>
{{ end }}
</tbody>
</table>
{{ else }}
<p>Вам не предусмотренно следующее задание</p>
{{ end }}
{{ template "footer" . }}

48
views/index.gotmpl Normal file
View file

@ -0,0 +1,48 @@
{{ template "header" . }}
<h1>Текущие игры</h1>
{{ range .Games }}
<table class="table table-bordered mb-4">
<thead class="table-primary">
<th colspan="2">
{{.Title}}
</th>
</thead>
<tbody>
<tr>
<td>
Начало
</td>
<td>
{{ .StartAt.Format "15:04 02.01.2006" }}
</td>
</tr>
<tr>
<td colspan="2">
{{ .Description | markDown }}
<div>
<a href="/go/{{ .ID }}" class="btn btn-primary">Войти в игру</a>
</div>
</td>
</tr>
<tr>
<td>
Участвуют:
</td>
<td>
<ul>
{{ range .Teams }}
<li><a href="/team/{{.Team.ID}}">{{.Team.Name}}</a></li>
{{ else }}
Никто пока не подал заявку
{{ end }}
</ul>
</td>
</tr>
</tbody>
</table>
{{ else }}
<p>
<strong>Игр пока не анонсировано</strong>
</p>
{{ end }}
{{ template "footer" . }}

67
views/layout.gotmpl Normal file
View file

@ -0,0 +1,67 @@
{{define "header"}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>nQuest</title>
<link href="/static/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/styles.css">
</head>
<body data-bs-theme="dark">
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container">
<a class="navbar-brand" href="https://nquest.ru/">nQuest</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Игры</a>
</li>
{{ if .User }}
<li class="nav-item">
<a class="nav-link" href="/teams">Команды</a>
</li>
{{ end }}
</ul>
<div class="d-flex">
{{ if .User }}
<span class="navbar-text me-2">
{{ .User.Username }}
{{ if .User.Team }}
(<a href="/team">{{.User.Team.Team.Name}}</a>)
{{ else }}
(без команды)
{{ end }}
</span>
<form method="POST" action="/user/logout">
<input type="submit" class="btn btn-defult" value="Выход" /></a>
</form>
{{ else }}
<div class="btn-group">
<a class="btn btn-success" href="/user/login">Вход</a>
<a class="btn btn-outline-success" href="/user/register">Регистрация</a>
</div>
{{ end }}
</form>
</div>
</div>
</nav>
<div class="container my-5">
{{end}}
{{define "footer"}}
</div>
</body>
</html>
{{end}}

21
views/team/create.gotmpl Normal file
View file

@ -0,0 +1,21 @@
{{ template "header" . }}
<h1>Новая команда</h1>
<form method="POST">
<div class="col-lg-8 px-0">
{{ if .Error }}
<div class="alert alert-danger" role="alert">{{ .Error }}</div>
{{ end }}
<div class="mb-3 row">
<label for="staticEmail" class="col-sm-4 col-form-label">Название команды</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="name" value="{{ .Name }}">
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-8 offset-md-4">
<input type="submit" class="form-control btn btn-primary" value="Регистрация">
</div>
</div>
</div>
</form>
{{ template "footer" . }}

35
views/team/list.gotmpl Normal file
View file

@ -0,0 +1,35 @@
{{ template "header" . }}
<h1>Команды</h1>
{{$MyTeam := 0}}
{{if and .User .User.Team }}
{{$MyTeam = .User.Team.Team.ID}}
{{end}}
{{if and .User (not .User.Team) }}
<div class="d-grid gap-2 col-6 mx-auto mb-2">
<a href="/team/new" class="btn btn-primary">Создать команду</a>
</div>
{{end}}
<table class="table table-bordered">
<thead class="table-primary">
<tr>
<th scope="col">Название</th>
<th scope="col" class="thin">Количество участников</th>
</tr>
</thead>
<tbody>
{{range .Teams}}
<tr class="{{ if eq $MyTeam .ID }}active{{ end }}">
<td>
<a href="/team/{{.ID}}">
{{.Name}}
</a>
</td>
<td>
{{len .Members}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{ template "footer" . }}

130
views/team/view.gotmpl Normal file
View file

@ -0,0 +1,130 @@
{{ template "header" . }}
{{ $IsAdmin := false }}
{{ if and .User.Team (eq .Team.ID .User.Team.TeamID) (eq .User.Team.Role 0) }}
{{ $IsAdmin = true }}
{{ end }}
<h1>{{ .Team.Name }}</h1>
<p class="fw-light">Создана {{ .Team.CreatedAt.Format "02.01.2006" }}</p>
{{ if $IsAdmin }}
<p>Вы капитан команды</p>
{{ end }}
{{ if or (not .User.Team) (ne .Team.ID .User.Team.TeamID) }}
{{ $requested := false }}
{{ $userID := .User.ID }}
{{ range .Team.Requests }}
{{ if eq .User.ID $userID }}
{{ $requested = true }}
{{ end }}
{{ end }}
{{ if $requested }}
<div class="alert alert-secondary">Заявка в команду рассматривается.</div>
{{ else }}
<form method="post" class="mt-2" action="/team/{{.Team.ID}}/request">
<input type="submit" class="btn btn-outline-primary" value="Отправить заявку в команду" />
</form>
{{ end }}
{{ end }}
<h2 class="mt-4">Участники</h2>
<table class="table table-bordered">
<thead class="table-primary">
<tr>
<th>Ник</th>
<th class="thin">Роль</th>
<th class="thin">Дата вступления</th>
{{ if $IsAdmin }}
<th class="thin">Действие</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range .Team.Members }}
<tr>
<td>
{{.User.Username}}
</td>
<td>
{{if eq .Role 0 }}
Капитан
{{else if eq .Role 1 }}
Участник
{{end}}
</td>
<td>
{{ .CreatedAt.Format "15:04 02.01.2006" }}
</td>
{{ if and $IsAdmin (ne .Role 0) }}
<td>
<form method="post" action="/team/member/remove">
<input type="hidden" name="member_id" value="{{.User.ID}}">
<input type="submit" class="btn btn-outline-danger" value="Выгнать" />
</form>
</td>
{{ else if $IsAdmin }}
<td>&nbsp;</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
<p>
Чтобы добавить участников в команду пришлите им ссылку на команду
(<a href="https://nquest.ru/team/{{.Team.ID}}">https://nquest.ru/team/{{.Team.ID}}</a>).
</p>
<p>
По этой ссылке будущие участники могут подать заявку на вступление в команду.
</p>
{{ if $IsAdmin }}
<h2>Заявки</h2>
<table class="table table-bordered">
<thead class="table-primary">
<tr>
<th>Ник</th>
<th class="thin">Дата заявки</th>
<th class="thin">Действие</th>
</tr>
</thead>
<tbody>
{{ range .Team.Requests }}
<tr>
<td>
{{ .User.Username }}
</td>
<td>
{{ .CreatedAt.Format "15:04 02.01.2006" }}
</td>
<td>
<form method="post" action="/team/member/approve">
<input type="hidden" name="member_id" value="{{ .User.ID }}">
<div class="btn-group">
<input type="submit" name="approve" class="btn btn-outline-success" value="Принять" />
<input type="submit" name="decline" class="btn btn-outline-danger" value="Отказать" />
</div>
</form>
</td>
</tr>
{{else}}
<tr>
<td colspan="3"><span>Нет заявок</span></td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}
{{ if and .User.Team (eq .Team.ID .User.Team.TeamID) (eq .User.Team.Role 1) }}
<form method="post" action="/team/member/leave">
<input type="submit" class="btn btn-outline-danger" value="Покинуть команду" />
</form>
{{ end }}
{{ template "footer" . }}

27
views/user/login.gotmpl Normal file
View file

@ -0,0 +1,27 @@
{{ template "header" . }}
<h1>Вход</h1>
<form method="POST">
<div class="col-lg-8 px-0">
{{ if .Error }}
<div class="alert alert-danger" role="alert">{{ .Error }}</div>
{{ end }}
<div class="mb-3 row">
<label for="staticEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="staticEmail" value="{{ .Email }}" name="email">
</div>
</div>
<div class="mb-3 row">
<label for="inputPassword" class="col-sm-2 col-form-label">Пароль</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="inputPassword" name="password">
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-10 offset-md-2">
<input type="submit" class="form-control btn btn-primary" value="Вход">
</div>
</div>
</div>
</form>
{{ template "footer" . }}

View file

@ -0,0 +1,39 @@
{{ template "header" . }}
<h1>Регистрация</h1>
<form method="POST">
<div class="col-lg-8 px-0">
{{ if .Error }}
<div class="alert alert-danger" role="alert">{{ .Error }}</div>
{{ end }}
<div class="mb-3 row">
<label for="staticEmail" class="col-sm-4 col-form-label">Имя пользователя</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="username" value="{{ .Username }}">
</div>
</div>
<div class="mb-3 row">
<label for="staticEmail" class="col-sm-4 col-form-label">Email</label>
<div class="col-sm-8">
<input type="text" class="form-control" name="email" value="{{ .Email }}">
</div>
</div>
<div class="mb-3 row">
<label for="inputPassword" class="col-sm-4 col-form-label">Пароль</label>
<div class="col-sm-8">
<input type="password" class="form-control" name="password" value="{{ .Password }}">
</div>
</div>
<div class="mb-3 row">
<label for="inputPassword" class="col-sm-4 col-form-label">Повторите пароль</label>
<div class="col-sm-8">
<input type="password" class="form-control" name="password_2" value="{{ .Password2 }}">
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-8 offset-md-2">
<input type="submit" class="form-control btn btn-primary" value="Регистрация">
</div>
</div>
</div>
</form>
{{ template "footer" . }}