This commit is contained in:
Александр Кирюхин 2024-05-05 22:26:52 +03:00
parent a1dc96088c
commit 1ad012b8fa
No known key found for this signature in database
GPG key ID: 35E33E1AB7776B39
14 changed files with 136 additions and 52 deletions

19
Caddyfile Normal file
View file

@ -0,0 +1,19 @@
{
debug
}
http:// {
handle_path /file/* {
root * /app/store
file_server
}
handle /api* {
reverse_proxy http://app:8000
}
handle {
root * /app/frontend/dist
file_server
try_files {path} /index.html
encode gzip zstd
}
}

View file

@ -377,6 +377,19 @@ paths:
responses: responses:
200: 200:
$ref: '#/components/responses/taskResponse' $ref: '#/components/responses/taskResponse'
/file/{uid}:
get:
operationId: getFile
parameters:
- in: path
name: uid
required: true
schema:
format: uuid
type: string
response:
307:
description: redirect
/games: /games:
get: get:
responses: responses:

View file

@ -40,4 +40,17 @@ paths:
responses: responses:
200: 200:
$ref: "#/components/responses/taskResponse" $ref: "#/components/responses/taskResponse"
/file/{uid}:
get:
operationId: getFile
parameters:
- name: uid
in: path
required: true
schema:
type: string
format: uuid
response:
307:
description: redirect

View file

@ -43,6 +43,9 @@ type ServerInterface interface {
// (POST /engine/{uid}/code) // (POST /engine/{uid}/code)
EnterCode(ctx echo.Context, uid openapi_types.UUID) error EnterCode(ctx echo.Context, uid openapi_types.UUID) error
// (GET /file/{uid})
GetFile(ctx echo.Context, uid openapi_types.UUID) error
// (GET /games) // (GET /games)
GetGames(ctx echo.Context) error GetGames(ctx echo.Context) error
@ -176,6 +179,24 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error {
return err return err
} }
// GetFile converts echo context to params.
func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
err = runtime.BindStyledParameterWithOptions("simple", "uid", ctx.Param("uid"), &uid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
}
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetFile(ctx, uid)
return err
}
// GetGames converts echo context to params. // GetGames converts echo context to params.
func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
var err error var err error
@ -262,6 +283,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame) router.GET(baseURL+"/admin/games/:uid", wrapper.AdminGetGame)
router.GET(baseURL+"/engine/:uid", wrapper.GameEngine) router.GET(baseURL+"/engine/:uid", wrapper.GameEngine)
router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode) router.POST(baseURL+"/engine/:uid/code", wrapper.EnterCode)
router.GET(baseURL+"/file/:uid", wrapper.GetFile)
router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/games", wrapper.GetGames)
router.GET(baseURL+"/user", wrapper.GetUser) router.GET(baseURL+"/user", wrapper.GetUser)
router.POST(baseURL+"/user/login", wrapper.PostUserLogin) router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
@ -273,26 +295,27 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"0I910y50yiIW0sJWrst/AwAA//8rjKvDLRwAAA==",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View file

@ -14,7 +14,6 @@ RUN apk update --no-cache && apk add --no-cache tzdata
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download && go mod verify RUN go mod download && go mod verify
COPY . . COPY . .
COPY --from=frontend /app/dist /app/frontend/dist
RUN go build -ldflags="-X gitrepo.ru/neonxp/nquest.Version=${VERSION}" -o nquest *.go RUN go build -ldflags="-X gitrepo.ru/neonxp/nquest.Version=${VERSION}" -o nquest *.go
# Runtime container # Runtime container
@ -24,6 +23,7 @@ RUN apk update --no-cache && apk add --no-cache ca-certificates
COPY --from=backend /usr/share/zoneinfo/Europe/Moscow /usr/share/zoneinfo/Europe/Moscow COPY --from=backend /usr/share/zoneinfo/Europe/Moscow /usr/share/zoneinfo/Europe/Moscow
COPY --from=backend /app/nquest /app/nquest COPY --from=backend /app/nquest /app/nquest
COPY --from=frontend /app/dist /app/frontend/dist
ENV TZ Europe/Moscow ENV TZ Europe/Moscow

3
build/caddy/Dockerfile Normal file
View file

@ -0,0 +1,3 @@
FROM caddy:2.8-alpine
COPY Caddyfile /etc/caddy/Caddyfile

View file

@ -3,19 +3,30 @@ version: '3.8'
volumes: volumes:
postgres-data: postgres-data:
storage: storage:
frontend:
services: services:
caddy:
build:
context: .
dockerfile: build/caddy/Dockerfile
volumes:
- frontend:/app/frontend/dist:ro
- storage:/app/store:ro
ports:
- 8989:80
depends_on:
- app
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: build/api/Dockerfile
ports:
- 8989:8989
depends_on: depends_on:
- db - db
env_file: env_file:
- .env - .env
volumes: volumes:
- frontend:/app/frontend/dist:rw
- storage:/app/store:rw - storage:/app/store:rw
db: db:
image: postgres:15-alpine3.17 image: postgres:15-alpine3.17

View file

@ -1,17 +0,0 @@
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")
)

View file

@ -4,7 +4,7 @@ import { UserProvider } from '../store/user'
import { ajax } from '../utils/fetch' import { ajax } from '../utils/fetch'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { MenuOutlined } from '@ant-design/icons' import { MenuOutlined } from '@ant-design/icons'
import { Content, Header } from 'antd/es/layout/layout' import { Content, Footer, Header } from 'antd/es/layout/layout'
import { useRole } from '../utils/roles' import { useRole } from '../utils/roles'
const AppLayout = () => { const AppLayout = () => {
@ -80,9 +80,12 @@ const AppLayout = () => {
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
</Header> </Header>
<Content style={{ padding: '0 24px' }}> <Content style={{ padding: '0 24px', minHeight: '80vh' }}>
<Outlet /> <Outlet />
</Content> </Content>
<Footer>
Сделал <a href="https://neonxp.ru">NeonXP</a> в 2024.
</Footer>
</Layout>) </Layout>)
} }

View file

@ -8,12 +8,16 @@ const Index = () => {
const { user } = UserProvider.useContainer() const { user } = UserProvider.useContainer()
const navigate = useNavigate() const navigate = useNavigate()
return (<> return (<>
<Title>NQuest</Title> <Title><img src="/assets/logo.png" />uest</Title>
<Paragraph>Привет! Это платформа для ARG игр.</Paragraph> <Paragraph>Привет! Это платформа для ARG игр.</Paragraph>
<Paragraph> <Paragraph>
Если ты попал сюда случайно, то скорее всего, для тебя здесь нет ничего интересного. Если ты попал сюда случайно, то скорее всего, для тебя здесь нет ничего интересного.<br />
А если ты знаешь зачем пришёл, то добро пожаловать! А если ты знаешь зачем пришёл, то добро пожаловать!
</Paragraph> </Paragraph>
<Paragraph>
Телеграм-канал с новостями проекта: <a href="https://t.me/nquest_ru">nquest_ru</a>, рекомендую подписаться.<br />
Там будут как roadmap проекта, так и анонсы новых квестов.
</Paragraph>
{!user {!user
? (<Button.Group> ? (<Button.Group>
<Button type="primary" onClick={() => navigate('/login')}>Вход</Button> <Button type="primary" onClick={() => navigate('/login')}>Вход</Button>

View file

@ -38,16 +38,14 @@ const renderItem = (user, navigate, item) => {
</Popover>, </Popover>,
<Popover key='taskCount' content={`Этот квест состоит из ${item.taskCount} уровней`} title='Количество уровней в квесте'> <Popover key='taskCount' content={`Этот квест состоит из ${item.taskCount} уровней`} title='Количество уровней в квесте'>
<Space>{item.taskCount} ур</Space> <Space>{item.taskCount} ур</Space>
</Popover>, </Popover>
<>{moment(item.createdAt).fromNow()}</>,
<>Автор {item.authors.map(a => a.username)}</>
] ]
let questAction = (<span>Необходимо войти</span>) let questAction = (<span>Необходимо войти</span>)
if (user) { if (user) {
questAction = (user.games.find(x => x.id === item.id) questAction = (user.games.find(x => x.id === item.id)
? <span>Вы уже прошли этот квест</span> ? <b>Вы уже прошли этот квест</b>
: <Button onClick={() => navigate(`/go/${item.id}`)} type="primary">Начать квест</Button> : <Button onClick={() => navigate(`/go/${item.id}`)} type="primary">Начать квест</Button>
) )
} }

View file

@ -130,9 +130,6 @@ func main() {
// --[ System ]-- // --[ System ]--
e.GET("/metrics", echoprometheus.NewHandler()) e.GET("/metrics", echoprometheus.NewHandler())
e.StaticFS("/file", afero.NewIOFS(storage))
e.StaticFS("/*", distDirFS)
e.Logger.Debugf("backend version %s", Version) e.Logger.Debugf("backend version %s", Version)
e.Logger.Fatal(e.Start(cfg.Listen)) e.Logger.Fatal(e.Start(cfg.Listen))
} }

View file

@ -1,6 +1,8 @@
package controller package controller
import ( import (
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"gitrepo.ru/neonxp/nquest/api" "gitrepo.ru/neonxp/nquest/api"
@ -57,3 +59,12 @@ func (u *File) AdminListFiles(c echo.Context, quest uuid.UUID) error {
return c.JSON(200, list) return c.JSON(200, list)
} }
func (u *File) GetFile(c echo.Context, uid uuid.UUID) error {
f, err := u.FileService.GetFilesByID(c.Request().Context(), uid)
if err != nil {
return err
}
return c.Redirect(307, fmt.Sprintf(`/file/%s/%s`, f.QuestID, f.Filename))
}

View file

@ -57,3 +57,9 @@ func (u *File) GetFilesByQuest(ctx context.Context, quest uuid.UUID) ([]*models.
return list, u.DB.WithContext(ctx).Find(&list, `quest_id = ?`, quest.String()).Error return list, u.DB.WithContext(ctx).Find(&list, `quest_id = ?`, quest.String()).Error
} }
func (u *File) GetFilesByID(ctx context.Context, fileID uuid.UUID) (*models.File, error) {
file := &models.File{}
return file, u.DB.WithContext(ctx).First(file, fileID).Error
}