Full authentication in front

This commit is contained in:
2025-04-07 02:33:27 +02:00
parent cd248c4aa9
commit 738a9bebf0
52 changed files with 7399 additions and 2199 deletions

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

30
api/rpk-api/cli.py Normal file
View File

@@ -0,0 +1,30 @@
import asyncio
import contextlib
from fastapi_users.exceptions import UserAlreadyExists
from fastapi_users.schemas import BaseUserCreate
from hub import init_db as hub_init_db
from hub.auth import get_user_manager
from hub.user import get_user_db
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, is_superuser: bool = False):
try:
await hub_init_db()
async with get_user_db_context() as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await user_manager.create(
BaseUserCreate(
email=email, password=password, is_superuser=is_superuser
)
)
print(f"User created {user}")
except UserAlreadyExists:
print(f"User {email} already exists")
if __name__ == "__main__":
asyncio.run(create_user("test@test.te", "test"))

View File

@@ -1,16 +1,19 @@
import os
import uuid
from typing import Any, Optional
from beanie import PydanticObjectId
from fastapi import Depends
from fastapi_users import UUIDIDMixin, BaseUserManager, FastAPIUsers, schemas
from fastapi_users.authentication import AuthenticationBackend, BearerTransport, CookieTransport
from fastapi import Depends, Response, status
from fastapi_users import BaseUserManager, FastAPIUsers, schemas, models
from fastapi_users.authentication import AuthenticationBackend, BearerTransport, CookieTransport, Strategy
from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy
from fastapi_users_db_beanie.access_token import BeanieBaseAccessTokenDocument, BeanieAccessTokenDatabase
from fastapi_users.openapi import OpenAPIResponseType
from httpx_oauth.clients.google import GoogleOAuth2
from httpx_oauth.clients.discord import DiscordOAuth2
from starlette.responses import JSONResponse, RedirectResponse
from hub.user import User, get_user_db
from hub.user.schemas import UserSchema
SECRET = os.getenv("FASTAPI_USERS_SECRET")
@@ -19,9 +22,6 @@ discord_oauth_client = DiscordOAuth2(os.getenv("DISCORD_CLIENT_ID"), os.getenv("
TOKEN_LIFETIME = 3600
# bearer_transport = BearerTransport(tokenUrl="auth/login")
cookie_transport = CookieTransport(cookie_name="rpkapiusersauth")
class AccessToken(BeanieBaseAccessTokenDocument):
pass
@@ -36,24 +36,57 @@ def get_database_strategy(
return DatabaseStrategy(access_token_db, lifetime_seconds=TOKEN_LIFETIME)
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
class UserManager(BaseUserManager[User, PydanticObjectId]):
reset_password_token_secret = SECRET
verification_token_secret = SECRET
def parse_id(self, value: Any) -> models.ID:
return str(value)
async def get_user_manager(user_db=Depends(get_user_db)):
yield UserManager(user_db)
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy,
)
class CookieTransportMe(CookieTransport):
async def get_login_me_response(self, token: str, user) -> Response:
user_schema = UserSchema(**user.model_dump())
response = JSONResponse(status_code=status.HTTP_200_OK, content=user_schema.model_dump(mode="json"))
return self._set_login_cookie(response, token)
@staticmethod
def get_openapi_login_responses_success() -> OpenAPIResponseType:
return {status.HTTP_200_OK: {"model": UserSchema}}
class AuthenticationBackendMe(AuthenticationBackend):
async def login(self, strategy: Strategy[models.UP, models.ID], user: models.UP) -> Response:
token = await strategy.write_token(user)
return await self.transport.get_login_me_response(token, user)
class CookieTransportOauth(CookieTransport):
async def get_login_response(self, token: str) -> Response:
response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
return self._set_login_cookie(response, token)
@staticmethod
def get_openapi_login_responses_success() -> OpenAPIResponseType:
return {status.HTTP_301_MOVED_PERMANENTLY: {"model": None}}
cookie_transport = CookieTransportMe(cookie_name="rpkapiusersauth")
auth_backend = AuthenticationBackendMe(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend])
auth_router = fastapi_users.get_auth_router(auth_backend, requires_verification=True)
register_router = fastapi_users.get_register_router(schemas.BaseUser, schemas.BaseUserCreate)
register_router = fastapi_users.get_register_router(UserSchema, schemas.BaseUserCreate)
password_router = fastapi_users.get_reset_password_router()
verification_router = fastapi_users.get_verify_router(schemas.BaseUser)
users_router = fastapi_users.get_users_router(schemas.BaseUser, schemas.BaseUserUpdate)
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET)
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET)
verification_router = fastapi_users.get_verify_router(UserSchema)
users_router = fastapi_users.get_users_router(UserSchema, schemas.BaseUserUpdate)
cookie_transport = CookieTransportOauth(cookie_name="rpkapiusersauth")
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET, is_verified_by_default=True)

View File

@@ -1,4 +1,3 @@
from fastapi import Depends
from fastapi_users_db_beanie import BaseOAuthAccount, BeanieUserDatabase, BeanieBaseUserDocument
from pydantic import Field
@@ -14,6 +13,3 @@ class UserDatabase(BeanieUserDatabase):
async def get_user_db():
yield UserDatabase(User, OAuthAccount)
async def get_user_manager(user_db=Depends(get_user_db)):
yield UserManager(user_db)

View File

@@ -0,0 +1,5 @@
from beanie import PydanticObjectId
from fastapi_users.schemas import BaseUser
class UserSchema(BaseUser[PydanticObjectId]):
pass

View File

@@ -12,9 +12,9 @@ services:
- ./api/media:/app/media
labels:
- "traefik.enable=true"
- "traefik.http.routers.back.entrypoints=web"
- "traefik.http.routers.back.rule=PathPrefix(`/api/v1/`)"
- "traefik.http.services.back.loadbalancer.server.port=8000"
- "traefik.http.routers.api.entrypoints=web"
- "traefik.http.routers.api.rule=PathPrefix(`/api/v1/`)"
- "traefik.http.services.api.loadbalancer.server.port=8000"
gui:
build:
@@ -22,15 +22,15 @@ services:
image: roleplay-contracts-gui-dev
restart: always
ports:
- "4200:4200"
- "5173:5173"
volumes:
- ./gui/rpk-gui/src:/app/src
- ./gui/rpk-gui/public:/app/public
labels:
- "traefik.enable=true"
- "traefik.http.routers.front.entrypoints=web"
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
- "traefik.http.services.front.loadbalancer.server.port=4200"
- "traefik.http.routers.gui.entrypoints=web"
- "traefik.http.routers.gui.rule=PathPrefix(`/`)"
- "traefik.http.services.gui.loadbalancer.server.port=5173"
proxy:
image: traefik:latest

View File

@@ -1,13 +1,13 @@
FROM node:lts-alpine
WORKDIR /app
RUN npm install -g @angular/cli http-server
RUN npm install -g http-server
COPY rpk-gui/package*.json ./
RUN npm install
COPY rpk-gui/ .
RUN npm run build
EXPOSE 4200
EXPOSE 5173
CMD [ "npm", "run", "ng", "serve", "--", "--host", "0.0.0.0", "--disable-host-check" ]
CMD [ "npm", "run", "dev", "--", "--host"]

16
gui/rpk-gui/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,16 @@
/* eslint-env node */
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
},
};

View File

@@ -1,7 +1,8 @@
# Logs
logs
*.log
npm-debug.log*
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
@@ -10,15 +11,16 @@ lerna-debug.log*
node_modules
dist
dist-ssr
*.local
\*.local
# Editor directories and files
.vscode/*
.vscode/_
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
_.suo
_.ntvs_
_.njsproj
_.sln
\*.sw?

2
gui/rpk-gui/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
legacy-peer-deps=true
strict-peer-dependencies=false

37
gui/rpk-gui/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# This Dockerfile uses `serve` npm package to serve the static files with node process.
# You can find the Dockerfile for nginx in the following link:
# https://github.com/refinedev/dockerfiles/blob/main/vite/Dockerfile.nginx
FROM refinedev/node:18 AS base
FROM base as deps
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base as builder
ENV NODE_ENV production
COPY --from=deps /app/refine/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base as runner
ENV NODE_ENV production
RUN npm install -g serve
COPY --from=builder /app/refine/dist ./
USER refine
CMD ["serve"]

49
gui/rpk-gui/README.MD Normal file
View File

@@ -0,0 +1,49 @@
# rpk-gui
<div align="center" style="margin: 30px;">
<a href="https://refine.dev">
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
</a>
</div>
<br/>
This [Refine](https://github.com/refinedev/refine) project was generated with [create refine-app](https://github.com/refinedev/refine/tree/master/packages/create-refine-app).
## Getting Started
A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility ✨
Refine's hooks and components simplifies the development process and eliminates the repetitive tasks by providing industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, state management, and i18n.
## Available Scripts
### Running the development server.
```bash
npm run dev
```
### Building for production.
```bash
npm run build
```
### Running the production server.
```bash
npm run start
```
## Learn More
To learn more about **Refine**, please check out the [Documentation](https://refine.dev/docs)
- **REST Data Provider** [Docs](https://refine.dev/docs/core/providers/data-provider/#overview)
- **Material UI** [Docs](https://refine.dev/docs/ui-frameworks/mui/tutorial/)
- **React Router** [Docs](https://refine.dev/docs/core/providers/router-provider/)
- **Custom Auth Provider** [Docs](https://refine.dev/docs/core/providers/auth-provider/)
## License
MIT

View File

@@ -1,54 +0,0 @@
# React + TypeScript + 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
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -1,13 +1,41 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="refine | Build your React-based CRUD applications, without constraints."
/>
<meta
data-rh="true"
property="og:image"
content="https://refine.dev/img/refine_social.png"
/>
<meta
data-rh="true"
name="twitter:image"
content="https://refine.dev/img/refine_social.png"
/>
<title>
Refine - Build your React-based CRUD applications, without constraints.
</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm dev` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,61 @@
{
"name": "rpk-gui",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@rjsf/mui": "^5.24.8",
"@tanstack/react-query": "^5.71.3",
"better-auth": "^1.2.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.1"
"@refinedev/cli": "^2.16.21",
"@refinedev/core": "^4.47.1",
"@refinedev/devtools": "^1.1.32",
"@refinedev/kbar": "^1.3.6",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router": "^7.0.2",
"@refinedev/simple-rest": "^5.0.1",
"@refinedev/mui": "^6.0.0",
"@refinedev/react-hook-form": "^4.8.14",
"@mui/icons-material": "^6.1.6",
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@mui/lab": "^6.0.0-beta.14",
"@mui/material": "^6.1.7",
"@mui/x-data-grid": "^7.22.2",
"react-hook-form": "^7.30.0",
"@refinedev/react-router": "^1.0.0",
"@rjsf/core": "^5.24.1",
"@rjsf/mui": "^5.24.1",
"@rjsf/utils": "^5.24.1",
"@rjsf/validator-ajv8": "^5.24.1"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
"@types/node": "^18.16.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.4.2",
"vite": "^4.3.1"
},
"scripts": {
"dev": "refine dev --devtools=false",
"build": "tsc && refine build",
"start": "refine start",
"refine": "refine"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,61 +1,75 @@
//https://www.robinwieruch.de/react-router-private-routes/
import {Authenticated, GitHubBanner, Refine, WelcomePage} from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import {
RefineSnackbarProvider, RefineThemes,
useNotificationProvider,
} from "@refinedev/mui";
import {createBrowserRouter, Link, Route, RouterProvider, Routes} from "react-router";
import { EntityList } from "./pages/entities/List.tsx";
import {ProtectedRoute} from "./pages/auth/ProtectedRoute.tsx";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import routerBindings, {
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import dataProvider from "@refinedev/simple-rest";
import {BrowserRouter, Outlet, Route, Routes, Catch} from "react-router";
import { authProvider } from "./providers/auth-provider";
import { ColorModeContextProvider } from "./contexts/color-mode";
import {Login} from "./pages/login";
import {Register} from "./pages/register";
import {ForgotPassword} from "./pages/forgotPassword";
import {Header} from "./components";
import {ThemeProvider} from "@mui/material/styles";
import {UpdatePassword} from "./pages/updatePassword";
function App() {
const [user, setUser] = useState<AuthUser | null>(null)
const [count, setCount] = useState(0)
const router = createBrowserRouter([
{
path: "/",
element: <div><Link to={"/entities/list"}>To the list</Link></div>,
},
{
path: "/entities/list",
element: <EntityList />,
},
]);
return (
<>
<RouterProvider router={router}>
</RouterProvider>
<Routes>
<Route index element={ <h1>INDEX</h1> } />
<Route element={ <ProtectedRoute user={user} /> }>
<Route path="toto" element={ <h1>PROTECTED ROUTE</h1>} />
</Route>
</Routes>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
<BrowserRouter>
<ThemeProvider theme={RefineThemes.Blue}>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true
}}
>
<Header />
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}>
<Outlet />
</Authenticated>
)}
>
<Route path="/toto" element={<h1>Where are in</h1>} />
</Route>
<Route index element={<h1>HOME</h1>} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/update-password" element={<UpdatePassword />} />
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</ThemeProvider>
</BrowserRouter>
);
}
export default App
export default App;

View File

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

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,65 +0,0 @@
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import {RegistryFieldsType, RegistryWidgetsType} from "@rjsf/utils";
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../../providers/jsonschema-provider";
import { useForm } from "@refinedev/core";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
type Props = {
schemaName: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
}
const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const CrudForm: React.FC<Props> = ({schemaName, resource, id}) => {
const { onFinish, query, formLoading } = useForm({
resource: resource,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
});
const record = query?.data?.data;
const [formData, setFormData] = useState(record);
const [schema, setSchema] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaName);
setSchema(resourceSchema);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setLoading(false);
}
};
fetchSchema();
}, []);
return (
<Form
schema={schema}
formData={record}
onChange={(e) => setFormData(e.formData)}
onSubmit={(e) => onFinish(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
/>
)
}

View File

@@ -1,93 +0,0 @@
import { ERRORS_KEY, FieldProps, getUiOptions } from "@rjsf/utils";
import AnyOfField from "@rjsf/core/lib/components/fields/MultiSchemaField";
import { UnionEnumWidget } from "../widgets/union-enum";
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import omit from 'lodash/omit';
const UnionEnumField = (props: FieldProps) => {
const {
name,
disabled = false,
errorSchema = {},
formContext,
formData,
onChange,
onBlur,
onFocus,
registry,
schema,
uiSchema,
options,
idSchema,
} = props;
const enumOptions: any[] = []
if (options.length == 2 && (options[0].type == "null" || options[1].type == "null")) {
const { SchemaField: _SchemaField } = registry.fields;
let opt_schema = {...schema}
delete(opt_schema.anyOf)
if (options[0].type == "null") {
opt_schema = {...opt_schema, ...options[1]}
} else if (options[1].type == "null") {
opt_schema = {...opt_schema, ...options[0]}
}
return <_SchemaField {...props} schema={opt_schema} uiSchema={uiSchema} />
}
for (let opt of options) {
if (!opt.hasOwnProperty('enum')) {
return (<AnyOfField {...props} />)
}
for (let val of opt.enum) {
enumOptions.push({
title: val,
value: val,
type: opt.title
})
}
}
const { globalUiOptions, schemaUtils } = registry;
const {
placeholder,
autofocus,
autocomplete,
title = schema.title,
...uiOptions
} = getUiOptions(uiSchema, globalUiOptions);
const rawErrors = get(errorSchema, ERRORS_KEY, []);
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
return (
<UnionEnumWidget
id={`${idSchema.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
name={`${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
schema={schema}
uiSchema={uiSchema}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
disabled={disabled || isEmpty(options)}
multiple={false}
rawErrors={rawErrors}
errorSchema={fieldErrorSchema}
value={formData}
options={{enumOptions}}
registry={registry}
formContext={formContext}
placeholder={placeholder}
autocomplete={autocomplete}
autofocus={autofocus}
label={title ?? name}
hideLabel={!displayLabel}
/>
);
}
export default UnionEnumField;

View File

@@ -1,34 +0,0 @@
import { WidgetProps } from "@rjsf/utils";
import { NumericFormat } from "react-number-format";
import TextField from "@mui/material/TextField";
import {InputAdornment} from "@mui/material";
export const BgfcPriceWidget = (props: WidgetProps) => {
const { inputRef, onChange, ...other } = props;
return (
<NumericFormat
{...props}
customInput={TextField}
getInputRef={inputRef}
onValueChange={values => {
onChange({
target: {
name: props.name,
value: values.value
}
});
}}
slotProps={{
input: {
//startAdornment: <InputAdornment position="end">$</InputAdornment>,
endAdornment: <InputAdornment position="start"></InputAdornment>
},
}}
valueIsNumericString={true}
fixedDecimalScale={true}
decimalScale={2}
//defaultValue={0}
/>
);
}

View File

@@ -1,17 +0,0 @@
import TextWidget from "@rjsf/core/lib/components/widgets/TextWidget";
import { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from "@rjsf/utils";
import { ForeignKeyWidget } from "./foreign-key";
import {BgfcPriceWidget} from "./bgfc-price";
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
if (props.schema.hasOwnProperty("foreign_key")) {
return (<ForeignKeyWidget {...props} />);
} else if (props.schema.hasOwnProperty("format") && props.schema.format == "price-bgdc") {
return (<BgfcPriceWidget {...props} />);
} else {
return (<TextWidget {...props} />);
}
}

View File

@@ -1,65 +0,0 @@
import { WidgetProps } from '@rjsf/utils';
import { Autocomplete } from "@mui/material";
import { useState, useEffect } from "react";
import TextField from "@mui/material/TextField";
import {BaseRecord, useList, useOne} from "@refinedev/core";
export const ForeignKeyWidget = (props: WidgetProps) => {
const resource = props.schema.foreign_key.reference.resource
const labelField = props.schema.foreign_key.reference.label
const valueResult = useOne({
resource: resource,
id: props.value != null ? props.value : undefined
});
const empty_option: BaseRecord = {
id: null
}
empty_option[labelField] = "(None)"
const [inputValue, setInputValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
useEffect(() => {
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
return () => clearTimeout(handler);
}, [inputValue]);
const listResult = useList({
resource: resource,
pagination: { current: 1, pageSize: 10 },
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "name", order: "asc" }],
});
const options = listResult.data?.data || [];
if (! props.required) {
options.unshift(empty_option);
}
const isLoading = listResult.isLoading || valueResult.isLoading;
if(! selectedValue && valueResult.data) {
setSelectedValue(valueResult.data?.data)
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue ? newValue : empty_option);
props.onChange(newValue ? newValue.id : null);
return true;
}}
//inputValue={inputValue}
onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
options={options}
getOptionLabel={(option) => option ? option[labelField] : ""}
loading={isLoading}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
};

View File

@@ -1,40 +0,0 @@
import {FieldProps, WidgetProps} from "@rjsf/utils";
import AnyOfField from "@rjsf/core/lib/components/fields/MultiSchemaField";
import {ListSubheader, MenuItem, Select} from "@mui/material";
import {useState} from "react";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
export const UnionEnumWidget = (props: WidgetProps) => {
const {
options,
value,
} = props;
const [selectedValue, setSelectedValue] = useState(null);
if (! selectedValue && value && options.enumOptions) {
for (const opt of options.enumOptions){
if (opt.value == value) {
setSelectedValue(opt);
break;
}
}
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue);
props.onChange(newValue.value);
}}
options={options.enumOptions}
groupBy={(option) => option.type}
getOptionLabel={(option) => option.title}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
const DiscordIcon: React.FC<SvgIconProps> = (props) => (
<SvgIcon {...props} viewBox="0 0 24 24">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</SvgIcon>
);
export default DiscordIcon;

View File

@@ -0,0 +1,11 @@
import {Navigate, useSearchParams} from "react-router";
import {AuthPage} from "@refinedev/mui";
import GoogleIcon from "@mui/icons-material/Google";
import DiscordIcon from "../DiscordIcon";
import { useLogout } from "@refinedev/core";
export const Logout = () => {
const { mutate: logout } = useLogout();
return <button onClick={() => logout()}>Logout</button>;
};

View File

@@ -0,0 +1,82 @@
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
import AppBar from "@mui/material/AppBar";
import Avatar from "@mui/material/Avatar";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import { useGetIdentity } from "@refinedev/core";
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
import {Logout} from "../auth/Logout";
type IUser = {
id: number;
email: string;
avatar: string;
};
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const { mode, setMode } = useContext(ColorModeContext);
const { data: user } = useGetIdentity<IUser>();
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
alignItems="center"
>
<HamburgerMenu />
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
alignItems="center"
>
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
{(user?.avatar || user?.email) && (
<Stack
direction="row"
gap="16px"
alignItems="center"
justifyContent="center"
>
{user?.email && (
<Typography
sx={{
display: {
xs: "none",
sm: "inline-block",
},
}}
variant="subtitle2"
>
{user?.email}
</Typography>
)}
<Avatar src={user?.avatar} alt={user?.email} />
<Logout />
</Stack>
)}
</Stack>
</Stack>
</Toolbar>
</AppBar>
);
};

View File

@@ -0,0 +1 @@
export { Header } from "./header";

View File

@@ -0,0 +1,59 @@
import { ThemeProvider } from "@mui/material/styles";
import { RefineThemes } from "@refinedev/mui";
import React, {
PropsWithChildren,
createContext,
useEffect,
useState,
} from "react";
type ColorModeContextType = {
mode: string;
setMode: () => void;
};
export const ColorModeContext = createContext<ColorModeContextType>(
{} as ColorModeContextType
);
export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const colorModeFromLocalStorage = localStorage.getItem("colorMode");
const isSystemPreferenceDark = window?.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
const systemPreference = isSystemPreferenceDark ? "dark" : "light";
const [mode, setMode] = useState(
colorModeFromLocalStorage || systemPreference
);
useEffect(() => {
window.localStorage.setItem("colorMode", mode);
}, [mode]);
const setColorMode = () => {
if (mode === "light") {
setMode("dark");
} else {
setMode("light");
}
};
return (
<ColorModeContext.Provider
value={{
setMode: setColorMode,
mode,
}}
>
<ThemeProvider
// you can change the theme colors here. example: mode === "light" ? RefineThemes.Magenta : RefineThemes.MagentaDark
theme={mode === "light" ? RefineThemes.Blue : RefineThemes.BlueDark}
>
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
};

View File

@@ -1,68 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

13
gui/rpk-gui/src/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -1,10 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,14 +0,0 @@
export const Login = () => {
async function handleLogin(e: any) {
e.preventDefault()
}
return (
<form onSubmit={ handleLogin }>
<input type={ "text" } name={ "email" } />
<input type={ "password" } name={ "password" } />
<input type={ "submit" } />
</form>
)
}

View File

@@ -1,16 +0,0 @@
import {Navigate, Outlet} from "react-router";
type AuthUser = { id: string; name: string };
type ProtectedRouteProps = {
user: AuthUser | null;
redirectPath?: string;
};
export const ProtectedRoute = ({ user, redirectPath = "/" }: ProtectedRouteProps) => {
if (!user) {
return <Navigate to={redirectPath} replace />;
}
return <Outlet />;
};

View File

@@ -1,15 +0,0 @@
export const Register = () => {
async function handleRegister(e: any) {
e.preventDefault()
}
return (
<form onSubmit={ handleRegister }>
<input type={ "text" } name={ "email" } />
<input type={ "password" } name={ "password" }/>
<input type={ "password" } name={ "confirm_password" }/>
<input type={ "submit" } />
</form>
)
}

View File

@@ -1,7 +0,0 @@
export const EntityList = () => {
return (
<h1>List des entity: Yoyoyo</h1>
)
}

View File

@@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const ForgotPassword = () => {
return <AuthPage type="forgotPassword" />;
};

View File

@@ -0,0 +1,32 @@
import { AuthPage } from "@refinedev/mui";
import GoogleIcon from "@mui/icons-material/Google";
import DiscordIcon from "../../components/DiscordIcon";
import { useSearchParams, Navigate } from "react-router";
export const Login = () => {
const [searchParams] = useSearchParams();
if (searchParams.get("oauth") == "success") {
const redirect_to = localStorage.getItem("redirect_after_login")
if (redirect_to) {
return (<Navigate replace to={ redirect_to } />);
}
}
return (
<AuthPage
type="login"
formProps={{ defaultValues: { email: "test@test.te", password: "test" }, }}
providers={[{
name: "google",
label: "Sign in with Google",
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
},
{
name: "discord",
label: "Sign in with Discord",
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
},
]}
/>
);
};

View File

@@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const Register = () => {
return <AuthPage type="register" />;
};

View File

@@ -0,0 +1,9 @@
import { AuthPage } from "@refinedev/mui";
import type {RegisterFormTypes} from "@refinedev/core";
import { useParams } from "react-router"
export const UpdatePassword = () => {
return <AuthPage type="updatePassword"/>;
};

View File

@@ -0,0 +1,192 @@
import { AuthProvider } from "@refinedev/core";
const API_URL = "/api/v1";
const LOCAL_STORAGE_USER_KEY = "rpk-gui-current-user"
export const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to");
if (providerName) {
let params, url = "";
if (providerName === "google") {
params = new URLSearchParams(
{"scopes":
"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
}
);
url = API_URL + "/auth/google/authorize?" + params.toString();
} else if (providerName === "discord") {
params = new URLSearchParams({"scopes": "identify email"});
url = API_URL + "/auth/discord/authorize?" + params.toString();
}
const response = await fetch(url, {method: "GET", },);
const body = await response.json();
if (to_param) {
localStorage.setItem("redirect_after_login", to_param);
}
console.log(body.authorization_url);
window.location.href = body.authorization_url;
return { success: true }
}
else if (email !== undefined && password !== undefined) {
const params = new URLSearchParams({"grant_type": "password", "username": email, "password": password});
const response = await fetch(
API_URL + "/auth/login",
{
method: "POST",
body: params.toString(),
headers: { "Content-Type": "application/x-www-form-urlencoded", },
},
);
if (response.status >= 200 && response.status < 300) {
const response = await fetch(API_URL + "/users/me");
const user = await response.json();
store_user(user);
return { success: true };
}
}
return { success: false };
},
logout: async () => {
const response = await fetch(API_URL + "/auth/logout", { method: "POST" });
if (response.status == 204 || response.status == 401) {
forget_user();
return { success: true };
}
return { success: false };
},
check: async () => {
return { authenticated: Boolean(get_user()) };
},
getIdentity: async () => {
const user = get_user();
if (user != null) {
return user;
}
const response = await fetch(API_URL + "/users/me");
if (response.status < 200 || response.status > 299) {
return
}
const user_data = await response.json();
store_user(user_data)
return user_data;
},
register: async (params) => {
const response = await fetch(API_URL + "/register", {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
},
});
if (response.status == 201) {
return {
success: true,
redirectTo: "/",
};
}
return {
success: false,
error: {
message: "Register failed",
name: "Invalid email or password",
},
};
},
forgotPassword: async (params) => {
const response = await fetch(API_URL + "/users/forgot-password", {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
},
});
if (response.status == 202) {
return {
success: true,
redirectTo: "/",
};
}
return {
success: false,
};
},
updatePassword: async (params) => {
if (params.token !== undefined) {
const response = await fetch(API_URL + "/users/reset-password", {
method: "POST",
body: JSON.stringify({
password: params.password,
token: params.token,
}),
headers: {
"Content-Type": "application/json",
},
});
if (response.status == 200) {
return {
success: true,
redirectTo: "/",
};
}
}
return {
success: false,
};
},
getPermissions: async () => { throw new Error("Not implemented"); },
onError: async (error) => {
if (error?.status === 401) {
forget_user()
return Promise<{
redirectTo: "/login",
logout: true,
error: { message: "Authentication required" },
}>;
}
else if (error?.status === 403) {
return Promise<{
error: { message: "Insufficient credentials" },
}>;
}
return {};
},
};
function store_user(user: any) {
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
}
function get_user() {
const user_string = localStorage.getItem(LOCAL_STORAGE_USER_KEY)
if (user_string == null) {
return null
}
return JSON.parse(user_string);
}
function forget_user() {
localStorage.removeItem(LOCAL_STORAGE_USER_KEY);
}
function findGetParameter(parameterName: string) {
let result = null, tmp = [];
location.search.substr(1).split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}

View File

@@ -0,0 +1,107 @@
import type { DataProvider } from "@refinedev/core";
const API_URL = "http://localhost:8000";
const fetcher = async (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
});
};
export const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
const response = id !== "" ? await fetcher(`${API_URL}/${resource}/${id}`) : await fetcher(`${API_URL}/${resource}`);
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data
};
},
update: async ({ resource, id, variables }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`, {
method: "PUT",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return { data };
},
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const params = new URLSearchParams();
if (pagination) {
params.append("page", String(pagination.current));
params.append("size", String(pagination.pageSize));
}
if (sorters && sorters.length > 0) {
params.append("order_by", sorters.map((sorter) => (sorter.order == "asc" ? "+" : "-") + sorter.field).join(","));
}
if (filters && filters.length > 0) {
filters.forEach((filter) => {
if ("field" in filter && filter.value && filter.operator === "contains") {
params.append(filter.field + "__like", "%" + filter.value + "%");
}
});
}
const response = await fetcher(`${API_URL}/${resource}?${params.toString()}`);
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data: data.items,
total: data.total, // We'll cover this in the next steps.
};
},
create: async ({ resource, variables }) => {
const response = await fetcher(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return { data };
},
deleteOne: async ({ resource, id, variables, meta }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`,{
method: "DELETE",
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data
};
},
getApiUrl: () => API_URL,
// Optional methods:
// getMany: () => { /* ... */ },
// createMany: () => { /* ... */ },
// deleteMany: () => { /* ... */ },
// updateMany: () => { /* ... */ },
// custom: () => { /* ... */ },
};

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +1,25 @@
{
"files": [],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -1,24 +1,8 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"composite": true,
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
});