документы + мейл сервер

This commit is contained in:
2026-07-27 15:42:54 +04:00
parent 9ed56ec93d
commit df0b051ee6
27 changed files with 1969 additions and 118 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
MOONWELL_OFFER_PUBLISHED=true
MOONWELL_PRIVACY_PUBLISHED=false
MOONWELL_PRIVACY_PUBLISHED=true
MOONWELL_RULES_PUBLISHED=true
MOONWELL_LEGAL_OPERATOR="Администрация проекта MoonWell"
MOONWELL_LEGAL_EMAIL="support@moon-well.online"
MOONWELL_LEGAL_EFFECTIVE_DATE="26 июля 2026 года"
+10
View File
@@ -0,0 +1,10 @@
# Public mail-server identity.
MAIL_HOSTNAME=mail.moon-well.online
MAIL_DOMAIN=moon-well.online
MAIL_PUBLIC_URL=https://mail.moon-well.online
# Public listeners. Leave 0.0.0.0 on a production mail host.
MAIL_BIND_IP=0.0.0.0
# The setup UI is bound to 127.0.0.1 only.
MAIL_SETUP_PORT=8081
+2 -1
View File
@@ -1,6 +1,7 @@
*.log
.DS_Store
.env
.env.mail
.env.backup
.env.production
.phpactor.json
@@ -22,4 +23,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
/public/appcast.xml
/public/appcast.xml
+7
View File
@@ -27,6 +27,13 @@ cp .env.example .env
Сайт будет доступен на `http://localhost:8080`.
## Почта техподдержки
Для собственного ящика `support@moon-well.online` подготовлен лёгкий
Stalwart-сервер с SMTP/IMAP, TLS, DKIM и антиспамом. Инструкция по первому
запуску, DNS и подключению Laravel находится в
[`docs/mail-server.md`](docs/mail-server.md).
## Деплой на Ubuntu
Скрипт деплоя лежит в `scripts/deploy-ubuntu.sh`. Он рассчитан на сервер, где уже установлены:
@@ -31,6 +31,7 @@ class LoginController extends Controller
'login_store' => route('login.store'),
'offer' => config('moonwell.legal.offer_published') ? route('legal.offer') : null,
'privacy' => config('moonwell.legal.privacy_published') ? route('legal.privacy') : null,
'rules' => config('moonwell.legal.rules_published') ? route('legal.rules') : null,
],
'flash' => [
'status' => $request->session()->get('status'),
@@ -126,6 +126,7 @@ class CabinetController extends Controller
'deposit' => route('cabinet.balance.deposit'),
'offer' => config('moonwell.legal.offer_published') ? route('legal.offer') : null,
'privacy' => config('moonwell.legal.privacy_published') ? route('legal.privacy') : null,
'rules' => config('moonwell.legal.rules_published') ? route('legal.rules') : null,
];
}
}
@@ -33,6 +33,7 @@ trait BuildsPortalProps
'logout' => route('logout'),
'offer' => config('moonwell.legal.offer_published') ? route('legal.offer') : null,
'privacy' => config('moonwell.legal.privacy_published') ? route('legal.privacy') : null,
'rules' => config('moonwell.legal.rules_published') ? route('legal.rules') : null,
'admin_accounts' => route('admin.accounts.index'),
'admin_balances' => route('admin.balances.index'),
'admin_shop' => route('admin.shop.index'),
@@ -84,8 +84,10 @@ class LandingController extends Controller
'legalUrls' => [
'offer_published' => (bool) config('moonwell.legal.offer_published'),
'privacy_published' => (bool) config('moonwell.legal.privacy_published'),
'rules_published' => (bool) config('moonwell.legal.rules_published'),
'offer' => config('moonwell.legal.offer_published') ? route('legal.offer') : null,
'privacy' => config('moonwell.legal.privacy_published') ? route('legal.privacy') : null,
'rules' => config('moonwell.legal.rules_published') ? route('legal.rules') : null,
],
'inviteRequired' => (bool) config('moonwell.registration.require_invite_code'),
'csrfToken' => csrf_token(),
+8
View File
@@ -22,6 +22,13 @@ class LegalController extends Controller
return Inertia::render('Legal/Privacy', $this->pageData($request));
}
public function rules(Request $request): Response
{
abort_unless(config('moonwell.legal.rules_published'), 404);
return Inertia::render('Legal/Rules', $this->pageData($request));
}
/**
* @return array<string, mixed>
*/
@@ -48,6 +55,7 @@ class LegalController extends Controller
'logout' => route('logout'),
'offer' => config('moonwell.legal.offer_published') ? route('legal.offer') : null,
'privacy' => config('moonwell.legal.privacy_published') ? route('legal.privacy') : null,
'rules' => config('moonwell.legal.rules_published') ? route('legal.rules') : null,
],
'legal' => [
'operator_name' => config('moonwell.legal.operator_name'),
+28
View File
@@ -0,0 +1,28 @@
services:
stalwart:
image: 'stalwartlabs/stalwart:v0.16'
restart: unless-stopped
profiles:
- mail
environment:
STALWART_PUBLIC_URL: '${MAIL_PUBLIC_URL:-https://mail.moon-well.online}'
ports:
- '${MAIL_BIND_IP:-0.0.0.0}:25:25'
- '${MAIL_BIND_IP:-0.0.0.0}:465:465'
- '${MAIL_BIND_IP:-0.0.0.0}:587:587'
- '${MAIL_BIND_IP:-0.0.0.0}:993:993'
# Initial setup UI. Keep it on loopback and use an SSH tunnel.
- '127.0.0.1:${MAIL_SETUP_PORT:-8081}:8080'
volumes:
- 'stalwart-config:/etc/stalwart'
- 'stalwart-data:/var/lib/stalwart'
networks:
default:
aliases:
- '${MAIL_HOSTNAME:-mail.moon-well.online}'
volumes:
stalwart-config:
driver: local
stalwart-data:
driver: local
+2 -1
View File
@@ -21,7 +21,8 @@ return [
'legal' => [
'offer_published' => (bool) env('MOONWELL_OFFER_PUBLISHED', true),
'privacy_published' => (bool) env('MOONWELL_PRIVACY_PUBLISHED', false),
'privacy_published' => (bool) env('MOONWELL_PRIVACY_PUBLISHED', true),
'rules_published' => (bool) env('MOONWELL_RULES_PUBLISHED', true),
'operator_name' => env('MOONWELL_LEGAL_OPERATOR', 'Администрация проекта MoonWell'),
'contact_email' => env('MOONWELL_LEGAL_EMAIL', 'support@moon-well.online'),
'effective_date' => env('MOONWELL_LEGAL_EFFECTIVE_DATE', '26 июля 2026 года'),
+136
View File
@@ -0,0 +1,136 @@
# Почта техподдержки
Для `support@moon-well.online` используется отдельный контейнер
[Stalwart Mail Server](https://stalw.art/). Он предоставляет SMTP, IMAP,
антиспам, DKIM и административный интерфейс. Данные и конфигурация хранятся
в именованных Docker volumes и переживают пересоздание контейнера.
## Что требуется от сервера
- постоянный публичный IPv4-адрес;
- возможность принимать TCP-соединения на порты `25`, `465`, `587`, `993`;
- разрешённый исходящий TCP-порт `25`;
- PTR/rDNS для IP со значением `mail.moon-well.online`;
- доступ к управлению DNS-зоной `moon-well.online`.
Без корректного PTR исходящие сообщения часто попадают в спам. Некоторые
хостинги блокируют исходящий порт 25 — это нужно проверить до настройки.
## Первый запуск
```bash
./scripts/mail-server.sh init
```
Проверь значения в `.env.mail`. Затем создай DNS-запись:
```text
mail.moon-well.online. A <публичный IPv4 сервера>
```
Запусти сервер:
```bash
./scripts/mail-server.sh up
./scripts/mail-server.sh credentials
```
Административный HTTP-порт намеренно доступен только на loopback-интерфейсе.
Открой туннель со своего компьютера:
```bash
ssh -L 8081:127.0.0.1:8081 <пользователь>@<сервер>
```
После этого открой `http://127.0.0.1:8081/admin` и войди с временными
реквизитами из команды `credentials`.
## Мастер Stalwart
Используй следующие значения:
- Server hostname: `mail.moon-well.online`
- Default email domain: `moon-well.online`
- Automatically obtain TLS certificate: включено
- Generate email signing keys: включено
- Storage: локальный RocksDB
- Directory: Internal Directory
- Logging: Console
Если DNS размещён у поддерживаемого Stalwart провайдера, подключи его в
последнем шаге. Тогда сертификат выпускается через DNS-01 без занятия
публичного порта 443. Иначе понадобится отдельно передать серверу TLS-сертификат
или временно предоставить ему порт 443 для ACME-проверки.
Сохрани постоянные реквизиты администратора, показанные мастером, и
перезапусти контейнер:
```bash
./scripts/mail-server.sh restart
```
Создай обычный пользовательский аккаунт `support@moon-well.online`.
Административный аккаунт нельзя использовать в почтовом клиенте.
## DNS после мастера
В панели Stalwart открой:
`Management → Domains → Domains → moon-well.online → View DNS Zone file`
Перенеси предложенные записи в DNS-панель. В обязательный минимум входят:
- MX;
- SPF;
- DKIM;
- DMARC;
- записи автоконфигурации, которые предложит Stalwart.
Значение DKIM нужно брать только из созданной установки: ключ генерируется
индивидуально. Не копируй DKIM из примеров.
## Настройка почтового клиента
```text
Адрес: support@moon-well.online
IMAP-сервер: mail.moon-well.online
IMAP-порт: 993
Шифрование IMAP: SSL/TLS
SMTP-сервер: mail.moon-well.online
SMTP-порт: 465
Шифрование SMTP: SSL/TLS
SMTP-аутентификация: включена
Логин: support@moon-well.online
```
## Подключение Laravel
После создания ящика укажи в production `.env`:
```dotenv
MAIL_MAILER=smtp
MAIL_SCHEME=smtps
MAIL_HOST=mail.moon-well.online
MAIL_PORT=465
MAIL_USERNAME=support@moon-well.online
MAIL_PASSWORD=<пароль ящика>
MAIL_FROM_ADDRESS=support@moon-well.online
MAIL_FROM_NAME="${APP_NAME}"
```
Затем сбрось и пересобери кэш конфигурации:
```bash
docker compose exec -T laravel.test php artisan optimize:clear
docker compose exec -T laravel.test php artisan config:cache
```
## Резервное копирование
В бэкап должны попадать оба именованных volume:
- `stalwart-config`;
- `stalwart-data`.
Перед восстановлением используй ту же minor-версию образа Stalwart, что указана
в `compose.mail.yaml`.
+21
View File
@@ -1861,6 +1861,19 @@ body[data-particles="false"] #particles { display: none; }
border-bottom: 2px solid var(--bg-0);
transform: rotate(45deg);
}
.join-check-content {
display: grid;
gap: 7px;
}
.join-legal-links {
display: flex;
flex-wrap: wrap;
gap: 7px 18px;
}
.join-legal-links a {
display: inline-flex;
align-items: center;
}
.join-check a { color: var(--moon); text-decoration: underline; text-decoration-color: rgba(127,184,212,0.3); }
.join-check a:hover { text-decoration-color: var(--moon); }
@@ -2859,6 +2872,14 @@ body[data-particles="false"] #particles { display: none; }
color: var(--moon);
}
.legal-document h2 { margin: 0 0 20px; font-size: clamp(27px, 4vw, 38px); }
.legal-document .legal-subheading {
margin: 30px 0 14px;
font-family: var(--f-serif);
font-size: clamp(20px, 2.5vw, 25px);
font-weight: 500;
line-height: 1.35;
color: var(--ivory);
}
.legal-document p,
.legal-document li {
font-family: var(--f-serif);
+5 -3
View File
@@ -3,7 +3,8 @@ defineProps({
eyebrow: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
effectiveDate: { type: String, required: true },
effectiveDate: { type: String, default: '' },
effectiveDateLabel: { type: String, default: 'Дата вступления в силу' },
intro: { type: Array, default: () => [] },
sections: { type: Array, required: true },
});
@@ -15,8 +16,8 @@ defineProps({
<span class="eyebrow">{{ eyebrow }}</span>
<h1 class="h-display">{{ title }}</h1>
<p class="lead">{{ description }}</p>
<div class="legal-effective">
<span>Дата вступления в силу</span>
<div v-if="effectiveDate" class="legal-effective">
<span>{{ effectiveDateLabel }}</span>
<strong>{{ effectiveDate }}</strong>
</div>
</header>
@@ -40,6 +41,7 @@ defineProps({
<template v-if="section.blocks">
<template v-for="(block, blockIndex) in section.blocks" :key="blockIndex">
<p v-if="block.type === 'paragraph'">{{ block.text }}</p>
<h3 v-else-if="block.type === 'subheading'" class="legal-subheading">{{ block.text }}</h3>
<ul v-else-if="block.type === 'list'">
<li v-for="item in block.items" :key="item">{{ item }}</li>
</ul>
+2 -1
View File
@@ -18,7 +18,8 @@ defineProps({
<nav aria-label="Юридическая информация">
<Link v-if="urls.news" :href="urls.news">Новости</Link>
<Link v-if="urls.offer" :href="urls.offer">Публичная оферта</Link>
<Link v-if="urls.privacy" :href="urls.privacy">Политика конфиденциальности</Link>
<Link v-if="urls.privacy" :href="urls.privacy">Политика обработки персональных данных</Link>
<Link v-if="urls.rules" :href="urls.rules">Правила проекта</Link>
</nav>
<span>© 2026 MoonWell · фанатский проект</span>
</div>
+4 -2
View File
@@ -54,7 +54,8 @@ onBeforeUnmount(() => {
</Link>
<Link v-if="auth.is_admin && urls.admin" :class="{ active: isAdmin }" :href="urls.admin">Админка</Link>
<Link v-if="urls.offer" :class="{ active: currentPath === '/public-offer' }" :href="urls.offer">Оферта</Link>
<Link v-if="urls.privacy" :class="{ active: currentPath === '/privacy-policy' }" :href="urls.privacy">Конфиденциальность</Link>
<Link v-if="urls.privacy" :class="{ active: currentPath === '/privacy-policy' }" :href="urls.privacy">Персональные данные</Link>
<Link v-if="urls.rules" :class="{ active: currentPath === '/project-rules' }" :href="urls.rules">Правила</Link>
</nav>
<div v-if="auth.authenticated" class="portal-account portal-nav--desktop">
@@ -85,7 +86,8 @@ onBeforeUnmount(() => {
</Link>
<Link v-if="auth.is_admin && urls.admin" :href="urls.admin" @click="menuOpen = false">Админка</Link>
<Link v-if="urls.offer" :href="urls.offer" @click="menuOpen = false">Публичная оферта</Link>
<Link v-if="urls.privacy" :href="urls.privacy" @click="menuOpen = false">Политика конфиденциальности</Link>
<Link v-if="urls.privacy" :href="urls.privacy" @click="menuOpen = false">Политика обработки персональных данных</Link>
<Link v-if="urls.rules" :href="urls.rules" @click="menuOpen = false">Правила проекта</Link>
<button v-if="auth.authenticated" type="button" @click="logout">Выйти из аккаунта</button>
</nav>
</div>
@@ -35,17 +35,19 @@ defineProps({
</div>
<p>Приватный сервер World of Warcraft 3.3.5a с личным прогрессом, одиночными подземельями и режимом «Предатель». Играй бесплатно покупки не дают боевого преимущества.</p>
</div>
<div class="footer-col"><h4>Сервер</h4><a href="#">Статус реалмов</a><a :href="playUrl">Скачать клиент</a><a v-if="legalUrls.offer_published" :href="legalUrls.offer">Публичная оферта</a><a v-if="legalUrls.privacy_published" :href="legalUrls.privacy">Конфиденциальность</a></div>
<div class="footer-col"><h4>Сервер</h4><a href="#">Статус реалмов</a><a :href="playUrl">Скачать клиент</a><a v-if="legalUrls.offer_published" :href="legalUrls.offer">Публичная оферта</a><a v-if="legalUrls.privacy_published" :href="legalUrls.privacy">Персональные данные</a><a v-if="legalUrls.rules_published" :href="legalUrls.rules">Правила проекта</a></div>
<div class="footer-col"><h4>Сообщество</h4><a href="#">Discord</a><a href="#">Форум</a><a href="#">Поддержка</a><a href="#">Вакансии</a></div>
<div class="footer-col"><h4>База знаний</h4><a href="#">Гайды</a><a href="#">База данных</a><a href="#">Рейтинги</a><a href="#">API</a></div>
</div>
<div class="footer-bottom">
<span>© 2026 MoonWell · фанатский проект · не связан с Blizzard Entertainment</span>
<span v-if="legalUrls.offer_published || legalUrls.privacy_published">
<span v-if="legalUrls.offer_published || legalUrls.privacy_published || legalUrls.rules_published">
<a v-if="legalUrls.offer_published" :href="legalUrls.offer">Оферта</a>
<template v-if="legalUrls.offer_published && legalUrls.privacy_published"> · </template>
<a v-if="legalUrls.privacy_published" :href="legalUrls.privacy">Политика конфиденциальности</a>
<template v-if="legalUrls.offer_published && (legalUrls.privacy_published || legalUrls.rules_published)"> · </template>
<a v-if="legalUrls.privacy_published" :href="legalUrls.privacy">Политика обработки персональных данных</a>
<template v-if="legalUrls.privacy_published && legalUrls.rules_published"> · </template>
<a v-if="legalUrls.rules_published" :href="legalUrls.rules">Правила проекта</a>
</span>
<span v-else>Сделано с лунным светом</span>
</div>
@@ -160,9 +160,13 @@ function submit(event) {
<label class="join-check">
<input type="checkbox" name="terms" value="1" :checked="form.terms" @change="updateField('terms', $event.target.checked)" />
<span class="box" aria-hidden="true"></span>
<span v-if="legalUrls.offer_published || legalUrls.privacy_published">
Я принимаю
<a v-if="legalUrls.offer_published" :href="legalUrls.offer" target="_blank" rel="noopener">публичную оферту</a><template v-if="legalUrls.offer_published && legalUrls.privacy_published"> и </template><a v-if="legalUrls.privacy_published" :href="legalUrls.privacy" target="_blank" rel="noopener">политику конфиденциальности</a>.
<span v-if="legalUrls.offer_published || legalUrls.privacy_published || legalUrls.rules_published" class="join-check-content">
<span>Я принимаю условия документов:</span>
<span class="join-legal-links">
<a v-if="legalUrls.offer_published" :href="legalUrls.offer" target="_blank" rel="noopener">Публичная оферта</a>
<a v-if="legalUrls.privacy_published" :href="legalUrls.privacy" target="_blank" rel="noopener">Политика обработки персональных данных</a>
<a v-if="legalUrls.rules_published" :href="legalUrls.rules" target="_blank" rel="noopener">Правила проекта</a>
</span>
</span>
<span v-else>Я принимаю правила проекта и согласен на обработку данных для создания игрового аккаунта.</span>
</label>
+13 -94
View File
@@ -1,117 +1,36 @@
<script setup>
import { Head } from '@inertiajs/vue3';
import { computed } from 'vue';
import LegalDocument from '../../Components/LegalDocument.vue';
import PortalLayout from '../../Layouts/PortalLayout.vue';
import {
privacyEffectiveDate,
privacySections,
privacySubtitle,
privacyTitle,
} from './privacyContent';
const props = defineProps({
defineProps({
site: { type: Object, required: true },
auth: { type: Object, required: true },
urls: { type: Object, required: true },
legal: { type: Object, required: true },
});
const sections = computed(() => [
{
id: 'scope',
title: 'Область действия',
paragraphs: [
`Настоящая политика описывает, как ${props.legal.operator_name} обрабатывает информацию пользователей сайта, личного кабинета, лаунчера и игрового сервера ${props.site.name}.`,
'Используя сервисы проекта или создавая аккаунт, Пользователь подтверждает, что ознакомился с этой политикой. Если Пользователь не согласен с условиями обработки, ему следует прекратить использование сервисов.',
],
},
{
id: 'data',
title: 'Какие данные мы обрабатываем',
paragraphs: [
'Состав данных зависит от используемых функций. Мы стремимся собирать только информацию, необходимую для работы проекта, безопасности и исполнения запросов Пользователя.',
],
items: [
'данные аккаунта: логин, email, пароль в виде криптографического проверочного значения, дата регистрации и последнего входа;',
'игровые данные: персонажи, прогресс, предметы, внутриигровые операции и дисциплинарный статус;',
'технические данные: IP-адрес, тип устройства и браузера, версия клиента, журналы авторизации и сведения об ошибках;',
'платёжные данные: сумма, валюта, статус и идентификатор операции; полные реквизиты банковских карт обрабатывает платёжный оператор;',
'данные обращений в поддержку и иная информация, которую Пользователь сообщает добровольно.',
],
},
{
id: 'purposes',
title: 'Цели и основания обработки',
paragraphs: [
'Данные используются для заключения и исполнения пользовательского соглашения, предоставления игровых функций, выполнения платежей, защиты проекта и пользователей, ответа на обращения и соблюдения требований закона.',
'В случаях, когда применимое законодательство требует согласия, обработка выполняется на основании согласия Пользователя. Отдельные данные могут обрабатываться для исполнения договора, выполнения юридической обязанности или защиты законных интересов проекта, если такие интересы не нарушают права Пользователя.',
],
},
{
id: 'cookies',
title: 'Cookies и аналитика',
paragraphs: [
'Сайт использует технические cookies и сведения сессии, необходимые для авторизации, защиты форм и сохранения настроек. Их отключение может сделать часть функций недоступной.',
'Также может использоваться веб-аналитика для оценки посещаемости и улучшения интерфейса. Аналитический сервис может получать обезличенные или псевдонимизированные технические сведения в соответствии со своими условиями.',
],
},
{
id: 'sharing',
title: 'Передача третьим лицам',
paragraphs: [
'Мы не продаём персональные данные. Информация передаётся только в объёме, необходимом для работы сервиса или исполнения требований закона.',
],
items: [
'платёжному оператору — для проведения платежа и обработки возврата;',
'поставщикам хостинга, хранения файлов, мониторинга и защиты инфраструктуры;',
'консультантам и подрядчикам, связанным обязанностью конфиденциальности;',
'государственным органам — при наличии законного и обязательного требования.',
],
},
{
id: 'storage',
title: 'Хранение и защита',
paragraphs: [
'Данные хранятся столько, сколько необходимо для указанных целей, разрешения споров, предотвращения злоупотреблений и выполнения юридических обязанностей. Конкретный срок зависит от категории данных и статуса аккаунта.',
'Мы применяем разумные организационные и технические меры: разграничение доступа, криптографическую защиту учётных данных, журналы безопасности и резервное копирование. При этом ни один способ передачи и хранения данных не может гарантировать абсолютную безопасность.',
],
},
{
id: 'rights',
title: 'Права пользователя',
paragraphs: [
'В пределах применимого законодательства Пользователь может запросить сведения об обработке, исправление неточных данных, удаление или ограничение обработки, отозвать согласие и возразить против отдельных способов обработки.',
`Для реализации прав направьте запрос на ${props.legal.contact_email}. Мы можем запросить подтверждение, необходимое для защиты аккаунта от несанкционированного доступа. Отзыв согласия не влияет на законность обработки, выполненной до его отзыва.`,
],
},
{
id: 'children',
title: 'Несовершеннолетние',
paragraphs: [
'Сервисы не предназначены для самостоятельного совершения платежей лицами, которые по применимому законодательству не вправе заключать такие соглашения без участия законного представителя.',
'Если законный представитель считает, что данные несовершеннолетнего были предоставлены без надлежащего согласия, он может обратиться по контактному адресу для проверки и удаления данных.',
],
},
{
id: 'updates',
title: 'Изменения и контакты',
paragraphs: [
'Политика может обновляться при изменении сервисов или требований законодательства. Актуальная редакция всегда размещается на этой странице с новой датой вступления в силу.',
`Оператор обработки: ${props.legal.operator_name}. Вопросы о конфиденциальности и запросы Пользователей принимаются по адресу ${props.legal.contact_email}.`,
],
note: 'Не отправляйте в обращении пароль, полные реквизиты банковской карты или иные секретные данные.',
},
]);
</script>
<template>
<Head>
<title>Политика конфиденциальности {{ site.name }}</title>
<title>Политика обработки персональных данных {{ site.name }}</title>
<meta name="description" content="Политика обработки и защиты данных пользователей MoonWell." />
</Head>
<PortalLayout :site="site" :auth="auth" :urls="urls">
<LegalDocument
eyebrow="Защита данных"
title="Политика конфиденциальности"
description="Какие данные использует MoonWell, зачем они нужны и как пользователь может управлять ими."
:effective-date="legal.effective_date"
:sections="sections"
:title="privacyTitle"
:description="privacySubtitle"
:effective-date="privacyEffectiveDate"
effective-date-label="Редакция от"
:sections="privacySections"
/>
</PortalLayout>
</template>
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import { Head } from '@inertiajs/vue3';
import LegalDocument from '../../Components/LegalDocument.vue';
import PortalLayout from '../../Layouts/PortalLayout.vue';
import {
rulesSections,
rulesSubtitle,
rulesTitle,
} from './rulesContent';
defineProps({
site: { type: Object, required: true },
auth: { type: Object, required: true },
urls: { type: Object, required: true },
legal: { type: Object, required: true },
});
</script>
<template>
<Head>
<title>Правила проекта {{ site.name }}</title>
<meta name="description" content="Запрещённые действия, меры ответственности и порядок обжалования санкций в проекте MoonWell." />
</Head>
<PortalLayout :site="site" :auth="auth" :urls="urls">
<LegalDocument
eyebrow="Правила MoonWell"
:title="rulesTitle"
:description="rulesSubtitle"
:sections="rulesSections"
/>
</PortalLayout>
</template>
+3 -3
View File
@@ -642,7 +642,7 @@ export const offerSections = [
},
{
"type": "paragraph",
"text": "sindoring@yandex.ru"
"text": "support@moon-well.online"
},
{
"type": "paragraph",
@@ -1197,7 +1197,7 @@ export const offerSections = [
},
{
"type": "paragraph",
"text": "sindoring@yandex.ru"
"text": "support@moon-well.online"
},
{
"type": "paragraph",
@@ -1263,7 +1263,7 @@ export const offerSections = [
},
{
"type": "paragraph",
"text": "Электронная почта: sindoring@yandex.ru"
"text": "Электронная почта: support@moon-well.online"
}
]
}
File diff suppressed because it is too large Load Diff
+391
View File
@@ -0,0 +1,391 @@
export const rulesTitle = "Правила проекта";
export const rulesSubtitle = "Запрещённые действия и санкции";
export const rulesSections = [
{
"id": "section-1",
"title": "Общие положения",
"blocks": [
{
"type": "paragraph",
"text": "1.1. Пользователь обязан соблюдать законодательство Российской Федерации, настоящие Правила, условия Публичной оферты и иные обязательные правила проекта MoonWell."
},
{
"type": "paragraph",
"text": "1.2. Запрещёнными признаются действия, совершённые с использованием сайта, игрового сервера, Учётной записи, внутриигрового чата, личных сообщений, официальных сообществ и иных сервисов MoonWell."
},
{
"type": "paragraph",
"text": "1.3. Администрация вправе применять предусмотренные настоящими Правилами меры к Пользователю, если имеются достаточные основания полагать, что его действия:"
},
{
"type": "list",
"items": [
"нарушают законодательство Российской Федерации;",
"нарушают настоящие Правила;",
"создают угрозу безопасности Проекта или других Пользователей;",
"нарушают нормальную работу игрового сервера;",
"позволяют получить несправедливое игровое преимущество;",
"создают для Проекта обоснованный правовой или технический риск."
]
},
{
"type": "paragraph",
"text": "1.4. Применение внутренних санкций не является признанием Пользователя виновным в совершении преступления или административного правонарушения и не заменяет решения суда либо иного уполномоченного государственного органа."
}
]
},
{
"id": "section-2",
"title": "Нарушения законодательства",
"blocks": [
{
"type": "paragraph",
"text": "2.1. Запрещается использовать сервисы MoonWell для совершения, организации, подготовки, поощрения или распространения информации о действиях, запрещённых законодательством Российской Федерации."
},
{
"type": "paragraph",
"text": "2.2. В частности, запрещаются:"
},
{
"type": "list",
"items": [
"публичные призывы к совершению противоправных действий;",
"угрозы убийством, причинением вреда здоровью или имуществу;",
"мошенничество, вымогательство и попытки хищения Учётных записей, денежных средств или цифровых объектов;",
"распространение заведомо запрещённой законом информации;",
"размещение персональных данных другого лица без законного основания;",
"распространение вредоносных программ, фишинговых ссылок и иных средств несанкционированного доступа;",
"продажа, покупка или распространение материалов и объектов, оборот которых запрещён законодательством Российской Федерации;",
"использование Проекта для иных противоправных целей."
]
},
{
"type": "paragraph",
"text": "2.3. Администрация вправе удалить или ограничить доступ к соответствующим материалам, временно ограничить Учётную запись либо заблокировать её без предварительного предупреждения, если характер нарушения требует незамедлительного реагирования."
}
]
},
{
"id": "section-3",
"title": "Экстремизм и запрещённые материалы",
"blocks": [
{
"type": "paragraph",
"text": "3.1. На сайте, игровом сервере и иных официальных ресурсах MoonWell запрещаются экстремистские материалы и действия, относимые законодательством Российской Федерации к экстремистской деятельности."
},
{
"type": "paragraph",
"text": "3.2. В частности, запрещаются:"
},
{
"type": "list",
"items": [
"публичные призывы к осуществлению экстремистской или террористической деятельности;",
"публичное оправдание или пропаганда терроризма;",
"пропаганда исключительности, превосходства либо неполноценности человека или группы лиц по признакам, указанным в законодательстве Российской Федерации;",
"возбуждение социальной, расовой, национальной или религиозной розни;",
"призывы к насилию в отношении лиц или групп лиц по соответствующим признакам;",
"распространение материалов, включённых в федеральный список экстремистских материалов;",
"пропаганда или запрещённая законом публичная демонстрация нацистской атрибутики, символики экстремистских организаций либо сходной с ними до степени смешения символики;",
"использование завуалированных формулировок, изображений, кодовых обозначений или ссылок с целью обхода настоящего запрета."
]
},
{
"type": "paragraph",
"text": "3.3. Критика, обсуждение исторических событий, произведений искусства, религии, политики или общественных явлений сами по себе не считаются нарушением настоящего раздела, если содержание не содержит запрещённых законом призывов, пропаганды, оправдания насилия или иных признаков противоправного материала."
},
{
"type": "paragraph",
"text": "3.4. Указание Пользователем на то, что сообщение было шуткой, цитатой, ролевым отыгрышем, мемом или провокацией, не исключает применение санкций, если содержание и контекст сообщения объективно создают нарушение настоящих Правил или правовой риск для Проекта."
}
]
},
{
"id": "section-4",
"title": "Читы и недобросовестная игра",
"blocks": [
{
"type": "paragraph",
"text": "4.1. Запрещается использовать программы, устройства, модификации, скрипты, технические средства или способы игры, которые предоставляют Пользователю преимущество, не предусмотренное обычной функциональностью Проекта."
},
{
"type": "paragraph",
"text": "4.2. К запрещённым действиям относятся, в частности:"
},
{
"type": "list",
"items": [
"использование читов, ботов и программ автоматизации игрового процесса;",
"автоматическое выполнение игровых действий без непосредственного участия Пользователя;",
"изменение игрового клиента, памяти процесса или передаваемых сетевых данных с целью получения преимущества;",
"подмена, перехват, повторная отправка или искусственное формирование сетевых пакетов;",
"обход установленных Проектом технических ограничений;",
"использование программ для изменения скорости, координат, характеристик, времени выполнения действий или иных игровых параметров;",
"использование ошибок и уязвимостей игрового сервера, сайта, Личного кабинета или платёжной системы;",
"сокрытие известной Пользователю критической ошибки с целью её дальнейшего использования;",
"получение, передача или сохранение игровых ценностей, заведомо полученных вследствие чита, ошибки, взлома или другого нарушения;",
"помощь другому Пользователю в совершении перечисленных действий;",
"распространение, реклама, продажа или передача запрещённых программ и инструкций по их использованию."
]
},
{
"type": "paragraph",
"text": "4.3. Обнаружив ошибку или уязвимость, Пользователь обязан прекратить её использование и сообщить о ней Администрации."
},
{
"type": "paragraph",
"text": "4.4. Однократное случайное воспроизведение ошибки само по себе не является нарушением, если Пользователь прекратил её использование, не получил существенной выгоды и своевременно сообщил об ошибке Администрации."
},
{
"type": "paragraph",
"text": "4.5. Администрация вправе отменить игровые результаты, восстановить состояние персонажа или Учётной записи, удалить неправомерно полученные предметы, валюту, достижения и иные игровые преимущества."
}
]
},
{
"id": "section-5",
"title": "Нарушение правил игрового процесса",
"blocks": [
{
"type": "paragraph",
"text": "5.1. Пользователь обязан соблюдать правила игрового процесса, опубликованные на сайте или иных официальных ресурсах MoonWell."
},
{
"type": "paragraph",
"text": "5.2. Запрещаются, в частности:"
},
{
"type": "list",
"items": [
"намеренное нарушение нормальной работы игровых механик;",
"препятствование другим Пользователям в использовании Проекта способами, не предусмотренными игровым процессом;",
"мошенничество при внутриигровом обмене;",
"выдача себя за представителя Администрации;",
"распространение заведомо ложной информации от имени Администрации;",
"обход ранее применённой блокировки;",
"передача Учётной записи для обхода санкций;",
"использование нескольких Учётных записей для обхода ограничений, голосований, акций или иных правил;",
"злоупотребление обращениями в службу поддержки;",
"подделка доказательств нарушения;",
"заведомо ложные жалобы на других Пользователей."
]
},
{
"type": "paragraph",
"text": "5.3. Конкретные правила отдельных игровых режимов, мероприятий, соревнований и акций могут устанавливаться в соответствующих описаниях. Участие в таком мероприятии означает обязанность соблюдать опубликованные для него правила."
}
]
},
{
"id": "section-6",
"title": "Виды санкций",
"blocks": [
{
"type": "paragraph",
"text": "6.1. В зависимости от характера, тяжести, повторности и последствий нарушения Администрация вправе применить одну или несколько следующих мер:"
},
{
"type": "list",
"items": [
"предупреждение;",
"удаление или редактирование запрещённого материала;",
"ограничение доступа к игровому или иному чату;",
"временное ограничение отдельных функций;",
"временную блокировку Учётной записи;",
"постоянную блокировку Учётной записи;",
"удаление неправомерно полученных игровых объектов;",
"отмену результатов нарушения;",
"восстановление предыдущего состояния персонажа, баланса или игрового мира;",
"исключение из мероприятия, рейтинга или соревнования;",
"ограничение создания новых Учётных записей;",
"иные соразмерные технические меры, необходимые для прекращения нарушения и устранения его последствий."
]
},
{
"type": "paragraph",
"text": "6.2. При выборе санкции Администрация вправе учитывать:"
},
{
"type": "list",
"items": [
"характер и тяжесть нарушения;",
"наличие умысла;",
"продолжительность нарушения;",
"полученную Пользователем выгоду;",
"вред Проекту и другим Пользователям;",
"попытки скрыть нарушение;",
"добровольное сообщение об ошибке;",
"содействие в устранении последствий;",
"наличие предыдущих нарушений;",
"возможность устранить нарушение менее строгой мерой."
]
},
{
"type": "paragraph",
"text": "6.3. За грубое нарушение постоянная блокировка может применяться без предварительного предупреждения."
},
{
"type": "paragraph",
"text": "К грубым нарушениям могут относиться:"
},
{
"type": "list",
"items": [
"использование или распространение читов;",
"взлом и попытки несанкционированного доступа;",
"мошенничество;",
"распространение вредоносных программ;",
"угрозы причинения реального вреда;",
"экстремистские или террористические призывы;",
"распространение запрещённых материалов;",
"умышленное использование критической уязвимости;",
"обход ранее применённой постоянной блокировки."
]
},
{
"type": "paragraph",
"text": "6.4. Перечень санкций не означает, что они обязательно применяются последовательно от менее строгой к более строгой."
},
{
"type": "paragraph",
"text": "6.5. Санкция применяется только в пределах сервисов MoonWell. Администрация не назначает уголовное, административное или иное государственное наказание."
}
]
},
{
"id": "section-7",
"title": "Фиксация нарушения",
"blocks": [
{
"type": "paragraph",
"text": "7.1. При рассмотрении нарушения Администрация вправе использовать:"
},
{
"type": "list",
"items": [
"журналы игрового сервера;",
"историю действий Учётной записи;",
"технические журналы сайта и Личного кабинета;",
"данные античит-системы;",
"историю внутриигровых операций;",
"сообщения в чатах;",
"снимки экрана и видеозаписи;",
"обращения других Пользователей;",
"данные платёжных операций;",
"иные относящиеся к нарушению сведения."
]
},
{
"type": "paragraph",
"text": "7.2. Решение не должно основываться исключительно на неподтверждённом сообщении другого Пользователя, если нарушение может быть проверено техническими или иными объективными средствами."
},
{
"type": "paragraph",
"text": "7.3. Администрация не обязана раскрывать внутренние алгоритмы античит-системы, способы обнаружения нарушений, сведения о технической защите или данные других Пользователей, если раскрытие может создать угрозу безопасности, позволить обходить защиту либо нарушить права третьих лиц."
}
]
},
{
"id": "section-8",
"title": "Удаление материалов и взаимодействие с государственными органами",
"blocks": [
{
"type": "paragraph",
"text": "8.1. Администрация вправе без предварительного уведомления удалить, скрыть или ограничить распространение материала, который:"
},
{
"type": "list",
"items": [
"запрещён законодательством Российской Федерации;",
"нарушает настоящие Правила;",
"содержит вредоносную ссылку или программное обеспечение;",
"создаёт непосредственную угрозу Пользователям или Проекту;",
"должен быть ограничен на основании законного требования уполномоченного органа."
]
},
{
"type": "paragraph",
"text": "8.2. При наличии законного требования уполномоченного государственного органа Администрация вправе предоставить находящиеся у неё сведения в порядке и объёме, предусмотренных законодательством Российской Федерации."
},
{
"type": "paragraph",
"text": "8.3. При обнаружении признаков возможного тяжкого правонарушения, реальной угрозы жизни и здоровью либо подготовки противоправного действия Администрация вправе самостоятельно обратиться в уполномоченные государственные органы."
}
]
},
{
"id": "section-9",
"title": "Обжалование санкции",
"blocks": [
{
"type": "paragraph",
"text": "9.1. Пользователь вправе обжаловать применённую санкцию, направив обращение по адресу электронной почты:"
},
{
"type": "paragraph",
"text": "support@moon-well.online"
},
{
"type": "paragraph",
"text": "9.2. В обращении необходимо указать:"
},
{
"type": "list",
"items": [
"логин или идентификатор Учётной записи;",
"дату применения санкции;",
"известное Пользователю основание санкции;",
"причины несогласия;",
"относящиеся к ситуации доказательства."
]
},
{
"type": "paragraph",
"text": "9.3. Обращение рассматривается в течение пяти рабочих дней со дня его получения. При необходимости изучения большого объёма технических данных срок может быть продлён, о чём Пользователю направляется уведомление."
},
{
"type": "paragraph",
"text": "9.4. По результатам рассмотрения Администрация вправе:"
},
{
"type": "list",
"items": [
"оставить санкцию без изменения;",
"сократить срок санкции;",
"заменить санкцию более мягкой;",
"отменить санкцию;",
"восстановить ошибочно удалённые игровые объекты;",
"применить иную соразмерную меру."
]
},
{
"type": "paragraph",
"text": "9.5. Повторные обращения без новых обстоятельств или доказательств могут быть оставлены без повторного рассмотрения."
}
]
},
{
"id": "section-10",
"title": "Платные услуги и блокировка",
"blocks": [
{
"type": "paragraph",
"text": "10.1. Применение санкции не означает автоматического аннулирования неиспользованного Платного баланса Пользователя."
},
{
"type": "paragraph",
"text": "10.2. Вопросы возврата неиспользованной предоплаты регулируются Публичной офертой и законодательством Российской Федерации."
},
{
"type": "paragraph",
"text": "10.3. Игровые предметы, достижения, внутриигровые результаты и иные преимущества, уже предоставленные в рамках надлежащим образом оказанной цифровой услуги, могут быть удалены или отменены, если они использовались для нарушения Правил либо были получены вследствие такого нарушения."
},
{
"type": "paragraph",
"text": "10.4. Положения настоящего раздела не ограничивают обязательные права Пользователя как потребителя."
}
]
}
];
+1
View File
@@ -22,6 +22,7 @@ Route::get('/news/{news}', [NewsController::class, 'show'])
->name('news.show');
Route::get('/public-offer', [LegalController::class, 'offer'])->name('legal.offer');
Route::get('/privacy-policy', [LegalController::class, 'privacy'])->name('legal.privacy');
Route::get('/project-rules', [LegalController::class, 'rules'])->name('legal.rules');
Route::get('/launcher/login-news.txt', [WowLoginNewsFeedController::class, 'show'])->name('launcher.login-news.show');
Route::get('/appcast.xml', static fn () => Storage::disk('public')->get('appcast.xml'));
Route::post('/register', [LandingController::class, 'register'])
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
APP_ENV_FILE="${ROOT_DIR}/.env"
MAIL_ENV_FILE="${ROOT_DIR}/.env.mail"
MAIL_ENV_EXAMPLE="${ROOT_DIR}/.env.mail.example"
APP_COMPOSE_FILE="${ROOT_DIR}/compose.yaml"
MAIL_COMPOSE_FILE="${ROOT_DIR}/compose.mail.yaml"
MAIL_SERVICE="stalwart"
usage() {
cat <<'EOF'
Использование:
./scripts/mail-server.sh init
./scripts/mail-server.sh up
./scripts/mail-server.sh down
./scripts/mail-server.sh restart
./scripts/mail-server.sh status
./scripts/mail-server.sh logs
./scripts/mail-server.sh credentials
./scripts/mail-server.sh shell
Первый запуск:
1. ./scripts/mail-server.sh init
2. Проверь .env.mail
3. Создай A-запись mail.<домен> на IP сервера
4. ./scripts/mail-server.sh up
5. ./scripts/mail-server.sh credentials
6. Открой SSH-туннель к 127.0.0.1:8081 и заверши мастер настройки
EOF
}
fail() {
printf '[mail:error] %s\n' "$*" >&2
exit 1
}
log() {
printf '[mail] %s\n' "$*"
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Не найдена команда: $1"
}
mail_env_value() {
local key="$1"
grep -E "^${key}=" "$MAIL_ENV_FILE" | tail -n 1 | cut -d '=' -f 2- | sed -e 's/^"//' -e 's/"$//'
}
compose() {
local env_args=(--env-file "$MAIL_ENV_FILE")
if [[ -f "$APP_ENV_FILE" ]]; then
env_args=(--env-file "$APP_ENV_FILE" --env-file "$MAIL_ENV_FILE")
fi
docker compose \
"${env_args[@]}" \
-f "$APP_COMPOSE_FILE" \
-f "$MAIL_COMPOSE_FILE" \
--profile mail \
"$@"
}
init() {
if [[ -f "$MAIL_ENV_FILE" ]]; then
log "${MAIL_ENV_FILE} уже существует"
return
fi
cp "$MAIL_ENV_EXAMPLE" "$MAIL_ENV_FILE"
chmod 600 "$MAIL_ENV_FILE"
log "Создан ${MAIL_ENV_FILE}; проверь домен перед запуском"
}
validate_config() {
[[ -f "$MAIL_ENV_FILE" ]] || fail "Сначала выполни: ./scripts/mail-server.sh init"
local hostname
local domain
hostname="$(mail_env_value MAIL_HOSTNAME)"
domain="$(mail_env_value MAIL_DOMAIN)"
[[ -n "$hostname" ]] || fail "MAIL_HOSTNAME не задан в .env.mail"
[[ -n "$domain" ]] || fail "MAIL_DOMAIN не задан в .env.mail"
[[ "$hostname" == *."$domain" ]] || fail "MAIL_HOSTNAME должен принадлежать MAIL_DOMAIN"
}
show_credentials() {
compose logs "$MAIL_SERVICE" 2>&1 \
| sed -n '/bootstrap mode/,+20p' \
| tail -n 24
}
main() {
require_command docker
docker compose version >/dev/null 2>&1 || fail "Не найден Docker Compose Plugin"
local action="${1:-}"
case "$action" in
init)
init
;;
up)
validate_config
compose up -d "$MAIL_SERVICE"
log "Stalwart запущен"
log "Временные реквизиты: ./scripts/mail-server.sh credentials"
;;
down)
validate_config
compose stop "$MAIL_SERVICE"
;;
restart)
validate_config
compose restart "$MAIL_SERVICE"
;;
status)
validate_config
compose ps "$MAIL_SERVICE"
;;
logs)
validate_config
compose logs --tail=200 -f "$MAIL_SERVICE"
;;
credentials)
validate_config
show_credentials
;;
shell)
validate_config
compose exec "$MAIL_SERVICE" /bin/sh
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"
@@ -19,18 +19,36 @@ class InertiaPublicPagesFeatureTest extends TestCase
);
}
public function test_offer_is_published_while_privacy_policy_stays_hidden(): void
public function test_legal_documents_are_published_by_default(): void
{
$this->get(route('legal.offer'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Legal/Offer', false)
->where('urls.offer', route('legal.offer'))
->where('urls.privacy', null)
->where('urls.privacy', route('legal.privacy'))
->etc()
);
$this->get(route('legal.privacy'))->assertNotFound();
$this->get(route('legal.privacy'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Legal/Privacy', false)
->where('urls.offer', route('legal.offer'))
->where('urls.privacy', route('legal.privacy'))
->where('urls.rules', route('legal.rules'))
->etc()
);
$this->get(route('legal.rules'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('Legal/Rules', false)
->where('urls.offer', route('legal.offer'))
->where('urls.privacy', route('legal.privacy'))
->where('urls.rules', route('legal.rules'))
->etc()
);
}
public function test_legal_pages_can_be_controlled_independently(): void
+4 -2
View File
@@ -18,9 +18,11 @@ class LandingPageTest extends TestCase
->component('Landing/Main', false)
->where('realm.server_name', 'MoonWell')
->where('legalUrls.offer_published', true)
->where('legalUrls.privacy_published', false)
->where('legalUrls.privacy_published', true)
->where('legalUrls.rules_published', true)
->where('legalUrls.offer', route('legal.offer'))
->where('legalUrls.privacy', null)
->where('legalUrls.privacy', route('legal.privacy'))
->where('legalUrls.rules', route('legal.rules'))
->etc()
);
}