Compare commits

..

3 Commits

Author SHA1 Message Date
deploy 5e3595f97f add docker for release; add cicdg
Build and Deploy Docker / build (push) Successful in 3m49s
Build and Deploy Docker / deploy (push) Successful in 30s
2026-07-14 15:01:23 +02:00
deploy 50186b6840 fix auth lib; add client view; add client validation; minor fixes 2026-07-13 22:16:15 +02:00
deploy 2c1fb161d1 add login view 2026-07-12 22:08:11 +02:00
15 changed files with 325 additions and 3723 deletions
+4
View File
@@ -0,0 +1,4 @@
dist
node_modules
package-lock.json
README.md
+48
View File
@@ -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
+1
View File
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
package-lock.json
dist
dist-ssr
*.local
-3681
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -16,6 +16,7 @@
"@mui/material": "^9.0.0",
"@tanstack/react-query": "^5.100.5",
"axios": "^1.15.2",
"jwt-decode": "^4.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.2",
+15
View File
@@ -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
+22
View File
@@ -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
View File
@@ -16,9 +16,9 @@ const darkTheme = createTheme({
},
});
const currentTime = Math.ceil(Date.now() / 1000);
export default function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline/>
@@ -30,7 +30,7 @@ export default function App() {
{/* PRIVATE */}
<Route element={
<ProtectedRoute>
<ProtectedRoute currentTime={currentTime}>
<DashboardLayout/>
</ProtectedRoute>
}>
+9 -1
View File
@@ -1,12 +1,20 @@
import {Navigate} from "react-router-dom";
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);
let isAuth = false;
if (token) {
try {
const decoded = jwtDecode(token);
if (typeof decoded.exp !== "undefined" && currentTime <= decoded.exp) {
isAuth = true;
}
} catch (error) {
console.error(error);
}
}
if (!isAuth) {
return <Navigate to={"/login"} />;
+55
View File
@@ -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
+1 -1
View File
@@ -19,7 +19,7 @@ api.interceptors.response.use(
localStorage.removeItem('token');
window.location.href = '/login';
}
// @ts-ignore
return Promise.reject(err);
}
);
+86 -16
View File
@@ -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 {useCreateClient} from "../hooks/useClients.jsx";
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 = {
text:["name","address","email"],
number:["nip","phone"]
};
function ClientAddView() {
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 checkForm = () => {
for (let errorsKey in errors) {
const error = errors[errorsKey];
if (error.length > 0) {
return false;
}
}
return true;
}
const save = async function(e) {
e.preventDefault();
if (!checkForm()) {
setFormError(true);
return;
}
if (formErrors) {
setFormError(false);
}
await addClient.mutateAsync(data);
navigate('/clients');
}
const handleChange = (e) => {
const { name, value } = e.target;
const mess = validate(name, value);
setErrors(prevState => ({
...prevState,
[name]: mess
}));
if (formErrors && checkForm()) {
setFormError(false);
}
addData(prevState => ({
...prevState,
[name]: value
}))
}
return (
<form onSubmit={save}>
{needData.text.map((type) => (
<TextField inputMode={"text"} id={type} variant="outlined" label={type} type={"text"} name={type}
onChange={handleChange}
value={data[type]}
/>
))}
{needData.number.map((type) => (
<TextField inputMode={"numeric"} id={type} variant="outlined" label={type} type={"text"} name={type}
onChange={handleChange}
value={data[type]}
/>
))}
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<Paper component="form" onSubmit={save}
elevation={1}
sx={{
p:2
}}>
<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}>
{addClient.isPending ? 'Dodawanie...' : 'Dodaj'}
</Button>
</form>
</Grid>
</Grid>
</Paper>
</Box>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@ import {Grid} from "@mui/material";
function DashboardView(){
return (
<Grid item>
<Grid>
trest
</Grid>
);
+41 -7
View File
@@ -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 {useAuthStore} from "../stores/authStore.jsx";
import {useState} from "react";
@@ -8,33 +8,67 @@ export default function LoginView() {
const loginAction = useAuthStore(s => s.login);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null)
async function handleLogin(e) {
e.preventDefault();
setIsLoading(true);
try{
await loginAction(username, password);
navigate("/");
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
}
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>}
<div>
<TextField inputMode={"text"} id="login" variant="outlined" label="Login" name={"login"}
onChange={e => setUsername(e.target.value)}
value={username}
fullWidth
disabled={isLoading}
/>
</div>
<div>
<TextField inputMode={"text"} id="password" variant="outlined" label="Password" type={"password"} name={"password"}
onChange={e => setPassword(e.target.value)}
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>
</form>
)}
</Stack>
</Paper>
</Box>
);
}
+25
View File
@@ -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>
);
}