2 Commits

Author SHA1 Message Date
15bc323ee4 test: add client services 2025-06-10 17:06:19 +05:00
958f00069f test 2025-06-10 17:05:35 +05:00
40 changed files with 15713 additions and 2285 deletions

View File

@@ -17,7 +17,7 @@ start-api:
start-client:
cd client && \
npm run dev
npm start
migrate:
cd api && \
@@ -42,10 +42,6 @@ venv-api:
poetry env activate \
poetry install
venv-client:
cd client && \
npm install
install:
make migrate head && \
cd api && \

View File

@@ -1,49 +1 @@
# Vorkout/connect
### Makefile cheat sheet
```Makefile
Dev:
venv-api create python virtual environment
venv-client install node modules
install Migrate database and initialize project
Application Api:
start-api Run api server
Application Client:
start-client Run client server
Prod:
...
Code:
check-api Check api code with ruff
format-api Reformat api code with ruff
Help:
...
Testing:
...
```
### Запуск в режиме разработки
Для запуска в режиме разработки нужно
1. Устрановить среду для clint и api
2. Запустить в докере или локально необходимые сервисы (базуб брокер и redis) `make services`
3. Для миграции и создания первого пользователя необходимо запустить `make install`
3. Запустить api `make start-api`
4. Запустить client `make start-client`
### Миграции алембик
1. Стоит внимательно учитывать, адрес какой базы стоит в настройках alembic - локальной или продакшн. Посмотреть это можно в файле [env.py](connect/api/api/db/alembic/env.py). Конфиг для локальной базы
```python
config.set_main_option(
"sqlalchemy.url",
f"mysql+pymysql://root:hackme@localhost:3306/connect_test",
)
```
Vorkout/connect

View File

@@ -18,7 +18,8 @@ class DbCredentialsSchema(BaseModel):
class DefaultSettings(BaseSettings):
ENV: str = environ.get("ENV", "local")
PATH_PREFIX: str = environ.get("PATH_PREFIX", "/api/v1")
APP_HOST: str = environ.get("APP_HOST", "http://127.0.0.1")
# APP_HOST: str = environ.get("APP_HOST", "http://127.0.0.1")
APP_HOST: str = environ.get("APP_HOST", "http://localhost")
APP_PORT: int = int(environ.get("APP_PORT", 8000))
APP_ID: uuid.UUID = environ.get("APP_ID", uuid.uuid4())
LOGS_STORAGE_PATH: str = environ.get("LOGS_STORAGE_PATH", "storage/logs")

View File

@@ -1,14 +1,14 @@
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import (
APIRouter,
Depends,
HTTPException,
Request,
Response,
status,
Request,
)
from loguru import logger
from fastapi_jwt_auth import AuthJWT
@@ -22,7 +22,7 @@ from api.services.auth import authenticate_user
from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token
from api.schemas.endpoints.auth import Auth, Tokens
from api.schemas.endpoints.auth import Auth, Access
api_router = APIRouter(
prefix="/auth",
@@ -30,11 +30,21 @@ api_router = APIRouter(
)
def get_login_from_jwt(token: str):
payload = jwt.decode(
token,
get_settings().SECRET_KEY,
algorithms=[get_settings().ALGORITHM],
)
return payload.get("sub")
class Settings(BaseModel):
authjwt_secret_key: str = get_settings().SECRET_KEY
# Configure application to store and get JWT from cookies
authjwt_token_location: set = {"headers"}
authjwt_token_location: set = {"headers", "cookies"}
authjwt_cookie_domain: str = get_settings().DOMAIN
authjwt_refresh_cookie_name: str = "refresh_token_cookie"
# Only allow JWT cookies to be sent over https
authjwt_cookie_secure: bool = get_settings().ENV == "prod"
@@ -48,7 +58,7 @@ def get_config():
return Settings()
@api_router.post("", response_model=Tokens)
@api_router.post("", response_model=Access)
async def login_for_access_token(
user: Auth,
response: Response,
@@ -68,7 +78,9 @@ async def login_for_access_token(
# headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
# access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
access_token_expires = timedelta(seconds=5)
refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS)
logger.debug(f"refresh_token_expires {refresh_token_expires}")
@@ -80,27 +92,26 @@ async def login_for_access_token(
await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user)
return Tokens(access_token=access_token, refresh_token=refresh_token)
Authorize.set_refresh_cookies(refresh_token)
return Access(access_token=access_token)
@api_router.post("/refresh", response_model=Tokens)
@api_router.post("/refresh", response_model=Access)
async def refresh(
request: Request,
connection: AsyncConnection = Depends(get_connection_dep),
Authorize: AuthJWT = Depends(),
) -> Tokens:
try:
Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject()
except Exception:
refresh_token = request.headers.get("Authorization").split(" ")[1]
await upgrade_old_refresh_token(connection, refresh_token)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
):
refresh_token = request.cookies.get("refresh_token_cookie")
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
if not refresh_token:
raise HTTPException(status_code=401, detail="Refresh token is missing")
Authorize.jwt_refresh_token_required(refresh_token)
current_user = Authorize.get_jwt_subject()
# try:
# access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
access_token_expires = timedelta(seconds=5)
new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires)
return Tokens(access_token=new_access_token)
return Access(access_token=new_access_token)

View File

@@ -8,6 +8,9 @@ class Auth(Base):
password: str
class Tokens(Base):
class Refresh(Base):
refresh_token: str
class Access(Base):
access_token: str
refresh_token: str | None = None

View File

@@ -1,4 +1,3 @@
from fastapi_jwt_auth import AuthJWT
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import (
Request,
@@ -12,6 +11,9 @@ import re
from re import escape
from fastapi_jwt_auth import AuthJWT
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
@@ -30,8 +32,10 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
content={"detail": "Method not allowed"},
)
if any(pattern.match(request.url.path) for pattern in self.excluded_routes):
return await call_next(request)
auth_header = request.headers.get("Authorization")
if not auth_header:
return JSONResponse(
@@ -39,6 +43,7 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
content={"detail": "Missing authorization header."},
headers={"WWW-Authenticate": "Bearer"},
)
try:
token = auth_header.split(" ")[1]
Authorize = AuthJWT(request)
@@ -50,4 +55,5 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
content={"detail": "The access token is invalid or expired."},
headers={"WWW-Authenticate": "Bearer"},
)
return await call_next(request)

View File

@@ -1,8 +1,10 @@
[project]
name = "api"
version = "0.0.4"
version = "0.0.3"
description = ""
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
authors = [
{name = "Vladislav",email = "vlad.dev@heado.ru"}
]
readme = "README.md"
requires-python = ">=3.11,<4.0"
dependencies = [

View File

@@ -1,4 +1,5 @@
VITE_APP_WEBSOCKET_PROTOCOL=ws
VITE_APP_HTTP_PROTOCOL=http
VITE_APP_API_URL=localhost:8000
VITE_APP_URL=localhost:3000
REACT_APP_WEBSOCKET_PROTOCOL=ws
REACT_APP_HTTP_PROTOCOL=http
REACT_APP_API_URL=localhost:8000
REACT_APP_URL=localhost:3000
BROWSER=none

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using Vite" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>VORKOUT</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

17414
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.0.3",
"version": "0.0.2",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.6.1",
@@ -9,6 +9,7 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4",
"antd": "^5.24.7",
@@ -20,13 +21,16 @@
"react-dom": "^18.3.1",
"react-i18next": "^15.5.1",
"react-router-dom": "^7.5.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4",
"zustand": "^5.0.5"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
@@ -45,13 +49,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@types/node": "^20.19.1",
"@vitejs/plugin-react": "^4.5.2",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-node-polyfills": "^0.23.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

43
client/public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>VORKOUT</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
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 start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,21 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect } from 'react';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { useSetUserSelector } from './store/userStore';
import LoginPage from './pages/LoginPage';
import ProtectedRoute from './pages/ProtectedRoute';
import MainLayout from './pages/MainLayout';
import ProtectedRoute from './pages/ProtectedRoute';
import LoginPage from './pages/LoginPage';
function App() {
const setUser = useSetUserSelector();
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
return (
<div className="App">
<Routes>

View File

@@ -1,14 +1,10 @@
import axios from 'axios';
// import { Auth, Tokens } from '../types/auth';
import { Access, Auth } from '../types/auth';
import { User } from '../types/user';
import { AuthService } from '../services/auth';
import axiosRetry from 'axios-retry';
import { Auth, Tokens } from '@/types/auth';
import { useAuthStore } from '@/store/authStore';
import { AuthService } from '@/services/authService';
import { User } from '@/types/user';
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
import.meta.env.VITE_APP_API_URL
}/api/v1`;
const baseURL = `${process.env.REACT_APP_HTTP_PROTOCOL}://${process.env.REACT_APP_API_URL}/api/v1`;
const base = axios.create({
baseURL,
@@ -19,40 +15,35 @@ const base = axios.create({
});
base.interceptors.request.use((config) => {
if (config.url === '/auth/refresh') {
return config;
}
const token = useAuthStore.getState().accessToken;
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axiosRetry(base, {
retries: 3,
retryDelay: (retryCount: number) => {
console.log(`retry attempt: ${retryCount}`);
return retryCount * 2000;
},
retryCondition: async (error: any) => {
if (error.code === 'ERR_CANCELED') {
return true;
}
return false;
},
});
// axiosRetry(base, {
// retries: 3,
// retryDelay: (retryCount: number) => {
// console.log(`retry attempt: ${retryCount}`);
// return retryCount * 2000;
// },
// retryCondition: async (error: any) => {
// if (error.code === 'ERR_CANCELED') {
// return true;
// }
// return false;
// },
// });
base.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
if (!error.response) {
return Promise.reject(error);
}
console.log('error', error);
const originalRequest = error.response.config;
console.log('originalRequest._retry', originalRequest);
const urlTokens = error?.request?.responseURL.split('/');
const url = urlTokens[urlTokens.length - 1];
console.log('url', url);
@@ -64,13 +55,11 @@ base.interceptors.response.use(
url !== 'logout'
) {
originalRequest._retry = true;
try {
await AuthService.refresh();
return base(originalRequest);
} catch (error) {
const res = await AuthService.refresh().catch(async () => {
await AuthService.logout();
return new Promise(() => {});
}
});
console.log('res', res);
return await base(originalRequest);
}
return await Promise.reject(error);
}
@@ -78,22 +67,14 @@ base.interceptors.response.use(
const api = {
// auth
async login(auth: Auth): Promise<Tokens> {
const response = await base.post<Tokens>('/auth', auth);
async login(auth: Auth): Promise<Access> {
console.log(auth);
const response = await base.post<Access>('/auth', auth);
return response.data;
},
async refreshToken(): Promise<Tokens> {
const token = localStorage.getItem('refreshToken');
const response = await base.post<Tokens>(
'/auth/refresh',
{},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
async refreshToken(): Promise<Access> {
const response = await base.post<Access>('/auth/refresh');
return response.data;
},
@@ -102,13 +83,6 @@ const api = {
const response = await base.get<User>('/profile');
return response.data;
},
async getUsers(page: number, limit: number): Promise<any> {
const response = await base.get<User[]>(
`/account?page=${page}&limit=${limit}`
);
return response.data;
},
};
export default api;

View File

@@ -1,9 +1,8 @@
import '@/config/i18n';
import './i18n';
import { ConfigProvider } from 'antd';
import { useTranslation } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import { theme } from '@/config/customTheme';
import { theme } from './customTheme';
import en from 'antd/locale/en_US';
import ru from 'antd/locale/ru_RU';
@@ -19,7 +18,7 @@ export default function AppWrapper({ children }: any) {
return (
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
<BrowserRouter>{children}</BrowserRouter>
{children}
</ConfigProvider>
);
}

12
client/src/env.d.ts vendored
View File

@@ -1,12 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_WEBSOCKET_PROTOCOL: string;
readonly VITE_APP_HTTP_PROTOCOL: string;
readonly VITE_APP_API_URL: string;
readonly VITE_APP_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,8 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import '@/index.css';
import App from '@/App';
import AppWrapper from '@/config/AppWrapper';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import AppWrapper from './config/AppWrapper';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
@@ -10,6 +11,8 @@ const root = ReactDOM.createRoot(
root.render(
<AppWrapper>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</AppWrapper>
);

View File

@@ -1,9 +1,8 @@
import Header from '../components/Header';
import { useState } from 'react';
import ContentDrawer from '../components/ContentDrawer';
import UserCreate from '../components/UserCreate';
import { useTranslation } from 'react-i18next';
import { User } from '@/types/user';
import Header from '@/components/Header';
import ContentDrawer from '@/components/ContentDrawer';
import UserCreate from '@/components/UserCreate';
export default function AccountsPage() {
const { t } = useTranslation();
@@ -12,8 +11,6 @@ export default function AccountsPage() {
const showDrawer = () => setOpen(true);
const closeDrawer = () => setOpen(false);
const [accounts, setAccounts] = useState<User[]>([]);
return (
<>
<Header

View File

@@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ConfigurationPage() {
const { t } = useTranslation();

View File

@@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function EventsListPage() {
const { t } = useTranslation();

View File

@@ -5,9 +5,9 @@ import {
EyeTwoTone,
UserOutlined,
} from '@ant-design/icons';
import { AuthService } from '../services/auth';
import { Auth } from '../types/auth';
import { useNavigate } from 'react-router-dom';
import { AuthService } from '@/services/authService';
import { Auth } from '@/types/auth';
const { Text, Link } = Typography;

View File

@@ -2,13 +2,15 @@
import React, { useEffect, useState } from 'react';
import { Layout } from 'antd';
import Sider from 'antd/es/layout/Sider';
import SiderMenu from '../components/SiderMenu';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import SiderMenu from '@/components/SiderMenu';
import ProcessDiagramPage from './ProcessDiagramPage';
import RunningProcessesPage from './RunningProcessesPage';
import AccountsPage from './AccountsPage';
import EventsListPage from './EventsListPage';
import ConfigurationPage from './ConfigurationPage';
import { useSetUserSelector } from '../store/user';
import { UserService } from '../services/user';
export default function MainLayout() {
const navigate = useNavigate();
@@ -19,6 +21,8 @@ export default function MainLayout() {
const [width, setWidth] = useState<number | string>('15%');
const [collapsedWidth, setCollapsedWidth] = useState(50);
const setUser = useSetUserSelector()
const calculateWidths = () => {
const windowWidth = window.innerWidth;
const expanded = Math.min(Math.max(windowWidth * 0.15, 180), 240);
@@ -54,6 +58,21 @@ export default function MainLayout() {
navigate(key);
}
useEffect(() => {
const token = localStorage.getItem('accessToken');
if (!token) {
navigate('/login');
} else {
if (localStorage.getItem('user')) {
setUser(JSON.parse(localStorage.getItem('user') as string))
} else {
UserService.getProfile().then((user) => {
setUser(user);
});
}
}
}, [])
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider

View File

@@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ProcessDiagramPage() {
const { t } = useTranslation();

View File

@@ -1,14 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */
// ProtectedRoute.js
import { Outlet, useNavigate } from 'react-router-dom';
import React, { useEffect } from 'react';
import { useUserSelector } from '@/store/userStore';
import { useUserSelector } from '../store/user';
const ProtectedRoute = (): React.JSX.Element => {
const navigate = useNavigate();
const user = useUserSelector();
const navigate = useNavigate();
useEffect(() => {
if (!user?.id) {
if (user.id === null) {
navigate('/login');
}
}, [user]);

View File

@@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function RunningProcessesPage() {
const { t } = useTranslation();

View File

@@ -0,0 +1,22 @@
import api from '../api/api';
import { useUserStore } from '../store/user';
import { Auth } from '../types/auth';
export class AuthService {
static async login(auth: Auth) {
const token = await api.login(auth);
console.log(token)
localStorage.setItem('accessToken', token.accessToken);
}
static async logout() {
useUserStore.getState().removeUser();
localStorage.removeItem('userInfo');
localStorage.removeItem('accessToken');
}
static async refresh() {
const token = await api.refreshToken();
localStorage.setItem('accessToken', token.accessToken);
}
}

View File

@@ -1,30 +0,0 @@
import api from "@/api/api";
import { useAuthStore } from "@/store/authStore";
import { Auth } from "@/types/auth";
import { UserService } from "./userService";
import { useUserStore } from "@/store/userStore";
export class AuthService {
static async login(auth: Auth) {
const token = await api.login(auth);
useAuthStore.getState().setAccessToken(token.accessToken);
localStorage.setItem('refreshToken', token.refreshToken as string);
await UserService.getProfile().then((user) => {
useUserStore.getState().setUser(user);
});
}
static async logout() {
console.log('logout');
useUserStore.getState().setUser(null);
useAuthStore.getState().setAccessToken(null);
localStorage.removeItem('userInfo');
localStorage.removeItem('refreshToken');
}
static async refresh() {
console.log('refresh');
const token = await api.refreshToken();
useAuthStore.getState().setAccessToken(token.accessToken);
}
}

View File

@@ -0,0 +1,10 @@
import api from '../api/api';
import { User } from '../types/user';
export class UserService {
static async getProfile(): Promise<User> {
const user = api.getProfile();
return user;
}
}

View File

@@ -1,16 +0,0 @@
import api from '@/api/api';
import { User } from '@/types/user';
export class UserService {
static async getProfile(): Promise<User> {
console.log('getProfile');
const user = api.getProfile();
return user;
}
static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
const users = api.getUsers(page, limit);
return users;
}
}

View File

@@ -1,18 +0,0 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
type AuthState = {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
};
export const useAuthStore = create<AuthState>()(
devtools((set) => ({
accessToken: null,
setAccessToken: (token) => set({ accessToken: token }),
}))
);
export const useAuthSelector = () => {
return useAuthStore((state) => state.accessToken);
};

View File

@@ -1,16 +1,17 @@
import { User } from '@/types/user';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { User } from '../types/user';
const userInfo = localStorage.getItem('userInfo');
type UserStoreState = {
user: User | null;
user: User;
loading: boolean;
};
type UserStoreActions = {
setUser: (user: User | null) => void;
setUser: (user: User) => void;
removeUser: () => void;
};
type UserStore = UserStoreState & UserStoreActions;
@@ -21,7 +22,8 @@ export const useUserStore = create<UserStore>()(
(set, get) => ({
user: userInfo != null ? JSON.parse(userInfo) : ({} as User),
loading: false,
setUser: (user: User | null) => set({ user }),
setUser: (user: User) => set({ user }),
removeUser: () => set({ user: {} as User }),
}),
{ name: 'userInfo' }
)

View File

@@ -1,4 +1,4 @@
import { components } from './openapi-types';
export type Auth = components['schemas']['Auth'];
export type Tokens = components['schemas']['Tokens'];
export type Access = components['schemas']['Access'];

View File

@@ -120,6 +120,11 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** Access */
Access: {
/** Accesstoken */
accessToken: string;
};
/** AccountKeyring */
AccountKeyring: {
/** Ownerid */
@@ -214,13 +219,6 @@ export interface components {
* @enum {string}
*/
KeyType: "PASSWORD" | "ACCESS_TOKEN" | "REFRESH_TOKEN" | "API_KEY";
/** Tokens */
Tokens: {
/** Accesstoken */
accessToken: string;
/** Refreshtoken */
refreshToken?: string | null;
};
/** User */
User: {
/** Id */
@@ -307,7 +305,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Tokens"];
"application/json": components["schemas"]["Access"];
};
};
/** @description Validation Error */
@@ -336,7 +334,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Tokens"];
"application/json": components["schemas"]["Access"];
};
};
};

View File

@@ -1,3 +1,3 @@
import { components } from './openapi-types';
import { components } from "./openapi-types"
export type User = components['schemas']['User'];
export type User = components["schemas"]["User"];

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -14,11 +18,9 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
"jsx": "react-jsx"
},
"include": ["src"]
"include": [
"src"
]
}

View File

@@ -1,23 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
open: false,
},
build: {
outDir: 'build',
},
preview: {
port: 3000,
open: false,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "connect",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

1
package.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -32,11 +32,11 @@ startretries=5
[program:client]
environment=
VITE_APP_WEBSOCKET_PROTOCOL=ws,
VITE_APP_HTTP_PROTOCOL=http,
VITE_APP_API_URL=localhost:8000,
VITE_APP_URL=localhost:3000
command=bash -c 'cd client; npm run build; npm run preview'
REACT_APP_WEBSOCKET_PROTOCOL=ws,
REACT_APP_HTTP_PROTOCOL=http,
REACT_APP_API_URL=localhost:8000,
REACT_APP_URL=localhost:3000
command=bash -c 'cd client; npm run build; serve -s build'
numprocs=1
process_name=node-%(process_num)d
stdout_logfile=client.out.log