diff --git a/.env b/.env
new file mode 100644
index 0000000..5c9b084
--- /dev/null
+++ b/.env
@@ -0,0 +1,7 @@
+POSTGRES_HOSTNAME=db
+POSTGRES_DB=nquest
+POSTGRES_USER=nquest
+POSTGRES_PASSWORD=nquest
+POSTGRES_PORT=5432
+LISTEN=:8000
+SECRET=d51a5056-c8ad-4c2a-939e-c60cff9c1214
\ No newline at end of file
diff --git a/Caddyfile b/Caddyfile
index e8c3eaf..6be8b81 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -1,8 +1,7 @@
-{
- debug
-}
-
http:// {
+ header /* {
+ Cache-Control: no-cache, no-store, must-revalidate
+ }
handle_path /file/* {
root * /app/store
file_server
diff --git a/api/openapi.yaml b/api/openapi.yaml
index fae1052..500e13f 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -66,6 +66,19 @@ components:
schema:
$ref: '#/components/schemas/userView'
description: ""
+ userShortResponse:
+ content:
+ application/json:
+ $ref: '#/components/schemas/userShortView'
+ description: ""
+ usersListResponse:
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: '#/components/schemas/userShortView'
+ type: array
+ description: ""
schemas:
codeEdit:
properties:
@@ -213,6 +226,37 @@ components:
- text
- codes
type: object
+ userShortView:
+ properties:
+ expToCurrentLevel:
+ type: integer
+ expToNextLevel:
+ type: integer
+ experience:
+ type: integer
+ games:
+ items:
+ $ref: '#/components/schemas/gameView'
+ type: array
+ gamesFinished:
+ type: integer
+ gamesStarted:
+ type: integer
+ id:
+ format: uuid
+ type: string
+ level:
+ type: integer
+ username:
+ type: string
+ required:
+ - id
+ - username
+ - experience
+ - level
+ - gamesStarted
+ - gamesFinished
+ type: object
userView:
properties:
email:
@@ -457,6 +501,26 @@ paths:
400:
$ref: '#/components/responses/errorResponse'
security: []
+ /users:
+ get:
+ responses:
+ 200:
+ $ref: '#/components/responses/usersListResponse'
+ description: users list
+ /users/{uid}:
+ get:
+ operationId: getUserInfo
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ format: uuid
+ type: string
+ responses:
+ 200:
+ $ref: '#/components/responses/userShortResponse'
+ description: users list
security:
- cookieAuth: []
servers:
diff --git a/api/parts/responses.yaml b/api/parts/responses.yaml
index 1da6894..c1f72ea 100644
--- a/api/parts/responses.yaml
+++ b/api/parts/responses.yaml
@@ -63,4 +63,17 @@ components:
schema:
type: array
items:
- $ref: "#/components/schemas/fileItem"
\ No newline at end of file
+ $ref: "#/components/schemas/fileItem"
+ usersListResponse:
+ description: ""
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/userShortView"
+ userShortResponse:
+ description: ""
+ content:
+ application/json:
+ $ref: "#/components/schemas/userShortView"
\ No newline at end of file
diff --git a/api/parts/schemas.yaml b/api/parts/schemas.yaml
index ec63f25..0b9e707 100644
--- a/api/parts/schemas.yaml
+++ b/api/parts/schemas.yaml
@@ -1,5 +1,36 @@
components:
schemas:
+ userShortView:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ username:
+ type: string
+ experience:
+ type: integer
+ level:
+ type: integer
+ gamesStarted:
+ type: integer
+ gamesFinished:
+ type: integer
+ expToCurrentLevel:
+ type: integer
+ expToNextLevel:
+ type: integer
+ games:
+ type: array
+ items:
+ $ref: "#/components/schemas/gameView"
+ required:
+ - id
+ - username
+ - experience
+ - level
+ - gamesStarted
+ - gamesFinished
userView:
type: object
properties:
diff --git a/api/parts/user.yaml b/api/parts/user.yaml
index 25947dd..bf05453 100644
--- a/api/parts/user.yaml
+++ b/api/parts/user.yaml
@@ -55,3 +55,23 @@ paths:
description: "success logout"
400:
$ref: "#/components/responses/errorResponse"
+ /users:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/usersListResponse"
+ description: "users list"
+ /users/{uid}:
+ get:
+ operationId: getUserInfo
+ parameters:
+ - name: uid
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ 200:
+ $ref: "#/components/responses/userShortResponse"
+ description: "users list"
\ No newline at end of file
diff --git a/api/server.go b/api/server.go
index 52b980a..434ee65 100644
--- a/api/server.go
+++ b/api/server.go
@@ -60,6 +60,12 @@ type ServerInterface interface {
// (POST /user/register)
PostUserRegister(ctx echo.Context) error
+
+ // (GET /users)
+ GetUsers(ctx echo.Context) error
+
+ // (GET /users/{uid})
+ GetUserInfo(ctx echo.Context, uid openapi_types.UUID) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
@@ -248,6 +254,35 @@ func (w *ServerInterfaceWrapper) PostUserRegister(ctx echo.Context) error {
return err
}
+// GetUsers converts echo context to params.
+func (w *ServerInterfaceWrapper) GetUsers(ctx echo.Context) error {
+ var err error
+
+ ctx.Set(CookieAuthScopes, []string{})
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.GetUsers(ctx)
+ return err
+}
+
+// GetUserInfo converts echo context to params.
+func (w *ServerInterfaceWrapper) GetUserInfo(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.GetUserInfo(ctx, uid)
+ 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
@@ -289,33 +324,36 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
router.POST(baseURL+"/user/register", wrapper.PostUserRegister)
+ router.GET(baseURL+"/users", wrapper.GetUsers)
+ router.GET(baseURL+"/users/:uid", wrapper.GetUserInfo)
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/8xY30/kthP/V77yt48p2TtOqpQ3iig6FZ3aK9cXtEImGRYfiR3sCccW5X+vxk6yycb5",
- "wZJy98SSTObHZz4znvEzi1WWKwkSDYuemQaTK2nA/gNaK/25ekIPYiURJNJPnuepiDkKJcOvRkl6ZuI7",
- "yDj9yrXKQaNwemKV2M9xmwOLmJAIG9CsDFgGxvBN+6VBLeSGlWXANDwUQkPCoiunYie/Dmp5dfMVYmQl",
- "fZCAibXIyScWMdJ/K1IwF8LgQVEIhMwG8JOGWxax/4c7sEInZkIy8REhI3OVT1xrvh1yacMzOEkyIQ9y",
- "acwT0nyWCByz/N9iQRb+FvBtLhbIzf3iMJBS54TfaJGniicL0LooREJ/b5XOOLLIPQgmmGyF5vK3MKAX",
- "B4iUDgNUBpWapnQtp6aKuo52T6Hn/SGgWWN90AL7wsaylH+lx0hT4j0js2IJmNJiIyRPP/HM75MR/3g7",
- "5B4MVntHWfWpD5qmHfS8nsxQ7F5MxjUz/FyJ6njpnwBUrWZ2gyFp1+L2G0zAUGDqR9c9mG5dlyRXBuxR",
- "GHHT0XWjVApc9hJSS9bWu+yqLNdBNkAMZeuy8hNkkTntGguesoDFAretz3ahNR23l2Re4J3S86HddYU+",
- "tLEGjpCc4GEF/8Z0OlWFxIHX34EjLogxelQBtb1vYx40ufQRpykJbwecn/+m03vyPzMxCE9+hgzBPoaU",
- "VRZUQQwFPtz6Xxb4EPFb82ldlOr+uhpGhXzkqUjqf1Xa/JTwhNcpPALVLrHmmuymgOAt4tfiNhuypsZ7",
- "kEHGRer1AZ7yS3VaaA0SL2xI3sqyYp/gaUIGtAAZD2wDhJRZYNicTdh02Fet0k7aCbq6KhX94jTDe9NJ",
- "otJ/0vsI34gHVRY6QNVO+hLRQ72GsHLfM2UGzEBcaIHbvwjGul7UvYCTAu8s+DQHukdEZRsIM2BMq2dF",
- "jOfid6gGeyFvlQ3W0ZXJPwswxMRH0MbNle+OVkcrOwrlIHkuWMSOj94draj7cbyzboQWUrtQhc8PpKKk",
- "xxuwxUFstdPux4RFzG5QtMz8Rhue1aJ5Bgh04l1VQZDmXQgPlVO7BKAuIGgNzVMz6Tro7sfvV6shkjZy",
- "YX8FbWfBOtvG/6pPsXJNX3jQCd0qY6tZmSGUvlghwuktYbJqflXJdm9tyYoURc41hqTm54SjZ7OiIDuW",
- "boTkeutdEzx71MuTtLcTvipDTRcbJ+55Xakvdra3x9ugRxhAx/q56zDDiVnm3qE8NKDulcjrExA+FyKZ",
- "6B/nUMMyXRaO6m/cOxYDBuRGSJjAhLA4s4I/LiKdGyPHt05wYb32+8vhTCLoUzekvVGEh9XbrLuMeZck",
- "BxalD2l3+IyTCHD2UbMgg0j8ePVL73aDaUiErmefFgqySFMb03i7rlrEco2aTNpZcsTiFzdrHnCMta8L",
- "y4B9WB1Pf9S95W+5GKZqI+RwMf2hjHX1wootxfXhTSTnxnxTOpmuhHqObr5YrCr6CK9ejnCnha87eKsC",
- "ZwFOcj3/P/Tpb4o4BmP+V6k+1OOdjxo2wqDj77iXn2vJ78qM3cv33rfzlzXPntbYbVv5sbk2Oj+sqaUa",
- "0I910y50yiIW0sJWrst/AwAA//8rjKvDLRwAAA==",
+ "H4sIAAAAAAAC/9RZX2+cOBD/KiffPXJh+0c6ad9yURpFF1V3TXov0SpyYLLrBmximzR7Ed/9NDawsNjA",
+ "Epq2T93CMH9+85uxZ/JMIpFmggPXiiyfiQSVCa7A/AekFPJT+QQfRIJr4Bp/0ixLWEQ1Ezz8ogTHZyra",
+ "QErxVyZFBlIzqycSsflcbzMgS8K4hjVIUgQkBaXouvlSacn4mhRFQCQ85ExCTJbXVsVOfhVU8uL2C0Sa",
+ "FPhBDCqSLEOfyJKg/juWgLpgSk+KgmlITQC/SbgjS/JruAMrtGIqRBPnGlI0V/pEpaRbn0trmsJxnDI+",
+ "yaU+T1Dzacx0n+VviwVa+JfB17FYaKruZ4cBlVon3EbzLBE0noHWec5i/PdOyJRqsrQPggEmG6Gx/M0V",
+ "yNkBQqW9ACmQlxshDyHKkD2jrzTqs/mNC3XPjTEMLYLSSN3GTH0NNbgq83sKHe+nEMgY6xIoMC9MbHP5",
+ "VziM1O2uY2RULAERkq0Zp8lHmrp9Uuw/52mxB4PR3lJWfuqCpm6NHa8HMxTZF4NxjQw/E6w8arunIXYu",
+ "NZrPKG3b/T6VA6KZTtzo2gfDbfwK5YqAPDLFblu6boVIgPJOQirJynqbXaXlKsgaCF+2rko/geep1S51",
+ "ThMSkIjpbeOzXWj16dNJMs31Rkh1UKtwd4mARBKohvhYTyv4V6bTici59rz+DhyxQfTRowyo6X0T86DO",
+ "pYs4dUk4O+D4/Ned3pH/kYnR8ORmiA/2PqSMsqAMwhe4v/UfFriP+I27elWU4v6mvJgz/kgTFlf/FUn9",
+ "k8OTvkngEbB2kTU3aDcBDc4ifiluoyFrXwc6uMFTdiVOcimB6wvjvbOIjNhHeBqQAcmAR54hCEFRM9yx",
+ "S00fGGdqA3GPsUtNpfZJjCR44g8YoeXu493F8lq8BVVlYc/l/SB9qfVkNaUscdLrp8z3y1MlRdKqaISu",
+ "argCf1EcVZ2V+oIsmyx4st1NRAf1CsLSfccwFRAFUS6Z3l4ijFUrFPcMjnO9MeDjFd8+wi5lAiEKlGoc",
+ "R0tCM/YXlNMB43fCBGs7EeH/5KCwyTyCVHZkeHO0OFqYW24GnGaMLMm7ozdHCzzYqN4YN0IDqdkbhM8P",
+ "qKLAx2swfQ/Zasac85gsiVkU4Fj0gSUm4IxKmoIGvMxcl0Gg5l0ID6VTuwRomUPQmJaGxo1V0F4DvV0s",
+ "fCSt5cLupqWZBeNsE//rLsWKFX7hQCe0E7upZqF8KH02QojTa8Jk1Pwp4u3evJrmiWYZlTpENb/HVDsW",
+ "CBhky9It41RunROgY11weJL2Vh8vylDdxfqJe1ZV6sHOdtZVJugeBuCN7cx2GH9i5lmvFVMDam/+Xp6A",
+ "8Dln8UD/OIMKluGysFR/5d4xGzDA14zDACaIxakR/HERaS1GLd9awYXVRsddDqdcgzyx9+9XinBavY1a",
+ "U43bf00sShfS9vDpJxHo0UfNjAxC8XeLPzqLKyIhZrK6+zRQ4HmSmJj623XZIuZr1GjS3CV7LH62d80J",
+ "x1hzK14E5P3i3fBH7T9mNVwME7Fm3F9MfwtlXL0wYnNx3T+JZFSpr0LGw5VQ3aPrL2arii7Ci8MRbrXw",
+ "VQtvketRgKNcx//3XfqrPIpAqV9K1VM93vkoYc2Utvzt9/JTJfldmbF7+db5dvyw5pjTartNKz8F19RQ",
+ "/1GTG5DytTw1fHSg5XOcJn/YC0j3b4CFDdF/K1uhJQXysYollwlZkhDH4GJV/B8AAP//bSQu1mogAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/api/types.go b/api/types.go
index 8c88a5d..b4e6596 100644
--- a/api/types.go
+++ b/api/types.go
@@ -101,6 +101,19 @@ type TaskView struct {
// TaskViewMessage defines model for TaskView.Message.
type TaskViewMessage string
+// UserShortView defines model for userShortView.
+type UserShortView struct {
+ ExpToCurrentLevel *int `json:"expToCurrentLevel,omitempty"`
+ ExpToNextLevel *int `json:"expToNextLevel,omitempty"`
+ Experience int `json:"experience"`
+ Games *[]GameView `json:"games,omitempty"`
+ GamesFinished int `json:"gamesFinished"`
+ GamesStarted int `json:"gamesStarted"`
+ Id openapi_types.UUID `json:"id"`
+ Level int `json:"level"`
+ Username string `json:"username"`
+}
+
// UserView defines model for userView.
type UserView struct {
Email string `json:"email"`
@@ -143,6 +156,12 @@ type UploadResponse struct {
// UserResponse defines model for userResponse.
type UserResponse = UserView
+// UserShortResponse defines model for userShortResponse.
+type UserShortResponse interface{}
+
+// UsersListResponse defines model for usersListResponse.
+type UsersListResponse = []UserShortView
+
// AdminUploadFileMultipartBody defines parameters for AdminUploadFile.
type AdminUploadFileMultipartBody interface{}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e37044d..c442b38 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -16,6 +16,8 @@ import User from './pages/User'
import { useRole } from './utils/roles'
import EditQuest from './pages/admin/Quest'
import AdminQuest from './pages/admin/Quests'
+import Users from './pages/Users'
+import UserView from './pages/UserView'
const router = createBrowserRouter(
createRoutesFromElements(
@@ -35,6 +37,18 @@ const router = createBrowserRouter(
element={}
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
/>
+ }
+ loader={() => ajax('/api/users').catch(x => { console.log(x); return null })}
+ />
+ }
+ loader={({ params }) => ajax(`/api/users/${params.userId}`).catch(x => { console.log(x); return null })}
+ />
} />
} />
} />
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index dc490a1..fb2d5c8 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -35,6 +35,11 @@ const AppLayout = () => {
label: 'Квесты',
link: '/quests'
},
+ {
+ key: 'users',
+ label: 'Игроки',
+ link: '/users'
+ },
{
key: 'me',
label: `${user.username} [${user.level}]`,
diff --git a/frontend/src/pages/UserView.jsx b/frontend/src/pages/UserView.jsx
new file mode 100644
index 0000000..20e9fbf
--- /dev/null
+++ b/frontend/src/pages/UserView.jsx
@@ -0,0 +1,34 @@
+import { Avatar, Popover, Progress, Space, Typography } from 'antd'
+import Markdown from 'react-markdown'
+import { useLoaderData } from 'react-router-dom'
+
+const { Paragraph } = Typography
+
+const UserView = () => {
+ const user = useLoaderData()
+ if (user == null) {
+ return (Загрузка...)
+ }
+
+ return (<>
+ {user.username}
+ Уровень: {user.level} ур
+ Очков опыта: {user.experience} ОО
+ Следующий уровень: {user.expToNextLevel} ОО
+
+ {user.games.map(item => {item.description}}
+ >
+
+ )}
+ >)
+}
+
+export default UserView
diff --git a/frontend/src/pages/Users.jsx b/frontend/src/pages/Users.jsx
new file mode 100644
index 0000000..4da18e8
--- /dev/null
+++ b/frontend/src/pages/Users.jsx
@@ -0,0 +1,44 @@
+import { Table, Typography } from 'antd'
+import { Link, useLoaderData } from 'react-router-dom'
+
+const { Title } = Typography
+
+const Users = () => {
+ const users = useLoaderData()
+
+ return (
+ <>
+ Статистика игроков
+ {username}
+ },
+ {
+ title: 'ОО',
+ dataIndex: 'experience',
+ key: 'experience'
+ },
+ {
+ title: 'Уровень',
+ dataIndex: 'level',
+ key: 'level'
+ },
+ {
+ title: 'Квесты начатые / Пройденные',
+ dataIndex: 'gamesStarted',
+ key: 'gamesStarted',
+ render: (gamesStarted, q) => (<>{gamesStarted} / {q.gamesFinished}>)
+ }
+ ]}
+
+ />
+ >
+ )
+}
+export default Users
diff --git a/frontend/src/pages/admin/Quest.jsx b/frontend/src/pages/admin/Quest.jsx
index aecadeb..b06c5bc 100644
--- a/frontend/src/pages/admin/Quest.jsx
+++ b/frontend/src/pages/admin/Quest.jsx
@@ -11,6 +11,7 @@ const { Title } = Typography
const Quest = () => {
let { quest, files } = useLoaderData()
const [error, setError] = useState()
+ const [info, setInfo] = useState()
if (!quest) {
quest = {
type: 'city',
@@ -45,6 +46,21 @@ const Quest = () => {
}
const onFinish = (values) => {
+ values.tasks = values.tasks.map(task => {
+ if (!task.id) {
+ task.id = uuidv4()
+ }
+ task.codes = task.codes.map(code => {
+ if (!code.id) {
+ code.id = uuidv4()
+ }
+
+ return code
+ })
+
+ return task
+ })
+ console.log(values)
ajax('/api/admin/games', {
method: 'POST',
headers: {
@@ -53,14 +69,24 @@ const Quest = () => {
},
body: JSON.stringify(values)
})
- .then(g => navigate(`/admin/quests/${g.id}/`))
- .catch(({ message }) => setError('Ошибка создания'))
+ .then(g => {
+ navigate(`/admin/quests/${g.id}/`)
+ setError('')
+ setInfo('Сохранено')
+
+ return true
+ })
+ .catch(({ message }) => {
+ setInfo('')
+ setError('Ошибка создания:' + message)
+ })
}
return (
<>
{quest.title ? (quest.title) : ('Новый квест')}
{error ? : null}
+ {info ? : null}