From 05bab6e620043bace556d2d6fc7fbdb678d06b01 Mon Sep 17 00:00:00 2001 From: arrelin Date: Tue, 16 Dec 2025 11:55:28 +0300 Subject: [PATCH] ci/cd + https + front --- .env.example | 5 + .gitignore | 4 + backend/src/lib.rs | 23 ++-- deploy-update.tar.gz | Bin 0 -> 3047 bytes docker-compose.prod.yml | 35 +++++- docker-compose.yml | 1 + frontend/.env | 2 +- frontend/.env.example | 2 +- frontend/Dockerfile | 2 +- frontend/index.html | 2 +- frontend/public/vite.svg | 4 +- frontend/src/pages/FamilyView.tsx | 187 +++++++++++++++++++++-------- init-letsencrypt.sh | 83 +++++++++++++ nginx/conf.d/app-ssl.conf.template | 81 +++++++++++++ nginx/conf.d/app.conf | 58 +++++++++ nginx/nginx.conf | 37 ++++++ renew-cert.sh | 9 ++ 17 files changed, 463 insertions(+), 72 deletions(-) create mode 100644 deploy-update.tar.gz create mode 100755 init-letsencrypt.sh create mode 100644 nginx/conf.d/app-ssl.conf.template create mode 100644 nginx/conf.d/app.conf create mode 100644 nginx/nginx.conf create mode 100755 renew-cert.sh diff --git a/.env.example b/.env.example index 07c4979..e6ac163 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,8 @@ DATABASE_URL=postgresql://your_db_user:your_secure_password@localhost:5435/famil APP_PORT=8080 RUST_LOG=info + +DOMAIN=yourdomain.com +EMAIL=your@email.com + +ALLOWED_ORIGINS=https://yourdomain.com diff --git a/.gitignore b/.gitignore index 43c1f52..03573f6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ dist/ *.log Cargo.lock + +certbot/ +nginx/conf.d/app-ssl.conf +.env diff --git a/backend/src/lib.rs b/backend/src/lib.rs index acbb429..5770be0 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -152,14 +152,16 @@ pub async fn create_app(db: DatabaseConnection) -> Result { let swagger_ui = SwaggerUi::new("/swagger-ui") .url("/api-docs/openapi.json", ApiDoc::openapi()); + let allowed_origins = std::env::var("ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:3000,http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:8080".to_string()); + + let origins: Vec = allowed_origins + .split(',') + .filter_map(|origin| origin.trim().parse::().ok()) + .collect(); + let cors = CorsLayer::new() - .allow_origin([ - "http://localhost:3000".parse::().unwrap(), - "http://localhost:5173".parse::().unwrap(), - "http://localhost:5174".parse::().unwrap(), - "http://localhost:5175".parse::().unwrap(), - "http://localhost:8080".parse::().unwrap(), - ]) + .allow_origin(origins) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) .allow_headers([ axum::http::header::CONTENT_TYPE, @@ -168,9 +170,10 @@ pub async fn create_app(db: DatabaseConnection) -> Result { ]) .allow_credentials(true); - let app = api_routes - .layer(cors) - .merge(swagger_ui); + let app = Router::new() + .nest("/api", api_routes) + .merge(swagger_ui) + .layer(cors); Ok(app) } diff --git a/deploy-update.tar.gz b/deploy-update.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..266e03e54158a91df4108661e05a6977219519ee GIT binary patch literal 3047 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 literal 0 HcmV?d00001 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 9f1ada1..a3fedf6 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -21,8 +21,7 @@ services: environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} RUST_LOG: ${RUST_LOG:-info} - ports: - - "8080:8080" + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173} depends_on: - postgres networks: @@ -31,16 +30,44 @@ services: frontend: image: ghcr.io/${OWNER:-${COMPOSE_PROJECT_NAME}}/family_budget-frontend:latest container_name: family_budget_frontend - ports: - - "80:80" depends_on: - backend networks: - app_network + nginx: + image: nginx:alpine + container_name: family_budget_nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - certbot_www:/var/www/certbot:ro + - certbot_conf:/etc/letsencrypt:ro + depends_on: + - backend + - frontend + networks: + - app_network + restart: unless-stopped + + certbot: + image: certbot/certbot + container_name: family_budget_certbot + volumes: + - certbot_www:/var/www/certbot:rw + - certbot_conf:/etc/letsencrypt:rw + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + volumes: postgres_data: driver: local + certbot_www: + driver: local + certbot_conf: + driver: local networks: app_network: diff --git a/docker-compose.yml b/docker-compose.yml index 3857c61..e8701e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} RUST_LOG: ${RUST_LOG:-info} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173} ports: - "8080:8080" depends_on: diff --git a/frontend/.env b/frontend/.env index b8ca0df..14ea4ad 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1 @@ -VITE_API_BASE_URL= +VITE_API_BASE_URL=/api diff --git a/frontend/.env.example b/frontend/.env.example index 99cbcd5..14ea4ad 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1 @@ -VITE_API_BASE_URL=http://localhost:3000 +VITE_API_BASE_URL=/api diff --git a/frontend/Dockerfile b/frontend/Dockerfile index ed29a58..98d305d 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -7,7 +7,7 @@ RUN npm ci COPY . . -ARG VITE_API_BASE_URL=/ +ARG VITE_API_BASE_URL=/api ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build diff --git a/frontend/index.html b/frontend/index.html index 072a57e..8862032 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - frontend + Family budget
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg index e7b8dfb..dba30ed 100644 --- a/frontend/public/vite.svg +++ b/frontend/public/vite.svg @@ -1 +1,3 @@ - \ No newline at end of file + + 💰 + diff --git a/frontend/src/pages/FamilyView.tsx b/frontend/src/pages/FamilyView.tsx index c687f1d..5eac7ae 100644 --- a/frontend/src/pages/FamilyView.tsx +++ b/frontend/src/pages/FamilyView.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { categoryApi, expenseApi } from '../api/client'; import { useStore } from '../store/useStore'; -import type { Category } from '../types'; +import type { Category, Expense } from '../types'; import { ArrowLeft, Wallet, @@ -14,6 +14,9 @@ import { X, DollarSign, Tag, + History, + Calendar, + MessageSquare, } from 'lucide-react'; export default function FamilyView() { @@ -34,6 +37,9 @@ export default function FamilyView() { const [expenseAmount, setExpenseAmount] = useState(''); const [expenseDescription, setExpenseDescription] = useState(''); + const [showHistory, setShowHistory] = useState(null); + const [categoryExpenses, setCategoryExpenses] = useState([]); + useEffect(() => { if (!familyId) { navigate('/'); @@ -150,6 +156,27 @@ export default function FamilyView() { } }; + const handleShowHistory = async (categoryId: number) => { + if (!familyId) return; + + if (showHistory === categoryId) { + setShowHistory(null); + return; + } + + try { + const response = await expenseApi.getAllByCategory( + parseInt(familyId), + categoryId + ); + setCategoryExpenses(response.data); + setShowHistory(categoryId); + } catch (err) { + alert('Ошибка загрузки истории трат'); + console.error(err); + } + }; + if (loading) { return (
@@ -172,6 +199,21 @@ export default function FamilyView() { return Math.max(0, Math.min(100, (remaining / limit) * 100)); }; + const formatDate = (dateString: string) => { + let dateStr = dateString; + if (!dateStr.endsWith('Z') && !dateStr.includes('+')) { + dateStr = dateStr + 'Z'; + } + const date = new Date(dateStr); + return date.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + return (
@@ -214,14 +256,14 @@ export default function FamilyView() { return (
-
-
-
- +
+
+
+
-

+

{category.name}

@@ -229,38 +271,113 @@ export default function FamilyView() { {showAddExpense !== category.id && ( )}
-
+
- Остаток: - + Остаток: + {remaining.toFixed(2)} ₽
-
+
Лимит: - {limit.toFixed(2)} ₽ + {limit.toFixed(2)} ₽
-
+
-

+

{percentage.toFixed(0)}% осталось

+
+ + + +
+ + {showHistory === category.id && ( +
+
+

+ + История трат +

+ +
+ + {categoryExpenses.length === 0 ? ( +

Нет трат

+ ) : ( +
+ {categoryExpenses.map((expense) => ( +
+
+
+
+ +
+ + {parseFloat(expense.amount.toString()).toFixed(2)} ₽ + +
+
+ + {formatDate(expense.created_at)} +
+
+ {expense.description && ( +
+ + {expense.description} +
+ )} +
+ ))} +
+ )} +
+ )} + {showAddExpense === category.id && (

@@ -364,48 +481,12 @@ export default function FamilyView() { ) : ( )} - -
- {categories.map((category) => ( -
-
-
- -
- - {category.name} - -
-
- - -
-
- ))} -

diff --git a/init-letsencrypt.sh b/init-letsencrypt.sh new file mode 100755 index 0000000..fcd886d --- /dev/null +++ b/init-letsencrypt.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +if [ -z "$DOMAIN" ]; then + echo "Error: DOMAIN environment variable is not set" + echo "Usage: DOMAIN=yourdomain.com EMAIL=your@email.com ./init-letsencrypt.sh" + exit 1 +fi + +if [ -z "$EMAIL" ]; then + echo "Error: EMAIL environment variable is not set" + echo "Usage: DOMAIN=yourdomain.com EMAIL=your@email.com ./init-letsencrypt.sh" + exit 1 +fi + +echo "### Initializing Let's Encrypt for domain: $DOMAIN" +echo "### Email: $EMAIL" + +data_path="./certbot" +rsa_key_size=4096 +staging=0 + +if [ -d "$data_path/conf/live/$DOMAIN" ]; then + read -p "Existing certificate found for $DOMAIN. Continue and replace? (y/N) " decision + if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then + exit + fi +fi + +if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then + echo "### Downloading recommended TLS parameters ..." + mkdir -p "$data_path/conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo +fi + +echo "### Creating dummy certificate for $DOMAIN ..." +path="/etc/letsencrypt/live/$DOMAIN" +mkdir -p "$data_path/conf/live/$DOMAIN" +docker compose -f docker-compose.prod.yml run --rm --entrypoint "\ + openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" certbot +echo + +echo "### Starting nginx ..." +docker compose -f docker-compose.prod.yml up --force-recreate -d nginx +echo + +echo "### Deleting dummy certificate for $DOMAIN ..." +docker compose -f docker-compose.prod.yml run --rm --entrypoint "\ + rm -Rf /etc/letsencrypt/live/$DOMAIN && \ + rm -Rf /etc/letsencrypt/archive/$DOMAIN && \ + rm -Rf /etc/letsencrypt/renewal/$DOMAIN.conf" certbot +echo + +echo "### Requesting Let's Encrypt certificate for $DOMAIN ..." +domain_args="-d $DOMAIN" + +case "$staging" in + 1) staging_arg="--staging" ;; + *) staging_arg="" ;; +esac + +docker compose -f docker-compose.prod.yml run --rm --entrypoint "\ + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + $domain_args \ + --email $EMAIL \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal" certbot +echo + +echo "### Enabling SSL configuration..." +envsubst '${DOMAIN}' < nginx/conf.d/app-ssl.conf.template > nginx/conf.d/app-ssl.conf +rm nginx/conf.d/app.conf + +echo "### Reloading nginx ..." +docker compose -f docker-compose.prod.yml exec nginx nginx -s reload + +echo "### Done! Your site is now secured with HTTPS" diff --git a/nginx/conf.d/app-ssl.conf.template b/nginx/conf.d/app-ssl.conf.template new file mode 100644 index 0000000..330e16b --- /dev/null +++ b/nginx/conf.d/app-ssl.conf.template @@ -0,0 +1,81 @@ +upstream backend { + server backend:8080; +} + +upstream frontend { + server frontend:80; +} + +server { + listen 80; + server_name ${DOMAIN}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name ${DOMAIN}; + + ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + } + + location /swagger-ui { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + } + + location /api-docs { + 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 / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/nginx/conf.d/app.conf b/nginx/conf.d/app.conf new file mode 100644 index 0000000..49194f6 --- /dev/null +++ b/nginx/conf.d/app.conf @@ -0,0 +1,58 @@ +upstream backend { + server backend:8080; +} + +upstream frontend { + server frontend:80; +} + +server { + listen 80; + server_name _; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + } + + location /swagger-ui { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + } + + location /api-docs { + 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 / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..ad5a57d --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,37 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 20M; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/renew-cert.sh b/renew-cert.sh new file mode 100755 index 0000000..dd89848 --- /dev/null +++ b/renew-cert.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "### Renewing SSL certificate..." +docker compose -f docker-compose.prod.yml run --rm certbot renew + +echo "### Reloading nginx..." +docker compose -f docker-compose.prod.yml exec nginx nginx -s reload + +echo "### Done!"