Compare commits
5 Commits
fix/dokplo
...
feat/testi
| Author | SHA1 | Date | |
|---|---|---|---|
| ab9b59897a | |||
| 286522d8a2 | |||
| 3e68192cf3 | |||
| 1a31dd1cf6 | |||
| 9060036024 |
@@ -26,6 +26,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
$this->configureLivewireTemporaryUploads();
|
||||||
$this->freezeClockWhenConfigured();
|
$this->freezeClockWhenConfigured();
|
||||||
|
|
||||||
View::composer('layouts.public', function (ViewInstance $view): void {
|
View::composer('layouts.public', function (ViewInstance $view): void {
|
||||||
@@ -46,6 +47,22 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep Livewire/Filament temp uploads on the local disk.
|
||||||
|
*
|
||||||
|
* When FILESYSTEM_DISK=r2, Livewire would otherwise use the S3 driver and
|
||||||
|
* browser-PUT straight to R2 (CORS). Final media still uses the r2 disk via
|
||||||
|
* PublicImageUploadRules. Explicit LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK wins.
|
||||||
|
*/
|
||||||
|
private function configureLivewireTemporaryUploads(): void
|
||||||
|
{
|
||||||
|
if (filled(config('livewire.temporary_file_upload.disk'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config(['livewire.temporary_file_upload.disk' => 'local']);
|
||||||
|
}
|
||||||
|
|
||||||
private function freezeClockWhenConfigured(): void
|
private function freezeClockWhenConfigured(): void
|
||||||
{
|
{
|
||||||
if ($this->app->environment('production')) {
|
if ($this->app->environment('production')) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
@@ -23,7 +22,7 @@ class ContentSeeder extends Seeder
|
|||||||
$this->seedSiteSettings();
|
$this->seedSiteSettings();
|
||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->seedTestimonials();
|
$this->call(TestimonialsSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -170,67 +169,6 @@ class ContentSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedTestimonials(): void
|
|
||||||
{
|
|
||||||
// Real couples from depoimentos.md. Production publication still requires
|
|
||||||
// explicit couple authorization before setting published_at outside local/demo seeds.
|
|
||||||
$testimonials = [
|
|
||||||
[
|
|
||||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
|
||||||
'author_name' => 'Jeniffer e Maick',
|
|
||||||
'context' => 'Casamento · 06/12/2025',
|
|
||||||
'sort_order' => 1,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
|
||||||
'author_name' => 'Quesia e Jhonata',
|
|
||||||
'context' => 'Casamento · 21/12/2025',
|
|
||||||
'sort_order' => 2,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
|
||||||
'author_name' => 'Milena e Weslley',
|
|
||||||
'context' => 'Casamento · 13/02/2026',
|
|
||||||
'sort_order' => 3,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
|
||||||
'author_name' => 'Raquel e Pedro',
|
|
||||||
'context' => 'Casamento · 09/05/2026',
|
|
||||||
'sort_order' => 4,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
|
||||||
'author_name' => 'Victoria e Pedro',
|
|
||||||
'context' => 'Casamento · 24/06/2026',
|
|
||||||
'sort_order' => 5,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$keepAuthors = array_column($testimonials, 'author_name');
|
|
||||||
|
|
||||||
Testimonial::query()
|
|
||||||
->whereNotIn('author_name', $keepAuthors)
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
foreach ($testimonials as $testimonial) {
|
|
||||||
Testimonial::query()->updateOrCreate(
|
|
||||||
['author_name' => $testimonial['author_name']],
|
|
||||||
[
|
|
||||||
...$testimonial,
|
|
||||||
'photo_path' => null,
|
|
||||||
'photo_alt' => null,
|
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function copyFixture(string $fixtureName, string $destination): string
|
private function copyFixture(string $fixtureName, string $destination): string
|
||||||
{
|
{
|
||||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||||
|
|||||||
70
database/seeders/TestimonialsSeeder.php
Normal file
70
database/seeders/TestimonialsSeeder.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Testimonial;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class TestimonialsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const PUBLISHED_AT = '2026-08-05 00:00:00';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$testimonials = [
|
||||||
|
[
|
||||||
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Casamento · 06/12/2025',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_featured' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_featured' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||||
|
'author_name' => 'Milena e Weslley',
|
||||||
|
'context' => 'Casamento · 13/02/2026',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||||
|
'author_name' => 'Raquel e Pedro',
|
||||||
|
'context' => 'Casamento · 09/05/2026',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||||
|
'author_name' => 'Victoria e Pedro',
|
||||||
|
'context' => 'Casamento · 24/06/2026',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::transaction(function () use ($testimonials): void {
|
||||||
|
foreach ($testimonials as $testimonial) {
|
||||||
|
Testimonial::query()->updateOrCreate(
|
||||||
|
['author_name' => $testimonial['author_name']],
|
||||||
|
[
|
||||||
|
'quote' => $testimonial['quote'],
|
||||||
|
'context' => $testimonial['context'],
|
||||||
|
'sort_order' => $testimonial['sort_order'],
|
||||||
|
'is_featured' => $testimonial['is_featured'],
|
||||||
|
'published_at' => self::PUBLISHED_AT,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
# IMAGE_TAG=staging|production|<git-sha>
|
# IMAGE_TAG=staging|production|<git-sha>
|
||||||
# PostgreSQL is a separate Dokploy database service (not defined here).
|
# PostgreSQL is a separate Dokploy database service (not defined here).
|
||||||
# Traefik/Dokploy domains should target service `web` port 8000.
|
# Traefik/Dokploy domains should target service `web` port 8000.
|
||||||
|
# App services join dokploy-network so they can resolve Dokploy-managed
|
||||||
|
# Postgres internal hosts (e.g. amare-stg-pez43e).
|
||||||
|
|
||||||
services:
|
services:
|
||||||
migrate:
|
migrate:
|
||||||
@@ -13,6 +15,8 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
|
command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
@@ -31,6 +35,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
queue:
|
queue:
|
||||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
@@ -44,6 +50,8 @@ services:
|
|||||||
command: ["php", "artisan", "queue:work", "--sleep=2", "--tries=3", "--max-time=3600"]
|
command: ["php", "artisan", "queue:work", "--sleep=2", "--tries=3", "--max-time=3600"]
|
||||||
stop_grace_period: 60s
|
stop_grace_period: 60s
|
||||||
stop_signal: SIGTERM
|
stop_signal: SIGTERM
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
@@ -57,3 +65,9 @@ services:
|
|||||||
command: ["php", "artisan", "schedule:work"]
|
command: ["php", "artisan", "schedule:work"]
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
stop_signal: SIGTERM
|
stop_signal: SIGTERM
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ IMAGE_TAG=staging # or production
|
|||||||
|
|
||||||
Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish PostgreSQL or host ports for app processes.
|
Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish PostgreSQL or host ports for app processes.
|
||||||
|
|
||||||
|
Compose services must join the external Docker network `dokploy-network` (declared in `docker-compose.deploy.yml`) so they can resolve the Dokploy-managed Postgres internal host (e.g. `amare-stg-pez43e`). Set `DB_HOST` to that **Internal Host** from the Dokploy database UI — not a public hostname.
|
||||||
|
|
||||||
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
|
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
|
||||||
|
|
||||||
## Required Laravel env (Dokploy only)
|
## Required Laravel env (Dokploy only)
|
||||||
@@ -68,7 +70,7 @@ APP_FALLBACK_LOCALE=pt_BR
|
|||||||
APP_TIMEZONE=America/Fortaleza
|
APP_TIMEZONE=America/Fortaleza
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
DB_HOST=<dokploy-postgres-host>
|
DB_HOST=<dokploy-postgres-internal-host> # Internal Host from Dokploy UI (requires dokploy-network)
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_DATABASE=amare_staging
|
DB_DATABASE=amare_staging
|
||||||
DB_USERNAME=...
|
DB_USERNAME=...
|
||||||
@@ -98,19 +100,41 @@ LOG_LEVEL=warning
|
|||||||
|
|
||||||
Trusted proxies are configured in `bootstrap/app.php` so Traefik `X-Forwarded-*` headers work for HTTPS cookies and URLs.
|
Trusted proxies are configured in `bootstrap/app.php` so Traefik `X-Forwarded-*` headers work for HTTPS cookies and URLs.
|
||||||
|
|
||||||
|
Livewire temporary uploads default to the **local** disk in `AppServiceProvider` (even when `FILESYSTEM_DISK=r2`), so Filament does not browser-PUT to R2. Final media still lands on R2 via `PublicImageUploadRules`. Optional override: `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK` in Dokploy `.env` (`env_file` accepts any key — does not need a compose `environment:` entry).
|
||||||
|
|
||||||
|
### R2 CORS (public/media reads from JS)
|
||||||
|
|
||||||
|
Upload path does not need R2 CORS with the local temp-disk default. Still useful if the browser fetches R2 URLs cross-origin from JS. Origin must match `APP_URL` exactly; include `AllowedHeaders`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"AllowedOrigins": ["https://hellomanoel.com"],
|
||||||
|
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
|
||||||
|
"AllowedHeaders": ["*"],
|
||||||
|
"ExposeHeaders": ["ETag", "Content-Type"],
|
||||||
|
"MaxAgeSeconds": 3600
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also enable public access / custom domain for `R2_URL` so `<img>` URLs work after save.
|
||||||
|
|
||||||
## GitHub Actions secrets
|
## GitHub Actions secrets
|
||||||
|
|
||||||
Repository secrets used by workflows:
|
Repository secrets used by workflows:
|
||||||
|
|
||||||
| Secret | Purpose |
|
| Secret | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DOKPLOY_URL` | Base URL of the Dokploy panel (no trailing slash) |
|
| `DOKPLOY_URL` | Panel origin **without** `/api` (e.g. `https://panel.example.com`). Do not use the OpenAPI base URL that ends in `/api` — that yields `/api/api/...` and 404s. |
|
||||||
| `DOKPLOY_API_KEY` | API key from Dokploy profile → API/CLI |
|
| `DOKPLOY_API_KEY` | API key from Dokploy profile → API/CLI |
|
||||||
| `DOKPLOY_STAGING_COMPOSE_ID` | Staging compose id |
|
| `DOKPLOY_STAGING_COMPOSE_ID` | Staging **Compose** service id (not an Application id) |
|
||||||
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production compose id |
|
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production **Compose** service id (not an Application id) |
|
||||||
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
|
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
|
||||||
| `PRODUCTION_URL` | Public origin for production smoke |
|
| `PRODUCTION_URL` | Public origin for production smoke |
|
||||||
|
|
||||||
|
HTTP 404 from `compose.deploy` usually means the compose id is wrong (Application id instead of Compose) or `DOKPLOY_URL` still includes `/api`.
|
||||||
|
|
||||||
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
|
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
@@ -135,27 +159,67 @@ Repository secrets used by workflows:
|
|||||||
|
|
||||||
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||||
|
|
||||||
## First admin (no `db:seed` in production)
|
## First admin and authorized production seeding
|
||||||
|
|
||||||
Seed credentials are local-only. For staging/production:
|
Seed credentials are local-only. For staging/production:
|
||||||
|
|
||||||
|
FrankenPHP sets `XDG_CONFIG_HOME=/config` (Caddy). PsySH/tinker then tries `/config/psysh`, which `appuser` cannot write — you get `Writing to directory /config/psysh is not allowed.` Override that env for the one-shot command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From Dokploy → staging/production → Open terminal on `web` (or one-off run)
|
# From Dokploy → staging/production → Open terminal on `web` (or one-off run)
|
||||||
php artisan tinker --execute="
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||||
\\App\\Models\\User::query()->updateOrCreate(
|
\$user = \\App\\Models\\User::query()->updateOrCreate(
|
||||||
['email' => 'admin@example.com'],
|
['email' => 'admin@example.com'],
|
||||||
[
|
[
|
||||||
'name' => 'Admin',
|
'name' => 'Admin',
|
||||||
'password' => bcrypt('use-a-strong-password'),
|
'password' => 'use-a-strong-password',
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'email_verified_at' => now(),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
\$user->forceFill(['email_verified_at' => now()])->save();
|
||||||
|
echo \$user->email.PHP_EOL;
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
Adjust field names if the model evolves. Never reuse `admin@amare.local` / `password`.
|
Plain password is enough: `User` casts `password` to `hashed` (and skips re-hash when value already hashed). `email_verified_at` is not mass-assignable — use `forceFill` as above.
|
||||||
|
|
||||||
|
Confirm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::query()->where('email', 'admin@example.com')->exists() ? 'ok' : 'missing';"
|
||||||
|
```
|
||||||
|
|
||||||
|
Never reuse `admin@amare.local` / `password`.
|
||||||
|
|
||||||
|
### Load authorized testimonials after migrations
|
||||||
|
|
||||||
|
After migrations, manually load the five authorized testimonials in **staging**, then repeat in **production**. From Dokploy, open a terminal on the environment's `web` service (or run an equivalent one-off process):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan db:seed --class='Database\Seeders\TestimonialsSeeder' --force --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
This seeder is safe to rerun: it overwrites canonical source-owned fields, preserves curated photo fields, and leaves unrelated testimonials unchanged. The five records are published with the approved deterministic timestamp. Upsert keys on `author_name` (no unique DB constraint); keep one row per couple before/after running.
|
||||||
|
|
||||||
|
Optional verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||||
|
\$expected = collect(['Jeniffer e Maick', 'Quesia e Jhonata', 'Milena e Weslley', 'Raquel e Pedro', 'Victoria e Pedro']);
|
||||||
|
\$rows = \\App\\Models\\Testimonial::query()->whereIn('author_name', \$expected)->get(['author_name', 'published_at'])->groupBy('author_name');
|
||||||
|
\$valid = \$expected->every(function (string \$author) use (\$rows): bool {
|
||||||
|
\$matches = \$rows->get(\$author, collect());
|
||||||
|
return \$matches->count() === 1
|
||||||
|
&& \$matches->first()->published_at?->format('Y-m-d H:i:s') === '2026-08-05 00:00:00';
|
||||||
|
});
|
||||||
|
echo (\$valid ? 'ok' : 'invalid').PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `ok`.
|
||||||
|
|
||||||
|
Do not run `DatabaseSeeder` or `ContentSeeder` in staging or production: they include local credentials and/or broad demo-content effects. Deployment workflows intentionally remain migrate-only; loading these testimonials is a deliberate manual operation in each environment.
|
||||||
|
|
||||||
## Backup and restore
|
## Backup and restore
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Trigger a Dokploy Compose deploy and wait until it finishes.
|
# Trigger a Dokploy Compose deploy and wait until it finishes.
|
||||||
# Required env:
|
# Required env:
|
||||||
# DOKPLOY_URL e.g. https://panel.example.com
|
# DOKPLOY_URL e.g. https://panel.example.com (panel origin, no /api)
|
||||||
# DOKPLOY_API_KEY x-api-key value
|
# DOKPLOY_API_KEY x-api-key value
|
||||||
# DOKPLOY_COMPOSE_ID target compose id
|
# DOKPLOY_COMPOSE_ID target compose id
|
||||||
# Optional env:
|
# Optional env:
|
||||||
@@ -15,21 +15,53 @@ set -euo pipefail
|
|||||||
: "${DOKPLOY_API_KEY:?DOKPLOY_API_KEY is required}"
|
: "${DOKPLOY_API_KEY:?DOKPLOY_API_KEY is required}"
|
||||||
: "${DOKPLOY_COMPOSE_ID:?DOKPLOY_COMPOSE_ID is required}"
|
: "${DOKPLOY_COMPOSE_ID:?DOKPLOY_COMPOSE_ID is required}"
|
||||||
|
|
||||||
|
# Panel origin only: strip trailing slash and accidental OpenAPI /api suffix.
|
||||||
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
||||||
|
DOKPLOY_URL="${DOKPLOY_URL%/api}"
|
||||||
|
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
||||||
|
|
||||||
DEPLOY_TITLE="${DEPLOY_TITLE:-GitHub deploy}"
|
DEPLOY_TITLE="${DEPLOY_TITLE:-GitHub deploy}"
|
||||||
DEPLOY_TIMEOUT_SEC="${DEPLOY_TIMEOUT_SEC:-900}"
|
DEPLOY_TIMEOUT_SEC="${DEPLOY_TIMEOUT_SEC:-900}"
|
||||||
DEPLOY_POLL_SEC="${DEPLOY_POLL_SEC:-10}"
|
DEPLOY_POLL_SEC="${DEPLOY_POLL_SEC:-10}"
|
||||||
|
|
||||||
|
echo "Dokploy API base: ${DOKPLOY_URL}/api"
|
||||||
|
|
||||||
api() {
|
api() {
|
||||||
local method="$1"
|
local method="$1"
|
||||||
local path="$2"
|
local path="$2"
|
||||||
shift 2
|
shift 2
|
||||||
curl -fsS -X "$method" \
|
local url="${DOKPLOY_URL}/api${path}"
|
||||||
|
local response http_code body
|
||||||
|
|
||||||
|
# Body then HTTP status on the last line (no -f so 4xx/5xx still return a body).
|
||||||
|
response="$(
|
||||||
|
curl -sS -w $'\n%{http_code}' -X "$method" \
|
||||||
-H "accept: application/json" \
|
-H "accept: application/json" \
|
||||||
-H "content-type: application/json" \
|
-H "content-type: application/json" \
|
||||||
-H "x-api-key: ${DOKPLOY_API_KEY}" \
|
-H "x-api-key: ${DOKPLOY_API_KEY}" \
|
||||||
"${DOKPLOY_URL}/api${path}" \
|
"$url" \
|
||||||
"$@"
|
"$@"
|
||||||
|
)" || {
|
||||||
|
echo "Dokploy API request failed (curl transport error)." >&2
|
||||||
|
echo " method: ${method}" >&2
|
||||||
|
echo " url: ${url}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
http_code="$(printf '%s' "$response" | tail -n1)"
|
||||||
|
body="$(printf '%s' "$response" | sed '$d')"
|
||||||
|
|
||||||
|
if [[ ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "Dokploy API error." >&2
|
||||||
|
echo " method: ${method}" >&2
|
||||||
|
echo " url: ${url}" >&2
|
||||||
|
echo " status: ${http_code}" >&2
|
||||||
|
echo " body:" >&2
|
||||||
|
printf '%s\n' "$body" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$body"
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Triggering Dokploy compose.deploy for ${DOKPLOY_COMPOSE_ID}..."
|
echo "Triggering Dokploy compose.deploy for ${DOKPLOY_COMPOSE_ID}..."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
tests/Feature/LivewireTemporaryUploadDiskTest.php
Normal file
13
tests/Feature/LivewireTemporaryUploadDiskTest.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Livewire\Features\SupportFileUploads\FileUploadConfiguration;
|
||||||
|
|
||||||
|
it('keeps livewire temporary uploads on the local disk when the default filesystem is r2', function (): void {
|
||||||
|
config(['filesystems.default' => 'r2']);
|
||||||
|
|
||||||
|
expect(config('livewire.temporary_file_upload.disk'))->toBe('local')
|
||||||
|
->and(FileUploadConfiguration::disk())->toBe('local')
|
||||||
|
->and(FileUploadConfiguration::isUsingS3())->toBeFalse();
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ use App\Models\SiteSetting;
|
|||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Database\Seeders\ContentSeeder;
|
use Database\Seeders\ContentSeeder;
|
||||||
|
use Database\Seeders\TestimonialsSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@@ -18,6 +19,59 @@ class TestimonialsTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<array{
|
||||||
|
* quote: string,
|
||||||
|
* author_name: string,
|
||||||
|
* context: string,
|
||||||
|
* sort_order: int,
|
||||||
|
* is_featured: bool,
|
||||||
|
* published_at: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private const EXPECTED_TESTIMONIALS = [
|
||||||
|
[
|
||||||
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Casamento · 06/12/2025',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_featured' => true,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_featured' => true,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||||
|
'author_name' => 'Milena e Weslley',
|
||||||
|
'context' => 'Casamento · 13/02/2026',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||||
|
'author_name' => 'Raquel e Pedro',
|
||||||
|
'context' => 'Casamento · 09/05/2026',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||||
|
'author_name' => 'Victoria e Pedro',
|
||||||
|
'context' => 'Casamento · 24/06/2026',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
public function test_published_scope_excludes_unpublished_testimonials(): void
|
public function test_published_scope_excludes_unpublished_testimonials(): void
|
||||||
{
|
{
|
||||||
Testimonial::factory()->create(['published_at' => null]);
|
Testimonial::factory()->create(['published_at' => null]);
|
||||||
@@ -48,11 +102,27 @@ class TestimonialsTest extends TestCase
|
|||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_maps_all_five_real_couples_from_depoimentos(): void
|
||||||
|
{
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$testimonials = Testimonial::query()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $this->sourceOwnedState($testimonial))
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertSame(self::EXPECTED_TESTIMONIALS, $testimonials);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
Storage::fake('public');
|
||||||
|
|
||||||
$this->seed(ContentSeeder::class);
|
$this->artisan('db:seed', [
|
||||||
|
'--class' => ContentSeeder::class,
|
||||||
|
'--no-interaction' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
||||||
|
|
||||||
@@ -64,11 +134,6 @@ class TestimonialsTest extends TestCase
|
|||||||
'Victoria e Pedro',
|
'Victoria e Pedro',
|
||||||
], $authors);
|
], $authors);
|
||||||
|
|
||||||
$this->assertNull(
|
|
||||||
Testimonial::query()->where('author_name', 'Ana Souza')->first(),
|
|
||||||
'Fictional demo authors must not remain after seeding real testimonials.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
||||||
|
|
||||||
$this->assertNotNull($jeniffer);
|
$this->assertNotNull($jeniffer);
|
||||||
@@ -77,6 +142,87 @@ class TestimonialsTest extends TestCase
|
|||||||
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_authoritatively_overwrites_source_owned_fields(): void
|
||||||
|
{
|
||||||
|
$testimonial = Testimonial::factory()->create([
|
||||||
|
'quote' => 'Depoimento desatualizado.',
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Contexto desatualizado',
|
||||||
|
'sort_order' => 99,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
self::EXPECTED_TESTIMONIALS[0],
|
||||||
|
$this->sourceOwnedState($testimonial->refresh()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_preserves_curated_photo_fields_for_known_author(): void
|
||||||
|
{
|
||||||
|
$testimonial = Testimonial::factory()->create([
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'photo_path' => 'testimonials/jeniffer-e-maick.jpg',
|
||||||
|
'photo_alt' => 'Jeniffer e Maick durante o casamento',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$testimonial->refresh();
|
||||||
|
|
||||||
|
$this->assertSame('testimonials/jeniffer-e-maick.jpg', $testimonial->photo_path);
|
||||||
|
$this->assertSame('Jeniffer e Maick durante o casamento', $testimonial->photo_alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_leaves_unrelated_testimonials_unchanged(): void
|
||||||
|
{
|
||||||
|
$unrelated = Testimonial::factory()->published()->featured()->create([
|
||||||
|
'quote' => 'Depoimento cadastrado pela equipe.',
|
||||||
|
'author_name' => 'Casal não presente na fonte',
|
||||||
|
'context' => 'Bodas · 02/08/2026',
|
||||||
|
'photo_path' => 'testimonials/casal-equipe.jpg',
|
||||||
|
'photo_alt' => 'Casal cadastrado pela equipe',
|
||||||
|
'sort_order' => 42,
|
||||||
|
]);
|
||||||
|
$originalState = $unrelated->getAttributes();
|
||||||
|
ksort($originalState);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$persisted = $unrelated->fresh();
|
||||||
|
|
||||||
|
$this->assertNotNull($persisted);
|
||||||
|
$persistedState = $persisted->getAttributes();
|
||||||
|
ksort($persistedState);
|
||||||
|
|
||||||
|
$this->assertSame($originalState, $persistedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_is_idempotent_without_duplicate_authors(): void
|
||||||
|
{
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
$firstRunState = Testimonial::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$secondRunState = Testimonial::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertCount(5, $secondRunState);
|
||||||
|
$this->assertSame($firstRunState, $secondRunState);
|
||||||
|
$this->assertSame(5, Testimonial::query()->distinct()->count('author_name'));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
@@ -97,4 +243,34 @@ class TestimonialsTest extends TestCase
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function runTestimonialsSeeder(): void
|
||||||
|
{
|
||||||
|
$this->artisan('db:seed', [
|
||||||
|
'--class' => TestimonialsSeeder::class,
|
||||||
|
'--no-interaction' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* quote: string,
|
||||||
|
* author_name: string,
|
||||||
|
* context: string,
|
||||||
|
* sort_order: int,
|
||||||
|
* is_featured: bool,
|
||||||
|
* published_at: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function sourceOwnedState(Testimonial $testimonial): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quote' => (string) $testimonial->quote,
|
||||||
|
'author_name' => (string) $testimonial->author_name,
|
||||||
|
'context' => (string) $testimonial->context,
|
||||||
|
'sort_order' => (int) $testimonial->sort_order,
|
||||||
|
'is_featured' => (bool) $testimonial->is_featured,
|
||||||
|
'published_at' => $testimonial->published_at?->format('Y-m-d H:i:s') ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user