Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e3595f97f | |||
| 50186b6840 | |||
| 2c1fb161d1 |
@@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: Build and Deploy Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.rhost.ovh
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./services/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: git.rhost.ovh/${{ github.repository }}:latest
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.2.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
port: ${{ secrets.SERVER_PORT }}
|
||||||
|
username: ${{ secrets.SERVER_USER }}
|
||||||
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /home/ryjek/docker/sprawozdania
|
||||||
|
docker compose pull frontend
|
||||||
|
docker compose up -d frontend
|
||||||
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
package-lock.json
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|||||||
Generated
-3681
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
|||||||
"@mui/material": "^9.0.0",
|
"@mui/material": "^9.0.0",
|
||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^7.14.2",
|
"react-router-dom": "^7.14.2",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:22.15 as build
|
||||||
|
RUN mkdir /public
|
||||||
|
|
||||||
|
WORKDIR /public
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=build /public/dist /usr/share/nginx/html
|
||||||
|
COPY ./services/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend/;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -16,9 +16,9 @@ const darkTheme = createTheme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentTime = Math.ceil(Date.now() / 1000);
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
<CssBaseline/>
|
<CssBaseline/>
|
||||||
@@ -30,7 +30,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* PRIVATE */}
|
{/* PRIVATE */}
|
||||||
<Route element={
|
<Route element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute currentTime={currentTime}>
|
||||||
<DashboardLayout/>
|
<DashboardLayout/>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import {Navigate} from "react-router-dom";
|
import {Navigate} from "react-router-dom";
|
||||||
import {useAuthStore} from "../stores/authStore.jsx";
|
import {useAuthStore} from "../stores/authStore.jsx";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
export default function ProtectedRoute({children}) {
|
export default function ProtectedRoute({children, currentTime}) {
|
||||||
const token = useAuthStore(s => s.token);
|
const token = useAuthStore(s => s.token);
|
||||||
let isAuth = false;
|
let isAuth = false;
|
||||||
if (token) {
|
if (token) {
|
||||||
|
try {
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
if (typeof decoded.exp !== "undefined" && currentTime <= decoded.exp) {
|
||||||
isAuth = true;
|
isAuth = true;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAuth) {
|
if (!isAuth) {
|
||||||
return <Navigate to={"/login"} />;
|
return <Navigate to={"/login"} />;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
type Need = {
|
||||||
|
regexp?: {
|
||||||
|
value: RegExp,
|
||||||
|
message: string
|
||||||
|
};
|
||||||
|
maxLength?: {
|
||||||
|
value: number;
|
||||||
|
message: string
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const validate = (type: string, value: any) => {
|
||||||
|
const needs: Record<string, Need> = {
|
||||||
|
"email": {
|
||||||
|
regexp:{
|
||||||
|
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||||
|
message: "Wpisz poprawny adres email!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nip": {
|
||||||
|
maxLength: {
|
||||||
|
value: 10,
|
||||||
|
message: "NIP powinien mieć 10 cyfr"
|
||||||
|
},
|
||||||
|
regexp:{
|
||||||
|
value: /^\d+$/,
|
||||||
|
message: "Wprowadź poprawny numer nip!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
maxLength: {
|
||||||
|
value: 9,
|
||||||
|
message: "Bledny numer telefonu!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof needs[type] === 'undefined') {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const obj = needs[type];
|
||||||
|
if (typeof obj.maxLength !== "undefined") {
|
||||||
|
if (value.length > obj.maxLength.value || value.length < obj.maxLength.value) {
|
||||||
|
return obj.maxLength.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof obj.regexp !== "undefined") {
|
||||||
|
if (!obj.regexp.value.test(value)) {
|
||||||
|
return obj.regexp.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
export default validate
|
||||||
@@ -19,7 +19,7 @@ api.interceptors.response.use(
|
|||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
+86
-16
@@ -1,46 +1,116 @@
|
|||||||
import {Button, TextField} from "@mui/material";
|
import {Box, Button, FormControl, Grid, Paper, Stack, TextField, Typography} from "@mui/material";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useCreateClient} from "../hooks/useClients.jsx";
|
import {useCreateClient} from "../hooks/useClients.jsx";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import validate from "../lib/addClientValidation.ts";
|
||||||
|
import ClientInput from "./form/ClientInput.jsx";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
|
||||||
const needData = {
|
const needData = {
|
||||||
text:["name","address","email"],
|
text:["name","address","email"],
|
||||||
number:["nip","phone"]
|
number:["nip","phone"]
|
||||||
};
|
};
|
||||||
function ClientAddView() {
|
function ClientAddView() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [data, addData] = useState({});
|
const [formErrors, setFormError] = useState(false);
|
||||||
|
const [data, addData] = useState({
|
||||||
|
name:"",
|
||||||
|
address:"",
|
||||||
|
email:"",
|
||||||
|
nip: "",
|
||||||
|
phone:"",
|
||||||
|
legitimacy:"",
|
||||||
|
proclamation:""
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
email: "",
|
||||||
|
nip: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
const addClient = useCreateClient();
|
const addClient = useCreateClient();
|
||||||
|
const checkForm = () => {
|
||||||
|
for (let errorsKey in errors) {
|
||||||
|
const error = errors[errorsKey];
|
||||||
|
if (error.length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const save = async function(e) {
|
const save = async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!checkForm()) {
|
||||||
|
setFormError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formErrors) {
|
||||||
|
setFormError(false);
|
||||||
|
}
|
||||||
await addClient.mutateAsync(data);
|
await addClient.mutateAsync(data);
|
||||||
navigate('/clients');
|
navigate('/clients');
|
||||||
}
|
}
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
const mess = validate(name, value);
|
||||||
|
setErrors(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: mess
|
||||||
|
}));
|
||||||
|
if (formErrors && checkForm()) {
|
||||||
|
setFormError(false);
|
||||||
|
}
|
||||||
addData(prevState => ({
|
addData(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[name]: value
|
[name]: value
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form onSubmit={save}>
|
<Box sx={{
|
||||||
{needData.text.map((type) => (
|
display: 'flex',
|
||||||
<TextField inputMode={"text"} id={type} variant="outlined" label={type} type={"text"} name={type}
|
alignItems: 'center',
|
||||||
onChange={handleChange}
|
justifyContent: 'center',
|
||||||
value={data[type]}
|
}}>
|
||||||
/>
|
<Paper component="form" onSubmit={save}
|
||||||
))}
|
elevation={1}
|
||||||
{needData.number.map((type) => (
|
sx={{
|
||||||
<TextField inputMode={"numeric"} id={type} variant="outlined" label={type} type={"text"} name={type}
|
p:2
|
||||||
onChange={handleChange}
|
}}>
|
||||||
value={data[type]}
|
<Typography variant="h4" component="h1" sx={{p:2}} >Dodaj nowego klienta</Typography>
|
||||||
/>
|
<Divider component="p" />
|
||||||
))}
|
{formErrors &&
|
||||||
|
<Typography variant="subtitle1" component="p" color={"error"}>Masz bledy w formularzu!</Typography>
|
||||||
|
}
|
||||||
|
<Grid container spacing={2} sx={{p:2}}>
|
||||||
|
<Grid size={6}>
|
||||||
|
<Typography variant="h5" component="p">Dane podmiotu</Typography>
|
||||||
|
|
||||||
|
<ClientInput type="name" label="Nazwa firmy" data={data["name"]} inputMode="text" inputType="text" handleChange={handleChange} error={errors["name"]}/>
|
||||||
|
<ClientInput type="address" label="Adres" data={data["address"]} inputMode="text" inputType="text" handleChange={handleChange} error={errors["address"]}/>
|
||||||
|
<ClientInput type="nip" label="NIP" data={data["nip"]} inputMode="numeric" inputType="number" handleChange={handleChange} error={errors["nip"]}/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={6}>
|
||||||
|
<Typography variant="h5" component="p">Dane kontaktowe</Typography>
|
||||||
|
<ClientInput type="email" label="Adres email" data={data["email"]} inputMode="text" inputType="email" handleChange={handleChange} error={errors["email"]}/>
|
||||||
|
<ClientInput type="phone" label="Numer telefonu" data={data["phone"]} inputMode="numeric" inputType="number" handleChange={handleChange} error={errors["phone"]}/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={6}>
|
||||||
|
<ClientInput type="legitimacy" label="Data prawomocności" data={data["legitimacy"]} inputMode="text" inputType="date" handleChange={handleChange} error={errors["legitimacy"]}/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={6}>
|
||||||
|
<ClientInput type="proclamation" label="Data obwieszczenia" data={data["proclamation"]} inputMode="text" inputType="date" handleChange={handleChange} error={errors["proclamation"]}/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={12}>
|
||||||
<Button variant="outlined" type={"submit"} disabled={addClient.isPending}>
|
<Button variant="outlined" type={"submit"} disabled={addClient.isPending}>
|
||||||
{addClient.isPending ? 'Dodawanie...' : 'Dodaj'}
|
{addClient.isPending ? 'Dodawanie...' : 'Dodaj'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Grid} from "@mui/material";
|
|||||||
|
|
||||||
function DashboardView(){
|
function DashboardView(){
|
||||||
return (
|
return (
|
||||||
<Grid item>
|
<Grid>
|
||||||
trest
|
trest
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
+41
-7
@@ -1,4 +1,4 @@
|
|||||||
import {Button, TextField} from "@mui/material";
|
import {Box, Button, CircularProgress, Paper, Stack, TextField, Typography} from "@mui/material";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import {useAuthStore} from "../stores/authStore.jsx";
|
import {useAuthStore} from "../stores/authStore.jsx";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
@@ -8,33 +8,67 @@ export default function LoginView() {
|
|||||||
const loginAction = useAuthStore(s => s.login);
|
const loginAction = useAuthStore(s => s.login);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
async function handleLogin(e) {
|
async function handleLogin(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
try{
|
try{
|
||||||
await loginAction(username, password);
|
await loginAction(username, password);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e);
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleLogin}>
|
<Box sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Paper component="form"
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
elevation={4}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 400,
|
||||||
|
borderRadius: 2,
|
||||||
|
p:2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography variant="h5" component="h1">Logowanie</Typography>
|
||||||
{error && <p>{error}</p>}
|
{error && <p>{error}</p>}
|
||||||
<div>
|
|
||||||
<TextField inputMode={"text"} id="login" variant="outlined" label="Login" name={"login"}
|
<TextField inputMode={"text"} id="login" variant="outlined" label="Login" name={"login"}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
value={username}
|
value={username}
|
||||||
|
fullWidth
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextField inputMode={"text"} id="password" variant="outlined" label="Password" type={"password"} name={"password"}
|
<TextField inputMode={"text"} id="password" variant="outlined" label="Password" type={"password"} name={"password"}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
value={password}
|
value={password}
|
||||||
|
fullWidth
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
{isLoading ? (
|
||||||
|
<Box sx={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
<CircularProgress aria-label="Loading…" sx={{margin:"auto"}}/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
<Button variant="outlined" type={"submit"}>Zaloguj</Button>
|
<Button variant="outlined" type={"submit"}>Zaloguj</Button>
|
||||||
</form>
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {FormControl, TextField, Typography} from "@mui/material";
|
||||||
|
|
||||||
|
export default function ClientInput(prop){
|
||||||
|
const { type, label, error, handleChange, data, inputMode, inputType } = prop;
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<Typography
|
||||||
|
component="label"
|
||||||
|
htmlFor={type}
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<TextField inputMode={inputMode}
|
||||||
|
id={type} variant="outlined" type={inputType} name={type} key={type}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={data}
|
||||||
|
error={error}
|
||||||
|
helperText={error}
|
||||||
|
// label={label}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user