Initial commit: HotelSync PMS v0.1.0
- React 18 + TypeScript + Vite + Tailwind CSS - Шахматка бронирований (drag-to-book) - Страницы: Calendar, Bookings, Rooms, Housekeeping, Channels, API Docs, Settings - Роли: super_admin, hotel_manager, housekeeper - Светлая/тёмная тема - Docker + Nginx конфигурация - Лендинг hotelsync.ru
This commit is contained in:
449
deploy/landing/index.html
Normal file
449
deploy/landing/index.html
Normal file
@@ -0,0 +1,449 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="HotelSync — современная SaaS PMS система для управления отелем. Шахматка бронирований, менеджер каналов, уборка." />
|
||||
<title>HotelSync — PMS система для отелей</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--brand: #4f46e5;
|
||||
--brand-light: #6366f1;
|
||||
--brand-dark: #3730a3;
|
||||
--text: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--bg: #f8fafc;
|
||||
--white: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Nav ─────────────────────────────────────── */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(255,255,255,0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 1.5rem;
|
||||
height: 64px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.logo {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 1.25rem; font-weight: 700; color: var(--text); text-decoration: none;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
background: var(--brand); color: white;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; font-weight: 800;
|
||||
}
|
||||
.logo span { color: var(--brand); }
|
||||
.nav-cta {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: var(--brand); color: white;
|
||||
padding: 0.5rem 1.25rem; border-radius: 8px;
|
||||
font-size: 0.875rem; font-weight: 600; text-decoration: none;
|
||||
transition: background 0.15s, transform 0.15s;
|
||||
}
|
||||
.nav-cta:hover { background: var(--brand-dark); transform: translateY(-1px); }
|
||||
|
||||
/* ── Hero ────────────────────────────────────── */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 6rem 1.5rem 4rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(99,102,241,0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
background: rgba(99,102,241,0.1); color: var(--brand);
|
||||
border: 1px solid rgba(99,102,241,0.25);
|
||||
padding: 0.35rem 0.9rem; border-radius: 999px;
|
||||
font-size: 0.8rem; font-weight: 600; margin-bottom: 1.5rem;
|
||||
}
|
||||
.badge-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--brand); animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.5rem, 7vw, 4.5rem);
|
||||
font-weight: 800; line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
max-width: 900px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
h1 em { font-style: normal; color: var(--brand); }
|
||||
|
||||
.hero-sub {
|
||||
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||
color: var(--text-muted); line-height: 1.7;
|
||||
max-width: 600px; margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; justify-content: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: var(--brand); color: white;
|
||||
padding: 0.875rem 2rem; border-radius: 12px;
|
||||
font-size: 1rem; font-weight: 600; text-decoration: none;
|
||||
transition: all 0.15s; box-shadow: 0 4px 16px rgba(99,102,241,0.35);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--brand-dark); transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(99,102,241,0.4);
|
||||
}
|
||||
.btn-secondary {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: white; color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.875rem 2rem; border-radius: 12px;
|
||||
font-size: 1rem; font-weight: 600; text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--bg); transform: translateY(-2px); }
|
||||
|
||||
/* ── App preview card ─────────────────────────── */
|
||||
.preview-card {
|
||||
width: min(860px, 90vw);
|
||||
background: white; border-radius: 20px;
|
||||
box-shadow: 0 32px 80px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.05);
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
.preview-topbar {
|
||||
background: white; border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.preview-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.preview-inner {
|
||||
display: grid; grid-template-columns: 180px 1fr;
|
||||
height: 320px;
|
||||
}
|
||||
.preview-sidebar {
|
||||
background: #f8fafc; border-right: 1px solid var(--border);
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
.preview-sidebar-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0.5rem 0.75rem; border-radius: 8px;
|
||||
font-size: 0.75rem; font-weight: 500; color: #64748b;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.preview-sidebar-item.active {
|
||||
background: rgba(99,102,241,0.1); color: var(--brand);
|
||||
}
|
||||
.preview-sidebar-icon {
|
||||
width: 16px; height: 16px; border-radius: 4px;
|
||||
background: currentColor; opacity: 0.4;
|
||||
}
|
||||
.preview-content { padding: 1rem; overflow: hidden; }
|
||||
.preview-row {
|
||||
display: flex; gap: 6px; margin-bottom: 6px; align-items: center;
|
||||
}
|
||||
.preview-label {
|
||||
width: 60px; font-size: 0.65rem; font-weight: 600;
|
||||
color: #94a3b8; shrink: 0;
|
||||
}
|
||||
.preview-block {
|
||||
height: 28px; border-radius: 6px;
|
||||
display: flex; align-items: center; padding: 0 8px;
|
||||
font-size: 0.6rem; font-weight: 600; color: white; white-space: nowrap; overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Features ────────────────────────────────── */
|
||||
section { padding: 5rem 1.5rem; }
|
||||
.section-title {
|
||||
text-align: center; margin-bottom: 3rem;
|
||||
}
|
||||
.section-title h2 {
|
||||
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||
font-weight: 800; margin-bottom: 0.75rem; letter-spacing: -0.02em;
|
||||
}
|
||||
.section-title p { color: var(--text-muted); font-size: 1.1rem; }
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem; max-width: 1100px; margin: 0 auto;
|
||||
}
|
||||
.feature-card {
|
||||
background: white; border: 1px solid var(--border);
|
||||
border-radius: 16px; padding: 1.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.feature-card:hover {
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
.feature-icon {
|
||||
width: 48px; height: 48px; border-radius: 12px;
|
||||
background: rgba(99,102,241,0.1);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.5rem; margin-bottom: 1rem;
|
||||
}
|
||||
.feature-card h3 {
|
||||
font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem;
|
||||
}
|
||||
.feature-card p { color: var(--text-muted); font-size: 0.9rem; line-height: 1.6; }
|
||||
|
||||
/* ── Coming soon ─────────────────────────────── */
|
||||
.coming-soon {
|
||||
background: linear-gradient(135deg, var(--brand) 0%, #7c3aed 100%);
|
||||
border-radius: 24px; max-width: 800px; margin: 0 auto;
|
||||
padding: 4rem 2rem; text-align: center; color: white;
|
||||
}
|
||||
.coming-soon h2 { font-size: 2rem; font-weight: 800; margin-bottom: 0.75rem; }
|
||||
.coming-soon p { opacity: 0.85; font-size: 1rem; margin-bottom: 2rem; }
|
||||
|
||||
.notify-form {
|
||||
display: flex; gap: 8px; max-width: 420px; margin: 0 auto; flex-wrap: wrap; justify-content: center;
|
||||
}
|
||||
.notify-form input {
|
||||
flex: 1; min-width: 220px;
|
||||
padding: 0.75rem 1rem; border-radius: 10px; border: none;
|
||||
font-size: 0.9rem; font-family: inherit;
|
||||
background: rgba(255,255,255,0.15); color: white;
|
||||
}
|
||||
.notify-form input::placeholder { color: rgba(255,255,255,0.6); }
|
||||
.notify-form input:focus { outline: 2px solid rgba(255,255,255,0.5); }
|
||||
.notify-form button {
|
||||
padding: 0.75rem 1.5rem; border-radius: 10px; border: none;
|
||||
background: white; color: var(--brand);
|
||||
font-size: 0.9rem; font-weight: 700; cursor: pointer;
|
||||
font-family: inherit; transition: all 0.15s;
|
||||
}
|
||||
.notify-form button:hover { background: #f1f5f9; transform: scale(1.02); }
|
||||
|
||||
/* ── Footer ──────────────────────────────────── */
|
||||
footer {
|
||||
text-align: center; padding: 2rem 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted); font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.preview-inner { grid-template-columns: 1fr; height: auto; }
|
||||
.preview-sidebar { display: none; }
|
||||
.hero-actions { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav>
|
||||
<a href="/" class="logo">
|
||||
<div class="logo-icon">H</div>
|
||||
Hotel<span>Sync</span>
|
||||
</a>
|
||||
<a href="https://app.hotelsync.ru" class="nav-cta">
|
||||
Войти в систему →
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="badge">
|
||||
<div class="badge-dot"></div>
|
||||
Скоро запуск · Уже в разработке
|
||||
</div>
|
||||
|
||||
<h1>
|
||||
Управляйте отелем<br/>
|
||||
как <em>профессионал</em>
|
||||
</h1>
|
||||
|
||||
<p class="hero-sub">
|
||||
HotelSync — современная PMS система с шахматкой бронирований,
|
||||
синхронизацией с Booking.com и Airbnb, управлением уборкой
|
||||
и полным API для интеграций.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a href="https://app.hotelsync.ru" class="btn-primary">
|
||||
✦ Открыть систему
|
||||
</a>
|
||||
<a href="#features" class="btn-secondary">
|
||||
Узнать больше
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- App preview -->
|
||||
<div class="preview-card">
|
||||
<div class="preview-topbar">
|
||||
<div class="preview-dot" style="background:#ff5f57"></div>
|
||||
<div class="preview-dot" style="background:#febc2e"></div>
|
||||
<div class="preview-dot" style="background:#28c840"></div>
|
||||
<div style="flex:1;height:22px;background:#f1f5f9;border-radius:6px;margin-left:8px"></div>
|
||||
</div>
|
||||
<div class="preview-inner">
|
||||
<div class="preview-sidebar">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:0.5rem 0.75rem;margin-bottom:12px">
|
||||
<div style="width:24px;height:24px;border-radius:6px;background:#4f46e5"></div>
|
||||
<span style="font-size:0.75rem;font-weight:700;color:#0f172a">HotelSync</span>
|
||||
</div>
|
||||
<div class="preview-sidebar-item active">
|
||||
<div class="preview-sidebar-icon"></div> Шахматка
|
||||
</div>
|
||||
<div class="preview-sidebar-item">
|
||||
<div class="preview-sidebar-icon"></div> Бронирования
|
||||
</div>
|
||||
<div class="preview-sidebar-item">
|
||||
<div class="preview-sidebar-icon"></div> Номера
|
||||
</div>
|
||||
<div class="preview-sidebar-item">
|
||||
<div class="preview-sidebar-icon"></div> Уборка
|
||||
</div>
|
||||
<div class="preview-sidebar-item">
|
||||
<div class="preview-sidebar-icon"></div> Каналы
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div style="display:flex;gap:6px;margin-bottom:12px">
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">101</div>
|
||||
<div class="preview-block" style="background:#4f46e5;width:22%">Волков Д.</div>
|
||||
<div style="flex:1"></div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">102</div>
|
||||
<div style="flex:0.3"></div>
|
||||
<div class="preview-block" style="background:#4f46e5;width:28%">Козлова А.</div>
|
||||
<div style="flex:1"></div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">201 ★</div>
|
||||
<div class="preview-block" style="background:#10b981;width:18%">Соколов</div>
|
||||
<div style="flex:0.2"></div>
|
||||
<div class="preview-block" style="background:#4f46e5;width:25%">Белова Т.</div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">301</div>
|
||||
<div class="preview-block" style="background:#10b981;width:35%">Александрова Н.</div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">401</div>
|
||||
<div style="flex:0.4"></div>
|
||||
<div class="preview-block" style="background:#4f46e5;width:30%">Орлов М.</div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">Пентхаус</div>
|
||||
<div style="flex:0.5"></div>
|
||||
<div class="preview-block" style="background:#f59e0b;width:38%">Громов В.</div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="preview-label">102A</div>
|
||||
<div class="preview-block" style="background:#10b981;width:42%">Лебедева Е.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section id="features" style="background:white">
|
||||
<div class="section-title">
|
||||
<h2>Всё что нужно отелю</h2>
|
||||
<p>Один инструмент вместо десяти</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📅</div>
|
||||
<h3>Шахматка бронирований</h3>
|
||||
<p>Наглядный календарь с номерами и датами. Перетаскивайте блоки, создавайте брони в один клик.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🌐</div>
|
||||
<h3>Менеджер каналов</h3>
|
||||
<p>Автоматическая синхронизация с Booking.com, Airbnb, Expedia и другими OTA-платформами.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✨</div>
|
||||
<h3>Управление уборкой</h3>
|
||||
<p>Канбан-доска для горничных. Задачи, статусы, приоритеты — всё в одном месте.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h3>Аналитика и отчёты</h3>
|
||||
<p>Загрузка отеля, RevPAR, ADR и другие ключевые метрики в реальном времени.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔌</div>
|
||||
<h3>REST API</h3>
|
||||
<p>Полное API для интеграций с вашими системами. Webhooks, документация, SDK.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🏨</div>
|
||||
<h3>Мультиотельность</h3>
|
||||
<p>Управляйте сетью отелей из одного аккаунта. Разные роли для каждого объекта.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Coming soon / notify -->
|
||||
<section>
|
||||
<div class="coming-soon">
|
||||
<h2>🚀 Скоро полноценный запуск</h2>
|
||||
<p>Оставьте email — сообщим первыми о старте и дадим скидку 30% на первый год</p>
|
||||
<form class="notify-form" onsubmit="handleSubmit(event)">
|
||||
<input type="email" placeholder="ваш@email.ru" required id="emailInput" />
|
||||
<button type="submit">Уведомить меня</button>
|
||||
</form>
|
||||
<p id="formSuccess" style="display:none;margin-top:1rem;font-weight:600;">✓ Отлично! Мы свяжемся с вами при запуске.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2026 HotelSync · SaaS PMS Platform · <a href="https://app.hotelsync.ru" style="color:var(--brand)">Войти в систему</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('emailInput').style.display = 'none';
|
||||
e.target.querySelector('button').style.display = 'none';
|
||||
document.getElementById('formSuccess').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
deploy/nginx/conf.d/hotelsync.conf
Normal file
67
deploy/nginx/conf.d/hotelsync.conf
Normal file
@@ -0,0 +1,67 @@
|
||||
# ── Редирект HTTP → HTTPS ─────────────────────────────────────────────────
|
||||
server {
|
||||
listen 80;
|
||||
server_name hotelsync.ru www.hotelsync.ru app.hotelsync.ru api.hotelsync.ru;
|
||||
|
||||
# Certbot challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# ── app.hotelsync.ru — Панель управления (React SPA) ─────────────────────
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name app.hotelsync.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/app.hotelsync.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/app.hotelsync.ru/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# ── hotelsync.ru — Лендинг (редирект на app пока нет лендинга) ───────────
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name hotelsync.ru www.hotelsync.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/hotelsync.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/hotelsync.ru/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
return 302 https://app.hotelsync.ru;
|
||||
}
|
||||
}
|
||||
|
||||
# ── api.hotelsync.ru — API Backend ────────────────────────────────────────
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.hotelsync.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/api.hotelsync.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/api.hotelsync.ru/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://api:3000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
32
deploy/nginx/nginx.conf
Normal file
32
deploy/nginx/nginx.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 20M;
|
||||
|
||||
# Логи
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/javascript
|
||||
text/xml application/xml text/javascript image/svg+xml;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
86
deploy/setup.sh
Normal file
86
deploy/setup.sh
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# HotelSync — первоначальная настройка сервера
|
||||
set -e
|
||||
|
||||
echo "🚀 HotelSync Server Setup"
|
||||
echo "========================="
|
||||
|
||||
# ── 1. Обновление системы ─────────────────────────────────────────────────
|
||||
echo "→ Обновление пакетов..."
|
||||
apt-get update -qq && apt-get upgrade -y -qq
|
||||
|
||||
# ── 2. Установка Docker ───────────────────────────────────────────────────
|
||||
echo "→ Установка Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
|
||||
# Docker Compose plugin
|
||||
apt-get install -y docker-compose-plugin
|
||||
|
||||
echo "Docker версия: $(docker --version)"
|
||||
echo "Docker Compose: $(docker compose version)"
|
||||
|
||||
# ── 3. Установка Git ──────────────────────────────────────────────────────
|
||||
echo "→ Установка Git..."
|
||||
apt-get install -y git
|
||||
|
||||
# ── 4. Настройка проекта ──────────────────────────────────────────────────
|
||||
echo "→ Создание директории проекта..."
|
||||
mkdir -p /opt/hotelsync
|
||||
cd /opt/hotelsync
|
||||
|
||||
# ── 5. Настройка Gitea (self-hosted Git) ──────────────────────────────────
|
||||
echo "→ Настройка Gitea..."
|
||||
mkdir -p /opt/gitea/data
|
||||
|
||||
cat > /opt/gitea/docker-compose.yml << 'EOF'
|
||||
version: '3'
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: hotelsync-gitea
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
- GITEA__server__DOMAIN=git.hotelsync.ru
|
||||
- GITEA__server__ROOT_URL=https://git.hotelsync.ru
|
||||
- GITEA__server__HTTP_PORT=3000
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "222:22"
|
||||
volumes:
|
||||
- /opt/gitea/data:/data
|
||||
EOF
|
||||
|
||||
docker compose -f /opt/gitea/docker-compose.yml up -d
|
||||
echo "✓ Gitea запущен на порту 3001"
|
||||
|
||||
# ── 6. Настройка bare git репозитория для деплоя ──────────────────────────
|
||||
echo "→ Настройка git deploy hook..."
|
||||
mkdir -p /opt/hotelsync/repo.git
|
||||
git init --bare /opt/hotelsync/repo.git
|
||||
|
||||
cat > /opt/hotelsync/repo.git/hooks/post-receive << 'HOOK'
|
||||
#!/bin/bash
|
||||
echo "→ Получен push, запускаем деплой..."
|
||||
cd /opt/hotelsync/app
|
||||
git --work-tree=/opt/hotelsync/app --git-dir=/opt/hotelsync/repo.git checkout -f main
|
||||
docker compose build frontend
|
||||
docker compose up -d --no-deps frontend
|
||||
echo "✓ Деплой завершён!"
|
||||
HOOK
|
||||
|
||||
chmod +x /opt/hotelsync/repo.git/hooks/post-receive
|
||||
mkdir -p /opt/hotelsync/app
|
||||
git init /opt/hotelsync/app
|
||||
echo "✓ Git deploy hook настроен"
|
||||
|
||||
echo ""
|
||||
echo "=============================="
|
||||
echo "✅ Базовая настройка завершена"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "Следующий шаг: запустить deploy/ssl.sh для SSL сертификатов"
|
||||
31
deploy/ssl.sh
Normal file
31
deploy/ssl.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Получение SSL сертификатов Let's Encrypt
|
||||
set -e
|
||||
|
||||
EMAIL="admin@hotelsync.ru"
|
||||
DOMAINS="hotelsync.ru,www.hotelsync.ru,app.hotelsync.ru,api.hotelsync.ru,git.hotelsync.ru"
|
||||
|
||||
echo "→ Получение SSL сертификатов..."
|
||||
|
||||
# Временный nginx для certbot challenge
|
||||
docker run --rm -p 80:80 -v /opt/hotelsync/certbot/www:/var/www/certbot nginx:alpine &
|
||||
NGINX_PID=$!
|
||||
sleep 2
|
||||
|
||||
for domain in hotelsync.ru app.hotelsync.ru api.hotelsync.ru git.hotelsync.ru; do
|
||||
docker run --rm \
|
||||
-v /opt/hotelsync/certbot/certs:/etc/letsencrypt \
|
||||
-v /opt/hotelsync/certbot/www:/var/www/certbot \
|
||||
certbot/certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email "$EMAIL" \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d "$domain" \
|
||||
$([ "$domain" = "hotelsync.ru" ] && echo "-d www.hotelsync.ru" || echo "")
|
||||
echo "✓ SSL для $domain получен"
|
||||
done
|
||||
|
||||
kill $NGINX_PID 2>/dev/null || true
|
||||
echo "✅ Все SSL сертификаты получены"
|
||||
Reference in New Issue
Block a user