diff --git a/api/openapi.yaml b/api/openapi.yaml index d8f4b39..e916808 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -38,7 +38,6 @@ paths: $ref: '#/components/responses/errorResponse' /user/logout: post: - security: [] responses: 204: description: "success logout" @@ -48,7 +47,6 @@ paths: # Game routes /games: get: - security: [] responses: 200: $ref: '#/components/responses/gameListResponse' @@ -91,8 +89,9 @@ paths: $ref: '#/components/responses/taskResponse' /file/upload: post: - security: [] operationId: uploadFile + security: + - cookieAuth: [creator, admin] requestBody: content: multipart/form-data: @@ -135,7 +134,36 @@ components: format: uuid username: type: string - required: [ id, username ] + email: + type: string + experience: + type: integer + level: + type: integer + expToCurrentLevel: + type: integer + expToNextLevel: + type: integer + games: + type: array + items: + $ref: "#/components/schemas/gameView" + role: + type: string + enum: + - user + - creator + - admin + required: + - id + - username + - email + - experience + - level + - expToCurrentLevel + - expToNextLevel + - games + - role gameView: type: object properties: @@ -339,36 +367,7 @@ components: content: 'application/json': schema: - type: object - properties: - id: - type: string - format: uuid - username: - type: string - email: - type: string - experience: - type: integer - level: - type: integer - expToCurrentLevel: - type: integer - expToNextLevel: - type: integer - games: - type: array - items: - $ref: "#/components/schemas/gameView" - required: - - id - - username - - email - - experience - - level - - expToCurrentLevel - - expToNextLevel - - games + $ref: "#/components/schemas/userView" errorResponse: description: '' content: diff --git a/api/server.go b/api/server.go index 360d913..34d1157 100644 --- a/api/server.go +++ b/api/server.go @@ -98,6 +98,8 @@ func (w *ServerInterfaceWrapper) EnterCode(ctx echo.Context) error { func (w *ServerInterfaceWrapper) UploadFile(ctx echo.Context) error { var err error + ctx.Set(CookieAuthScopes, []string{"creator", "admin"}) + // Invoke the callback with all the unmarshaled arguments err = w.Handler.UploadFile(ctx) return err @@ -125,6 +127,8 @@ func (w *ServerInterfaceWrapper) GetFile(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { var err error + ctx.Set(CookieAuthScopes, []string{}) + // Invoke the callback with all the unmarshaled arguments err = w.Handler.GetGames(ctx) return err @@ -165,6 +169,8 @@ func (w *ServerInterfaceWrapper) PostUserLogin(ctx echo.Context) error { func (w *ServerInterfaceWrapper) PostUserLogout(ctx echo.Context) error { var err error + ctx.Set(CookieAuthScopes, []string{}) + // Invoke the callback with all the unmarshaled arguments err = w.Handler.PostUserLogout(ctx) return err @@ -223,26 +229,26 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/types.go b/api/types.go index 6aa564e..609673b 100644 --- a/api/types.go +++ b/api/types.go @@ -26,6 +26,13 @@ const ( OldCode TaskViewMessage = "old_code" ) +// Defines values for UserViewRole. +const ( + Admin UserViewRole = "admin" + Creator UserViewRole = "creator" + User UserViewRole = "user" +) + // CodeEdit defines model for codeEdit. type CodeEdit struct { Code string `json:"code"` @@ -98,10 +105,20 @@ type TaskViewMessage string // UserView defines model for userView. type UserView struct { - Id openapi_types.UUID `json:"id"` - Username string `json:"username"` + Email string `json:"email"` + ExpToCurrentLevel int `json:"expToCurrentLevel"` + ExpToNextLevel int `json:"expToNextLevel"` + Experience int `json:"experience"` + Games []GameView `json:"games"` + Id openapi_types.UUID `json:"id"` + Level int `json:"level"` + Role UserViewRole `json:"role"` + Username string `json:"username"` } +// UserViewRole defines model for UserView.Role. +type UserViewRole string + // ErrorResponse defines model for errorResponse. type ErrorResponse struct { Code int `json:"code"` @@ -123,16 +140,7 @@ type UploadResponse struct { } // UserResponse defines model for userResponse. -type UserResponse struct { - Email string `json:"email"` - ExpToCurrentLevel int `json:"expToCurrentLevel"` - ExpToNextLevel int `json:"expToNextLevel"` - Experience int `json:"experience"` - Games []GameView `json:"games"` - Id openapi_types.UUID `json:"id"` - Level int `json:"level"` - Username string `json:"username"` -} +type UserResponse = UserView // EnterCodeRequest defines model for enterCodeRequest. type EnterCodeRequest struct { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 230cdba..42dc117 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import NoMatch from './pages/NoMatch' import Engine from './pages/Engine' import Quests from './pages/Quests' import User from './pages/User' +import { useRole } from './utils/roles' const router = createBrowserRouter( createRoutesFromElements( @@ -40,19 +41,17 @@ const router = createBrowserRouter( element={} loader={({ params }) => ajax(`/api/engine/${params.gameId}`).catch(x => { console.log(x); return null })} /> - {/* } - loader={() => ajax(`/api/admin/games`)} + element={} + // loader={() => ajax(`/api/admin/games`)} /> - } - loader={() => ({ - title: "Новая игра", - tasks: [] - })} - /> */} + } + // loader={() => ajax(`/api/admin/games`)} + /> + } /> ) @@ -66,16 +65,22 @@ function App () { function Auth (props) { const baseUser = useRouteLoaderData('root') const { user } = UserProvider.useContainer() + const { hasRole } = useRole() const location = useLocation() if (!user && !baseUser) { return } + if (props.role && !hasRole(props.role)) { + return + } + return props.children } Auth.propTypes = { - children: PropTypes.any + children: PropTypes.any, + role: PropTypes.string } export default App diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 8d88c8a..9279613 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -5,6 +5,7 @@ import { ajax } from '../utils/fetch' import { Layout, Menu } from 'antd' import { MenuOutlined } from '@ant-design/icons' import { Content, Header } from 'antd/es/layout/layout' +import { useRole } from '../utils/roles' const AppLayout = () => { const params = useLoaderData() @@ -14,6 +15,7 @@ const AppLayout = () => { setUser(params) }, [params]) const { user, setUser } = UserProvider.useContainer() + const { hasRole } = useRole() const logout = () => { ajax('/api/user/logout', { @@ -33,6 +35,20 @@ const AppLayout = () => { label: 'Квесты', link: '/quests' }, + hasRole('creator') + ? { + key: 'quest/new', + label: 'Создать квест', + link: '/quest/new' + } + : null, + hasRole('admin') + ? { + key: 'admin', + label: 'Админка', + link: '/admin' + } + : null, { key: 'me', label: `${user.username} [${user.level}]`, diff --git a/main.go b/main.go index cd8552e..db83bf2 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,23 @@ func main() { echoCtx := ctx.Value(oapiMiddleware.EchoContextKey).(echo.Context) user := appmiddleware.GetUser(echoCtx) if user != nil { + if len(ai.Scopes) > 0 { + for _, v := range ai.Scopes { + switch v { + case "user": + return nil + case "creator": + if user.HasRole(models.RoleCreator) { + return nil + } + case "admin": + if user.HasRole(models.RoleAdmin) { + return nil + } + } + } + return echo.ErrForbidden + } return nil } diff --git a/pkg/controller/user.go b/pkg/controller/user.go index 67a00fc..2123096 100644 --- a/pkg/controller/user.go +++ b/pkg/controller/user.go @@ -138,7 +138,15 @@ func mapUser(c echo.Context, user *models.User) error { } level := utils.ExpToLevel(user.Experience) - + role := api.User + switch user.Role { + case models.RoleUser: + role = api.User + case models.RoleCreator: + role = api.Creator + case models.RoleAdmin: + role = api.Admin + } return c.JSON(http.StatusOK, &api.UserResponse{ Id: user.ID, Username: user.Username, @@ -148,5 +156,6 @@ func mapUser(c echo.Context, user *models.User) error { ExpToNextLevel: utils.LevelToExp(level + 1), Level: int(level), Games: games, + Role: role, }) } diff --git a/pkg/models/user.go b/pkg/models/user.go index 5d480d4..2d4d28c 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -13,4 +13,17 @@ type User struct { Password string `json:"-"` Experience int Games []*GameCursor + Role UserRole } + +func (u *User) HasRole(role UserRole) bool { + return u.Role >= role +} + +type UserRole int + +const ( + RoleUser UserRole = iota + RoleCreator + RoleAdmin +)