commit 97109c233646b56eb7ca0bc73afcb3edb7fb3e71 Author: admin Date: Sun Apr 5 18:17:09 2026 +0700 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03599d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.github +.idea +.vscode +node_modules +vendor +storage/logs/* +ansible +*.md +.env +docker/app.env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3e8d0c0 --- /dev/null +++ b/.env.example @@ -0,0 +1,66 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=db +DB_PASSWORD= + +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=redis + +CACHE_STORE=file +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +# В Docker-сети: redis. Локально без Docker: 127.0.0.1 +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..443dbcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +docker/app.env + +# Ansible: инвентарь и секреты +ansible/inventory/hosts.ini +ansible/group_vars/laravel/vault.yml + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..601dc37 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,26 @@ +stages: + - deploy + +# Деплой на self-hosted runner с тегом docker (см. Ansible: gitlab-runner). +deploy_production: + stage: deploy + image: alpine:3.19 + tags: + - docker + variables: + GIT_STRATEGY: none + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + before_script: + - apk add --no-cache openssh-client bash + - eval $(ssh-agent -s) + - echo "$CI_DEPLOY_SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh && chmod 700 ~/.ssh + - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true + script: + - | + ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "set -e + cd '${DEPLOY_PATH}' + git fetch origin + git reset --hard 'origin/${CI_DEFAULT_BRANCH}' + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --remove-orphans" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d1082f --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Laravel + Docker + Ansible (thin repo) + +Воспроизводимая инфраструктура: локально — Docker Compose; на сервере Debian 11 — Ansible (Docker, TLS, GitLab Runner); CI/CD — GitLab с self-hosted runner. + +## Структура репозитория + +| Путь | Назначение | +|------|------------| +| Корень | Исходный код Laravel | +| `docker/` | Образ PHP-FPM, Nginx, файл `app.env` (не секреты в примере) | +| `docker-compose.yml`, `docker-compose.prod.yml` | Сервисы приложения | +| `ansible/` | Плейбуки, роли, переменные | +| `.gitlab-ci.yml` | Pipeline деплоя при пуше в основную ветку | + +Секреты для Ansible храните в `ansible/group_vars/laravel/vault.yml` (рекомендуется `ansible-vault`). Пример без шифрования — `vault.yml.example`. + +Конфигурация приложения в Docker задаётся **файлами** (`docker/app.env`), а не переменными окружения хоста. Для GitLab CI целевой сервер и путь к проекту задаются **переменными в интерфейсе GitLab** (см. ниже) — это не конфигурация приложения. + +--- + +## Часть 1. Локальный запуск + +### Требования + +- Docker и Docker Compose v2 + +### Подготовка + +1. Скопируйте пример конфигурации: + + ```bash + cp docker/app.env.example docker/app.env + ``` + +2. При необходимости отредактируйте `docker/app.env` (URL, пароли MySQL для локальной разработки). + +### Запуск + +```bash +docker compose up -d --build +``` + +- Приложение: `http://localhost:8080` (порт меняется через `HTTP_PORT` в `docker-compose.yml`, по умолчанию `8080`). +- phpMyAdmin: `http://localhost:8080/pma/` — вход пользователем БД `db` и паролем из `docker/app.env` (`MYSQL_PASSWORD`). + +Первый запуск PHP-контейнера выполнит `composer install` (зависимости в именованном томе `laravel_vendor`), сгенерирует `APP_KEY` при необходимости и применит миграции. Код монтируется с хоста — правки PHP/Blade видны без пересборки образа. + +### Полезные команды + +```bash +docker compose logs -f php +docker compose exec php php artisan migrate:status +``` + +--- + +## Часть 2. GitLab CI/CD + +### Self-hosted Runner + +Runner ставится Ansible-ролью `gitlab_runner` (Docker executor, тег `docker`, доступ к `docker.sock`). + +Регистрация использует токен из **Ansible Vault** (`vault_gitlab_runner_token`). + +### Pipeline + +Файл `.gitlab-ci.yml`: при пуше в **основную ветку** (`CI_DEFAULT_BRANCH`) job `deploy_production` подключается по SSH к серверу и выполняет `git fetch` / `git reset --hard` и `docker compose ... up -d --build`. + +### Переменные в GitLab (Settings → CI/CD → Variables) + +| Переменная | Маскировать | Описание | +|------------|-------------|----------| +| `CI_DEPLOY_SSH_PRIVATE_KEY` | да | Приватный ключ для пользователя деплоя на сервере | +| `DEPLOY_HOST` | нет | Хост или IP сервера | +| `DEPLOY_USER` | нет | Обычно `deploy` | +| `DEPLOY_PATH` | нет | Каталог клона репозитория на сервере (как в Ansible `project_path`) | + +На сервере в `authorized_keys` пользователя деплоя должен быть **публичный** ключ, соответствующий этому приватному ключу. Удобно добавить его в Ansible в `deploy_authorized_keys` в `ansible/group_vars/laravel/main.yml`. + +### Доступ Git на сервере + +Для приватного репозитория на сервере нужен доступ к GitLab (Deploy Key или `vault_git_repo_url` с токеном при первом клоне). После клона можно переключить `origin` на SSH и использовать deploy key. + +--- + +## Часть 3. Ansible (Debian 11) + +### Подготовка + +1. Установите Ansible на управляющей машине (например `pip install ansible` или пакет дистрибутива). + +2. Скопируйте инвентарь и vault: + + ```bash + cd ansible + cp inventory/hosts.ini.example inventory/hosts.ini + cp group_vars/laravel/vault.yml.example group_vars/laravel/vault.yml + ``` + +3. В `inventory/hosts.ini` укажите `ansible_host` и пользователя SSH (часто `root` для первого захода). + +4. В `group_vars/laravel/main.yml` задайте: + + - `project_domain` — домен для Let's Encrypt и `APP_URL`; + - `letsencrypt_email` — почта для ACME; + - `git_repo_url_public` — URL публичного репозитория **или** оставьте пустым и задайте приватный URL в vault; + - `deploy_authorized_keys` — список SSH-публичных ключей для пользователя `deploy`. + +5. Заполните `group_vars/laravel/vault.yml` (минимум токен регистрации Runner и пароли БД, см. комментарии в `vault.yml.example`). Рекомендуется шифрование: + + ```bash + ansible-vault encrypt group_vars/laravel/vault.yml + ``` + +6. Сгенерируйте `vault_app_key` (например после локального `php artisan key:generate --show`). + +### Запуск плейбука + +Из каталога `ansible`: + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/site.yml --ask-vault-pass +``` + +Если vault не шифруется: + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/site.yml +``` + +### Пропуск получения сертификата (нет DNS / тест) + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/site.yml --ask-vault-pass --skip-tags letsencrypt +``` + +Тег `letsencrypt` назначен задаче `certbot` в роли `nginx_letsencrypt`. + +### Что делает плейбук + +1. **common** — пользователь `deploy`, каталог проекта, `authorized_keys`. +2. **docker** — пакеты `docker.io` и `docker-compose-plugin`, группа `docker`. +3. **security** — UFW: 22, 80, 443. +4. **laravel_docker** — клон репозитория, шаблон `docker/app.env`, `docker compose up` (с `docker-compose.prod.yml`: Nginx приложения только на `127.0.0.1:8080`). +5. **nginx_letsencrypt** — системный Nginx как reverse proxy на `127.0.0.1:8080`, Certbot (Let's Encrypt), попытка включить `certbot.timer` (при отсутствии — обычно остаётся cron из пакета). +6. **gitlab_runner** — установка и однократная регистрация runner с тегом `docker`. + +После успешного выполнения приложение доступно по **HTTPS** на `project_domain`, phpMyAdmin — по `https://<домен>/pma/`. + +--- + +## Переменные: краткая таблица + +| Где | Что задать | +|-----|------------| +| `docker/app.env` (локально, из примера) | Параметры Laravel и MySQL для Compose | +| `ansible/group_vars/laravel/main.yml` | Домен, email LE, URL репозитория, ключи SSH | +| `ansible/group_vars/laravel/vault.yml` | Токен Runner, пароли MySQL, `APP_KEY`, опционально URL приватного Git | +| GitLab CI Variables | SSH-ключ, `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_PATH` | + +--- + +## Технические детали + +- **Очереди:** по умолчанию `QUEUE_CONNECTION=redis`, воркер — сервис `queue` в Compose. +- **Миграции:** выполняются entrypoint-скриптом PHP-контейнера при старте. +- **Тонкий репозиторий:** `vendor` не в Git — в Docker том `laravel_vendor` и установка через Composer внутри контейнера. diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..886eb41 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +retry_files_enabled = False +interpreter_python = auto_silent +collections_path = ./collections + +[privilege_escalation] +become = True diff --git a/ansible/group_vars/laravel/main.yml b/ansible/group_vars/laravel/main.yml new file mode 100644 index 0000000..03ec786 --- /dev/null +++ b/ansible/group_vars/laravel/main.yml @@ -0,0 +1,16 @@ +--- +# Публичные переменные плейбука (секреты — в vault.yml, см. vault.yml.example). +project_domain: app.example.com +letsencrypt_email: admin@example.com + +deploy_user: deploy +project_path: "/home/{{ deploy_user }}/laravel" + +# Репозиторий (публичный). Для приватного задайте vault_git_repo_url в vault.yml. +git_repo_url_public: "https://gitlab.com/yourgroup/yourproject.git" +git_branch: main + +gitlab_url: "https://gitlab.com/" + +# Публичные SSH-ключи для входа пользователя deploy (строки authorized_keys). +deploy_authorized_keys: [] diff --git a/ansible/group_vars/laravel/vault.yml.example b/ansible/group_vars/laravel/vault.yml.example new file mode 100644 index 0000000..47a2fee --- /dev/null +++ b/ansible/group_vars/laravel/vault.yml.example @@ -0,0 +1,20 @@ +--- +# Скопируйте в vault.yml и зашифруйте: ansible-vault encrypt group_vars/laravel/vault.yml +# ansible-vault edit group_vars/laravel/vault.yml + +# Обязательно: токен регистрации GitLab Runner (Settings → CI/CD → Runners). +vault_gitlab_runner_token: "glrt-xxxxxxxxxxxxxxxxxxxx" + +# Пароли БД и корня MySQL (должны совпадать с шаблоном docker/app.env). +vault_mysql_password: "сгенерируйте_надёжный_пароль" +vault_mysql_root_password: "сгенерируйте_надёжный_root" + +# Ключ приложения Laravel: сгенерируйте локально `php artisan key:generate --show` +vault_app_key: "base64:................................................" + +# Опционально: полный URL клона приватного репозитория (перекрывает git_repo_url_public). +# Пример: https://oauth2:glpat-xxx@gitlab.com/group/project.git +vault_git_repo_url: "" + +# Опционально: пароль для пользователя БД при отличии от vault_mysql_password (редко нужно). +vault_db_username: "db" diff --git a/ansible/inventory/hosts.ini.example b/ansible/inventory/hosts.ini.example new file mode 100644 index 0000000..b3026e6 --- /dev/null +++ b/ansible/inventory/hosts.ini.example @@ -0,0 +1,4 @@ +[laravel] +# Скопируйте этот файл в hosts.ini и укажите адрес чистого Debian 11. +# cp inventory/hosts.ini.example inventory/hosts.ini +debian11 ansible_host=203.0.113.10 ansible_user=root diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000..0667e68 --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,12 @@ +--- +# Полный разворот: Debian 11 → Docker, приложение, TLS, GitLab Runner. +- name: Laravel Docker инфраструктура + hosts: laravel + become: true + roles: + - role: common + - role: docker + - role: security + - role: laravel_docker + - role: nginx_letsencrypt + - role: gitlab_runner diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000..57d2ee3 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: Обновить кэш apt + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + +- name: Базовые пакеты + ansible.builtin.apt: + name: + - curl + - git + - acl + - ca-certificates + - gnupg + - software-properties-common + state: present + +- name: Пользователь деплоя + ansible.builtin.user: + name: "{{ deploy_user }}" + shell: /bin/bash + create_home: true + +- name: Каталог .ssh для {{ deploy_user }} + ansible.builtin.file: + path: "/home/{{ deploy_user }}/.ssh" + state: directory + owner: "{{ deploy_user }}" + group: "{{ deploy_user }}" + mode: "0700" + +- name: authorized_keys для {{ deploy_user }} + ansible.builtin.lineinfile: + path: "/home/{{ deploy_user }}/.ssh/authorized_keys" + line: "{{ item }}" + create: true + mode: "0600" + owner: "{{ deploy_user }}" + group: "{{ deploy_user }}" + loop: "{{ deploy_authorized_keys }}" + when: deploy_authorized_keys | length > 0 + +- name: Каталог проекта + ansible.builtin.file: + path: "{{ project_path }}" + state: directory + owner: "{{ deploy_user }}" + group: "{{ deploy_user }}" + mode: "0755" diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..d4dbffe --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Docker и плагин Compose (Debian) + ansible.builtin.apt: + name: + - docker.io + - docker-compose-plugin + state: present + +- name: Сервис Docker + ansible.builtin.service: + name: docker + state: started + enabled: true + +- name: Пользователь {{ deploy_user }} в группе docker + ansible.builtin.user: + name: "{{ deploy_user }}" + groups: docker + append: true diff --git a/ansible/roles/gitlab_runner/tasks/main.yml b/ansible/roles/gitlab_runner/tasks/main.yml new file mode 100644 index 0000000..60a6658 --- /dev/null +++ b/ansible/roles/gitlab_runner/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Скрипт репозитория GitLab Runner + ansible.builtin.get_url: + url: https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh + dest: /tmp/gitlab-runner-repo.sh + mode: "0755" + +- name: Подключить репозиторий GitLab Runner + ansible.builtin.command: bash /tmp/gitlab-runner-repo.sh + args: + creates: /etc/apt/sources.list.d/runner_gitlab-runner.list + +- name: Установить gitlab-runner + ansible.builtin.apt: + name: gitlab-runner + state: present + update_cache: true + +- name: Проверить наличие зарегистрированных runner + ansible.builtin.stat: + path: /etc/gitlab-runner/config.toml + register: gitlab_runner_cfg + +- name: Зарегистрировать GitLab Runner (docker executor + docker.sock) + ansible.builtin.shell: | + set -e + gitlab-runner register \ + --non-interactive \ + --url "{{ gitlab_url }}" \ + --token "{{ vault_gitlab_runner_token }}" \ + --executor "docker" \ + --docker-image "docker:24-cli" \ + --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \ + --description "debian-ansible" \ + --tag-list "docker" \ + --run-untagged=false \ + --locked=false + when: not gitlab_runner_cfg.stat.exists + no_log: true diff --git a/ansible/roles/laravel_docker/tasks/main.yml b/ansible/roles/laravel_docker/tasks/main.yml new file mode 100644 index 0000000..5c77efe --- /dev/null +++ b/ansible/roles/laravel_docker/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Эффективный URL Git + ansible.builtin.set_fact: + effective_git_repo_url: "{{ (vault_git_repo_url | default('') | length > 0) | ternary(vault_git_repo_url, git_repo_url_public) }}" + +- name: Клонирование приложения + ansible.builtin.git: + repo: "{{ effective_git_repo_url }}" + dest: "{{ project_path }}" + version: "{{ git_branch }}" + force: false + accept_hostkey: true + become_user: "{{ deploy_user }}" + +- name: Конфигурация docker/app.env для Laravel и Compose + ansible.builtin.template: + src: docker.app.env.j2 + dest: "{{ project_path }}/docker/app.env" + owner: "{{ deploy_user }}" + group: "{{ deploy_user }}" + mode: "0600" + +- name: Запуск стека Docker Compose + ansible.builtin.shell: | + set -e + cd {{ project_path }} + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --remove-orphans + become_user: "{{ deploy_user }}" + args: + executable: /bin/bash diff --git a/ansible/roles/laravel_docker/templates/docker.app.env.j2 b/ansible/roles/laravel_docker/templates/docker.app.env.j2 new file mode 100644 index 0000000..ac6cd71 --- /dev/null +++ b/ansible/roles/laravel_docker/templates/docker.app.env.j2 @@ -0,0 +1,37 @@ +# Сгенерировано Ansible — не редактируйте вручную на сервере (обновляйте vault и плейбук). + +MYSQL_DATABASE=laravel +MYSQL_USER={{ vault_db_username | default('db') }} +MYSQL_PASSWORD={{ vault_mysql_password }} +MYSQL_ROOT_PASSWORD={{ vault_mysql_root_password }} + +APP_NAME=Laravel +APP_ENV=production +APP_KEY={{ vault_app_key }} +APP_DEBUG=false +APP_URL=https://{{ project_domain }} + +LOG_CHANNEL=stack +LOG_LEVEL=warning + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME={{ vault_db_username | default('db') }} +DB_PASSWORD={{ vault_mysql_password }} + +SESSION_DRIVER=file +CACHE_STORE=file +FILESYSTEM_DISK=local + +QUEUE_CONNECTION=redis + +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log + +PMA_ABSOLUTE_URI=https://{{ project_domain }}/pma/ diff --git a/ansible/roles/nginx_letsencrypt/tasks/main.yml b/ansible/roles/nginx_letsencrypt/tasks/main.yml new file mode 100644 index 0000000..77223db --- /dev/null +++ b/ansible/roles/nginx_letsencrypt/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: Nginx и certbot + ansible.builtin.apt: + name: + - nginx + - certbot + - python3-certbot-nginx + state: present + update_cache: true + +- name: Виртуальный хост (HTTP → Docker) + ansible.builtin.template: + src: laravel.nginx.conf.j2 + dest: /etc/nginx/sites-available/laravel.conf + mode: "0644" + +- name: Включить сайт + ansible.builtin.file: + src: /etc/nginx/sites-available/laravel.conf + dest: /etc/nginx/sites-enabled/laravel.conf + state: link + force: true + +- name: Отключить default сайт + ansible.builtin.file: + path: /etc/nginx/sites-enabled/default + state: absent + +- name: Проверить и перезагрузить nginx + ansible.builtin.command: nginx -t + changed_when: false + +- name: Перезапуск nginx + ansible.builtin.service: + name: nginx + state: reloaded + +- name: Получить сертификат Let's Encrypt + ansible.builtin.command: > + certbot --nginx + -d {{ project_domain }} + --non-interactive + --agree-tos + -m {{ letsencrypt_email }} + --redirect + args: + creates: /etc/letsencrypt/live/{{ project_domain }}/fullchain.pem + tags: + - letsencrypt + +- name: Автообновление сертификатов (timer или cron от пакета certbot) + ansible.builtin.service: + name: certbot.timer + state: started + enabled: true + ignore_errors: true diff --git a/ansible/roles/nginx_letsencrypt/templates/laravel.nginx.conf.j2 b/ansible/roles/nginx_letsencrypt/templates/laravel.nginx.conf.j2 new file mode 100644 index 0000000..455b3f8 --- /dev/null +++ b/ansible/roles/nginx_letsencrypt/templates/laravel.nginx.conf.j2 @@ -0,0 +1,15 @@ +server { + listen 80; + listen [::]:80; + server_name {{ project_domain }}; + + location / { + proxy_pass http://127.0.0.1:8080; + 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; + proxy_read_timeout 120s; + } +} diff --git a/ansible/roles/security/tasks/main.yml b/ansible/roles/security/tasks/main.yml new file mode 100644 index 0000000..e0ed1be --- /dev/null +++ b/ansible/roles/security/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Пакет ufw + ansible.builtin.apt: + name: ufw + state: present + +- name: Разрешить входящие порты + ansible.builtin.command: ufw allow {{ item }}/tcp + loop: + - "22" + - "80" + - "443" + register: ufw_allow + changed_when: "'added' in ufw_allow.stdout | default('') | lower or 'updated' in ufw_allow.stdout | default('') | lower" + failed_when: false + +- name: Политика по умолчанию — запрет входящих + ansible.builtin.command: ufw default deny incoming + changed_when: false + +- name: Включить ufw + ansible.builtin.command: ufw --force enable + register: ufw_en + changed_when: "'Firewall is active' in ufw_en.stdout | default('') or ufw_en.rc == 0" + failed_when: false diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // За reverse proxy (Nginx + Docker) корректно определять HTTPS и клиентский IP. + $middleware->trustProxies('*'); + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..38b258d --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,5 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..0ba5d5d --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..925f7d2 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..8910562 --- /dev/null +++ b/config/database.php @@ -0,0 +1,174 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..3d671bd --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..8d94292 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..756305b --- /dev/null +++ b/config/mail.php @@ -0,0 +1,116 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..3013e93 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,112 @@ + env('QUEUE_CONNECTION', 'redis'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..27a3617 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..ba0aa60 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..d01a0ef --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,23 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e974d3f --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,5 @@ +# Продакшен: слушать только localhost — TLS терминирует системный Nginx (Ansible). +services: + nginx: + ports: + - "127.0.0.1:8080:80" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7e0d83 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,103 @@ +services: + nginx: + image: nginx:1.27-alpine + ports: + - "${HTTP_PORT:-8080}:80" + volumes: + - .:/var/www/html:ro + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + php: + condition: service_started + phpmyadmin: + condition: service_started + networks: + - app + + php: + build: + context: . + dockerfile: docker/php/Dockerfile + volumes: + - .:/var/www/html + - laravel_vendor:/var/www/html/vendor + env_file: + - docker/app.env + environment: + APP_ENV: "${APP_ENV:-local}" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started + networks: + - app + + queue: + build: + context: . + dockerfile: docker/php/Dockerfile + command: > + php artisan queue:work redis + --sleep=3 + --tries=3 + --max-time=3600 + volumes: + - .:/var/www/html + - laravel_vendor:/var/www/html/vendor + env_file: + - docker/app.env + depends_on: + php: + condition: service_started + mysql: + condition: service_healthy + redis: + condition: service_started + networks: + - app + restart: unless-stopped + + mysql: + image: mysql:8.0 + env_file: + - docker/app.env + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"] + interval: 5s + timeout: 5s + retries: 15 + start_period: 30s + networks: + - app + + redis: + image: redis:7-alpine + command: redis-server --appendonly yes + volumes: + - redis_data:/data + networks: + - app + + phpmyadmin: + image: phpmyadmin:5-apache + env_file: + - docker/app.env + environment: + PMA_HOST: mysql + depends_on: + mysql: + condition: service_healthy + networks: + - app + +volumes: + laravel_vendor: + mysql_data: + redis_data: + +networks: + app: + driver: bridge diff --git a/docker/app.env.example b/docker/app.env.example new file mode 100644 index 0000000..569b988 --- /dev/null +++ b/docker/app.env.example @@ -0,0 +1,39 @@ +# Скопируйте в docker/app.env и при необходимости измените значения. +# Секреты для продакшена задаются через Ansible Vault, не коммитьте docker/app.env. + +MYSQL_DATABASE=laravel +MYSQL_USER=db +MYSQL_PASSWORD=secret +MYSQL_ROOT_PASSWORD=rootsecret + +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost:8080 + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=db +DB_PASSWORD=secret + +SESSION_DRIVER=file +CACHE_STORE=file +FILESYSTEM_DISK=local + +QUEUE_CONNECTION=redis + +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PASSWORD= +REDIS_PORT=6379 + +MAIL_MAILER=log + +# Для phpMyAdmin (ссылки в интерфейсе) +PMA_ABSOLUTE_URI=http://localhost:8080/pma/ diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..5e0cdb3 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + root /var/www/html/public; + index index.php; + + client_max_body_size 64M; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # phpMyAdmin за единым входом Nginx приложения + location /pma/ { + 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; + proxy_pass http://phpmyadmin:80/; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_pass php:9000; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + fastcgi_read_timeout 120; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..32007a3 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,24 @@ +# PHP-FPM для Laravel с расширениями под MySQL и Redis. +FROM php:8.3-fpm-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + unzip \ + libzip-dev \ + libpng-dev \ + libonig-dev \ + libicu-dev \ + && docker-php-ext-install -j$(nproc) pdo_mysql zip intl opcache \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["php-fpm"] diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh new file mode 100644 index 0000000..6585364 --- /dev/null +++ b/docker/php/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -e + +cd /var/www/html + +# Синхронизация Laravel .env с файлом конфигурации Docker (настройки — в файлах, не в ENV хоста). +if [ -f docker/app.env ]; then + cp docker/app.env .env +elif [ ! -f .env ] && [ -f docker/app.env.example ]; then + cp docker/app.env.example .env +fi + +if [ ! -d vendor ] || [ ! -f vendor/autoload.php ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader +fi + +if [ -f artisan ]; then + mkdir -p storage/framework/{sessions,views,cache} storage/logs bootstrap/cache + chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true + php artisan config:clear 2>/dev/null || true + if ! grep -q '^APP_KEY=.\{10,\}' .env 2>/dev/null; then + php artisan key:generate --force --ansi || true + fi + php artisan migrate --force --ansi +fi + +exec "$@" diff --git a/package.json b/package.json new file mode 100644 index 0000000..e32a862 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "axios": "^1.7.4", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^1.2.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "vite": "^6.0.11" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..506b9a3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..b9d609c --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,176 @@ + + + + + + + Laravel + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + + + + diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..421b569 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + ], +});