Compare commits
11 commits
dev
...
feat/spati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c977d1f5 | ||
|
|
8e1650653b | ||
|
|
6d1d88542e | ||
|
|
a94a34ce3b | ||
|
|
c49249ee20 | ||
|
|
340b466ade | ||
|
|
9f01d44c9d | ||
|
|
d24b9b0732 | ||
|
|
02884d4e2b | ||
|
|
d40b87438d | ||
|
|
5ea0ddce23 |
190 changed files with 858 additions and 7495 deletions
|
|
@ -1,32 +0,0 @@
|
||||||
# Ignore .env files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
.envrc
|
|
||||||
|
|
||||||
# Ignore node_modules
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Ignore vendor folder
|
|
||||||
vendor/
|
|
||||||
|
|
||||||
# Ignore log files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Ignore IDE and editor files
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# Ignore system files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Ignore Laravel storage
|
|
||||||
/storage/*.key
|
|
||||||
/storage/*.log
|
|
||||||
/storage/framework/cache/*
|
|
||||||
/storage/framework/sessions/*
|
|
||||||
/storage/framework/views/*
|
|
||||||
/storage/logs/*
|
|
||||||
|
|
||||||
# Ignore database files if in development
|
|
||||||
database/database.sqlite
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
DUSK_HEADLESS_DISABLED=true
|
|
||||||
ADMIN_EMAIL=login-test@example.com
|
|
||||||
APP_ENV=testing
|
|
||||||
DB_DATABASE=database/dusk.sqlite
|
|
||||||
|
|
||||||
APP_NAME=Laravel
|
|
||||||
APP_KEY=base64:YOUR_GENERATED_KEY_HERE=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://127.0.0.1:8000
|
|
||||||
|
|
||||||
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=sqlite
|
|
||||||
# DB_HOST=127.0.0.1
|
|
||||||
# DB_PORT=3306
|
|
||||||
# DB_DATABASE=laravel
|
|
||||||
# DB_USERNAME=root
|
|
||||||
# DB_PASSWORD=
|
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_ENCRYPT=false
|
|
||||||
SESSION_PATH=/
|
|
||||||
SESSION_DOMAIN=null
|
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=database
|
|
||||||
|
|
||||||
CACHE_STORE=database
|
|
||||||
# CACHE_PREFIX=
|
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
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
|
|
||||||
|
|
||||||
MEDIA_DISK=s3
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
|
|
@ -61,7 +61,5 @@ AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
# AWS_BUCKET=share-lt-images
|
|
||||||
# MEDIA_DISK=s3
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
|
||||||
12
.github/copilot-instructions.md
vendored
12
.github/copilot-instructions.md
vendored
|
|
@ -13,11 +13,8 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v12
|
- laravel/framework (LARAVEL) - v12
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- laravel/reverb (REVERB) - v1
|
|
||||||
- laravel/sanctum (SANCTUM) - v4
|
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v3
|
- livewire/livewire (LIVEWIRE) - v3
|
||||||
- laravel/dusk (DUSK) - v8
|
|
||||||
- laravel/mcp (MCP) - v0
|
- laravel/mcp (MCP) - v0
|
||||||
- laravel/pint (PINT) - v1
|
- laravel/pint (PINT) - v1
|
||||||
- laravel/sail (SAIL) - v1
|
- laravel/sail (SAIL) - v1
|
||||||
|
|
@ -500,12 +497,3 @@ Fortify is a headless authentication backend that provides authentication routes
|
||||||
- `Features::updatePasswords()` to let users change their passwords.
|
- `Features::updatePasswords()` to let users change their passwords.
|
||||||
- `Features::resetPasswords()` for password reset via email.
|
- `Features::resetPasswords()` for password reset via email.
|
||||||
</laravel-boost-guidelines>
|
</laravel-boost-guidelines>
|
||||||
|
|
||||||
|
|
||||||
=== docker/core rules ===
|
|
||||||
## Over-Engineering & Bloat
|
|
||||||
- Do not add unnecessary boilerplate or "nice-to-have" features.
|
|
||||||
- Only implement what solves the immediate problem.
|
|
||||||
- Ask before adding optional infrastructure or configuration sections.
|
|
||||||
- If a system worked before without something, don't add it "just in case".
|
|
||||||
- Minimize configuration, complexity, and dependencies.
|
|
||||||
|
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -9,7 +9,6 @@
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.production
|
.env.production
|
||||||
.env.dev
|
|
||||||
.phpactor.json
|
.phpactor.json
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
Homestead.json
|
Homestead.json
|
||||||
|
|
@ -23,12 +22,3 @@ yarn-error.log
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
.vite
|
.vite
|
||||||
*.deleted
|
|
||||||
.env.dusk.local
|
|
||||||
log*.txt
|
|
||||||
.envrc
|
|
||||||
database/backups
|
|
||||||
*backup.tar.gz
|
|
||||||
public/css
|
|
||||||
public/js
|
|
||||||
notes
|
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: dev
|
|
||||||
steps:
|
|
||||||
build-local:
|
|
||||||
image: docker:24-dind
|
|
||||||
privileged: true
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
commands:
|
|
||||||
- echo "Pulling base images to ensure latest layers..."
|
|
||||||
- docker pull --quiet php:8.4-fpm-alpine3.23 || true
|
|
||||||
- echo "Try to pull previous image to use as cache ..."
|
|
||||||
- docker pull quay.io/marshyon/share-lt:latest || true
|
|
||||||
- echo "Building image for testing (amd64 only for CI compatibility)..."
|
|
||||||
- docker build --platform linux/amd64 --cache-from=quay.io/marshyon/share-lt:latest -t share-lt:test .
|
|
||||||
- echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.8..."
|
|
||||||
- docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.8
|
|
||||||
- echo "Generating SBOM..."
|
|
||||||
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.8 -o cyclonedx-json > sbom.json
|
|
||||||
scan-vulnerabilities:
|
|
||||||
image: aquasec/trivy:0.67.2
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
commands:
|
|
||||||
- echo "Ensuring latest Trivy image is pulled..."
|
|
||||||
- docker pull aquasec/trivy:latest || true
|
|
||||||
- echo "Scanning for vulnerabilities via Docker daemon..."
|
|
||||||
# Disabling scan for testing, will re-enable once a fix for
|
|
||||||
# vulnerability is available.
|
|
||||||
# Scan the image present in the Docker daemon; fail on CRITICAL severities
|
|
||||||
# - trivy image --exit-code 1 --severity CRITICAL --no-progress share-lt:test
|
|
||||||
# Run a full scan without failing just for logs
|
|
||||||
- trivy image --severity HIGH,MEDIUM,LOW --no-progress share-lt:test
|
|
||||||
- echo "Generating vulnerability report..."
|
|
||||||
- trivy image --format cyclonedx --output trivy-vuln-bom.json share-lt:test
|
|
||||||
- echo "Vulnerability Summary:"
|
|
||||||
- trivy image --format table share-lt:test | tee trivy-vuln-summary.txt
|
|
||||||
publish:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
settings:
|
|
||||||
registry: quay.io
|
|
||||||
repo: quay.io/marshyon/share-lt
|
|
||||||
platforms: linux/amd64
|
|
||||||
tags:
|
|
||||||
- v0.0.8
|
|
||||||
- latest
|
|
||||||
username:
|
|
||||||
from_secret: QUAY_USERNAME
|
|
||||||
password:
|
|
||||||
from_secret: QUAY_PASSWORD
|
|
||||||
upload-sbom:
|
|
||||||
image: cgr.dev/chainguard/cosign:latest
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
COSIGN_REGISTRY_USERNAME:
|
|
||||||
from_secret: QUAY_USERNAME
|
|
||||||
COSIGN_REGISTRY_PASSWORD:
|
|
||||||
from_secret: QUAY_PASSWORD
|
|
||||||
commands:
|
|
||||||
- cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.8 || echo "SBOM attach failed"
|
|
||||||
- echo "Done - trivy report saved to workspace for manual review"
|
|
||||||
|
|
||||||
59
CHANGELOG.md
59
CHANGELOG.md
|
|
@ -1,67 +1,12 @@
|
||||||
|
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 2026-02-17
|
|
||||||
|
|
||||||
added reverb, echo and toast messages for site builds
|
|
||||||
|
|
||||||
added change log for go-live logging and audit
|
|
||||||
|
|
||||||
## 2026-02-09
|
|
||||||
|
|
||||||
added reference compose files
|
|
||||||
|
|
||||||
added basic NATS integration
|
|
||||||
|
|
||||||
## 2026-01-25
|
|
||||||
|
|
||||||
added s3, docker build
|
|
||||||
|
|
||||||
## 2026-01-19
|
|
||||||
|
|
||||||
added text widgets
|
|
||||||
|
|
||||||
added categories
|
|
||||||
|
|
||||||
updated api to access text widgets and categories
|
|
||||||
|
|
||||||
added url and call to action for entries for use in cards
|
|
||||||
|
|
||||||
added import image and blogs import commands
|
|
||||||
|
|
||||||
## 2026-01-08
|
|
||||||
|
|
||||||
added tags to entry model
|
|
||||||
|
|
||||||
added text widget and category
|
|
||||||
|
|
||||||
## 2026-01-07
|
|
||||||
|
|
||||||
added simple API for entries model
|
|
||||||
- to view entries
|
|
||||||
- implement initial access control
|
|
||||||
|
|
||||||
this is sufficient to test static site generation
|
|
||||||
|
|
||||||
## 2026-01-06
|
|
||||||
|
|
||||||
added
|
|
||||||
- Spatie Media Library
|
|
||||||
- media library configuration file
|
|
||||||
- Updated Entry model to support media handling
|
|
||||||
- featured image upload with gallery selection and preview
|
|
||||||
- login tests with Dusk for user authentication
|
|
||||||
- Dusk test for featured image selection
|
|
||||||
|
|
||||||
## 2026-01-02
|
|
||||||
|
|
||||||
added initial model and filament resource
|
|
||||||
|
|
||||||
## 2026-01-01
|
## 2026-01-01
|
||||||
|
|
||||||
added: laravel 12
|
added: laravel 12
|
||||||
|
|
||||||
added: AGPLv3
|
added: AGPLv3
|
||||||
|
|
||||||
|
## 2026-01-02
|
||||||
|
|
||||||
|
added initial model and filament resource
|
||||||
|
|
||||||
|
|
|
||||||
107
Dockerfile
107
Dockerfile
|
|
@ -1,107 +0,0 @@
|
||||||
# Build stage for NATS CLI
|
|
||||||
FROM golang:1.26-alpine AS nats-builder
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
RUN git clone --depth 1 https://github.com/nats-io/natscli.git /src
|
|
||||||
WORKDIR /src/nats
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nats .
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM php:8.4-fpm-alpine3.23
|
|
||||||
|
|
||||||
ENV APP_ENV=production
|
|
||||||
ENV APP_DEBUG=false
|
|
||||||
WORKDIR /var/www
|
|
||||||
RUN apk update && apk add --no-cache \
|
|
||||||
build-base \
|
|
||||||
libpng-dev \
|
|
||||||
libjpeg-turbo-dev \
|
|
||||||
freetype-dev \
|
|
||||||
zip \
|
|
||||||
jpegoptim optipng pngquant gifsicle \
|
|
||||||
vim \
|
|
||||||
unzip \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
libzip-dev \
|
|
||||||
oniguruma-dev \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
icu-dev \
|
|
||||||
sqlite-dev \
|
|
||||||
sqlite-libs \
|
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
su-exec \
|
|
||||||
tini \
|
|
||||||
unzip \
|
|
||||||
bash \
|
|
||||||
jq \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
COPY --from=nats-builder /src/nats/nats /usr/local/bin/nats
|
|
||||||
RUN chmod +x /usr/local/bin/nats
|
|
||||||
|
|
||||||
RUN rm -rf /var/cache/apk/*
|
|
||||||
RUN docker-php-ext-install mbstring zip exif pcntl intl gd pdo pdo_sqlite bcmath
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
|
||||||
|
|
||||||
# Copy entrypoint script
|
|
||||||
COPY cmd/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Copy supervisord configuration
|
|
||||||
COPY ./docker/supervisord.conf /etc/supervisord.conf
|
|
||||||
RUN mkdir -p /var/log/supervisor \
|
|
||||||
&& mkdir -p /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx \
|
|
||||||
&& chown -R root:root /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx
|
|
||||||
|
|
||||||
# Create www user and add to www-data group
|
|
||||||
RUN adduser -u 1000 -G www-data -s /bin/sh -D www
|
|
||||||
|
|
||||||
# Configure PHP-FPM to run as www user
|
|
||||||
RUN sed -i 's/user = www-data/user = www/g' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
# Remove the semicolon to uncomment the listen directive
|
|
||||||
RUN sed -i 's/;listen = 127.0.0.1:9000/listen = 9000/' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
# Ensure the worker running the code is correct (usually www-data or nginx)
|
|
||||||
RUN sed -i 's/;listen.owner = www-data/listen.owner = www/' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
RUN sed -i 's/;listen.group = www-data/listen.group = www-data/' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
# Update nginx.conf to use 'www' user instead of 'nginx'
|
|
||||||
RUN sed -i 's/user nginx;/user www;/' /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
# Remove user and group directives from nginx and php-fpm configs to avoid conflicts
|
|
||||||
RUN sed -i '/^user /d' /etc/nginx/nginx.conf
|
|
||||||
RUN sed -i '/^user = /d' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
RUN sed -i '/^group = /d' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
# Set permissions for nginx directories
|
|
||||||
RUN mkdir -p /var/lib/nginx/tmp/client_body /var/log/nginx \
|
|
||||||
&& chown -R www:www-data /var/lib/nginx /var/log/nginx \
|
|
||||||
&& chmod -R 755 /var/lib/nginx /var/log/nginx \
|
|
||||||
&& touch /run/nginx/nginx.pid \
|
|
||||||
&& chown www:www-data /run/nginx/nginx.pid
|
|
||||||
|
|
||||||
# Copy application code (includes database/migrations/) and excluding
|
|
||||||
# files in .dockerignore
|
|
||||||
COPY --chown=www:www-data . /var/www
|
|
||||||
RUN chown -R www:www-data /var/www
|
|
||||||
RUN chown -R www:www-data /var/log/supervisor
|
|
||||||
|
|
||||||
# Switch to www user
|
|
||||||
USER www
|
|
||||||
|
|
||||||
# Install app dependencies
|
|
||||||
RUN composer install --optimize-autoloader --no-dev
|
|
||||||
RUN npm ci
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# run laravel cache optimization
|
|
||||||
RUN php artisan optimize
|
|
||||||
|
|
||||||
EXPOSE 8889
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
|
||||||
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
|
|
||||||
|
|
@ -13,11 +13,8 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v12
|
- laravel/framework (LARAVEL) - v12
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- laravel/reverb (REVERB) - v1
|
|
||||||
- laravel/sanctum (SANCTUM) - v4
|
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v3
|
- livewire/livewire (LIVEWIRE) - v3
|
||||||
- laravel/dusk (DUSK) - v8
|
|
||||||
- laravel/mcp (MCP) - v0
|
- laravel/mcp (MCP) - v0
|
||||||
- laravel/pint (PINT) - v1
|
- laravel/pint (PINT) - v1
|
||||||
- laravel/sail (SAIL) - v1
|
- laravel/sail (SAIL) - v1
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -1,48 +1,6 @@
|
||||||
# share-lt
|
# share-lt
|
||||||
|
|
||||||
Share Light CMS - Headless CMS with Real-time Publishing
|
Share Light CMS
|
||||||
|
|
||||||
[](https://wpk.headshed.dev/repos/2) [](https://quay.io/repository/marshyon/share-lt)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Share Light is a modern headless CMS built with Laravel 12 that enables real-time content management and automated static site generation. It provides a complete backend solution for content creators and developers who need a robust, scalable CMS with live preview capabilities.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This application runs as a **fat container** orchestrated by **tini** and **supervisor**, managing multiple services:
|
|
||||||
|
|
||||||
- **Laravel 12** - Core CMS application with Filament admin interface
|
|
||||||
- **PHP-FPM** - PHP process manager
|
|
||||||
- **nginx** - Web server and reverse proxy
|
|
||||||
- **Queue Worker** - Background job processing
|
|
||||||
- **Laravel Reverb** - WebSocket server for real-time communication
|
|
||||||
|
|
||||||
### Integration with External Services
|
|
||||||
|
|
||||||
- **NATS Messaging** - Publishes messages to NATS streams when content changes
|
|
||||||
- **share-lt-astro-consumer** - Companion application that consumes NATS messages to rebuild static sites
|
|
||||||
- **Real-time Notifications** - Uses Reverb to send toast alerts to users about build status
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Content Management
|
|
||||||
- **Entry Model** - Core content entities with media library support
|
|
||||||
- **Text Widgets** - Reusable content blocks
|
|
||||||
- **Categories** - Content organization and taxonomy
|
|
||||||
- **Media Library** - Image and file management with Spatie Media Library
|
|
||||||
|
|
||||||
### Real-time Publishing Workflow
|
|
||||||
1. **Content Updates** - When entries are modified, messages are automatically sent to NATS streams
|
|
||||||
2. **Consumer Notifications** - External applications (like share-lt-astro-consumer) listen for these messages
|
|
||||||
3. **Preview Generation** - Consumers rebuild preview websites based on content changes
|
|
||||||
4. **Status Updates** - Build results are sent back via API
|
|
||||||
5. **Live Alerts** - Users receive real-time toast notifications about build status through Reverb
|
|
||||||
|
|
||||||
### API & Integration
|
|
||||||
- **RESTful API** - Complete API for content retrieval and management
|
|
||||||
- **Access Control** - Built-in authorization and authentication
|
|
||||||
- **NATS Integration** - Message streaming for distributed architecture
|
|
||||||
|
|
||||||
this project is in 'Alpha'
|
this project is in 'Alpha'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use App\Models\Entry;
|
|
||||||
|
|
||||||
class ImportBlogs extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'app:import-blogs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Command description';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 7,
|
|
||||||
"title": "Low/No Code FlowiseAI",
|
|
||||||
"slug": "lowno-code-flowiseai",
|
|
||||||
"content": "<p><img alt=\"\" src=\"http://127.0.0.1:8000/storage/99/ZsTHWAQRbdvgRUEwGmCIsm4TyChIjBwiY71VmnnR.webp\"/></p>\n<p>In <a href=\"https://flowiseai.com/\">FlowiseAI</a>, applications based on the JavaScript fork of LangChain are modeled in a 'no/low-code' environment. If you are coming from a closed source world, yet trying to implement devops principles this may fill you with fear, dread and uncertainty. FlowiseAI offers the advantage of using plain-text JSON files to represent each workflow. These files are easy to understand, open, and readily backup-able, unlike opaque proprietary binary formats\nThe data used at runtime and other component prerequisites like credentials are stored in the FlowiseAI data volume, which looks like this</p>\n<p><code>bash\nMode LastWriteTime ..... 8ZS1C0ZBB.webp",
|
|
||||||
"created_at": "2024-12-17 11:24:59",
|
|
||||||
"updated_at": "2024-12-17 12:53:54",
|
|
||||||
"category_id": 1,
|
|
||||||
"blog_date": "2024-02-26 00:00",
|
|
||||||
"is_featured": 0,
|
|
||||||
"published": 1
|
|
||||||
},
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
$filePath = '/home/user/projects/laravel/12/media_library/boring-astro-static/imported_database/blogs.json';
|
|
||||||
|
|
||||||
if (!file_exists($filePath)) {
|
|
||||||
$this->error("File not found: $filePath");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$jsonContent = file_get_contents($filePath);
|
|
||||||
$blogs = json_decode($jsonContent, true);
|
|
||||||
|
|
||||||
if (!$blogs) {
|
|
||||||
$this->error("Could not parse JSON file: $filePath");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($blogs as $blog) {
|
|
||||||
// Only process the blog with ID 51
|
|
||||||
if (($blog['id'] ?? null) !== 51) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$slug = $blog['slug'] ?? null;
|
|
||||||
$this->info("Processing blog ID: {$blog['id']} with slug: {$slug}");
|
|
||||||
// Check if the entry already exists
|
|
||||||
$existingEntry = Entry::where('slug', $slug)->first();
|
|
||||||
|
|
||||||
if ($existingEntry) {
|
|
||||||
// Update existing entry with cleaned content
|
|
||||||
$existingEntry->update([
|
|
||||||
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
|
||||||
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
|
||||||
]);
|
|
||||||
$this->info("Updated content for: {$existingEntry->title}");
|
|
||||||
} else {
|
|
||||||
// Create new entry
|
|
||||||
Entry::create([
|
|
||||||
'title' => $blog['title'] ?? null,
|
|
||||||
'slug' => $slug,
|
|
||||||
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
|
||||||
'is_published' => $blog['published'] ?? false,
|
|
||||||
'is_featured' => $blog['is_featured'] ?? false,
|
|
||||||
'published_at' => $blog['blog_date'] ?? null,
|
|
||||||
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
|
||||||
'category_id' => 1, // Default category
|
|
||||||
]);
|
|
||||||
$this->info("Created new entry: " . ($blog['title'] ?? 'Untitled'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Blogs imported successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract plain text from HTML for description field
|
|
||||||
*/
|
|
||||||
private function extractPlainTextFromHtml(string $html): string
|
|
||||||
{
|
|
||||||
if (empty($html)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode HTML entities and strip tags
|
|
||||||
$text = html_entity_decode(strip_tags($html));
|
|
||||||
|
|
||||||
// Clean up whitespace
|
|
||||||
$text = preg_replace('/\s+/', ' ', $text);
|
|
||||||
|
|
||||||
// Limit to reasonable length for description
|
|
||||||
return trim(substr($text, 0, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean HTML content for Filament rich editor
|
|
||||||
*/
|
|
||||||
private function cleanHtmlForFilament(string $html): string
|
|
||||||
{
|
|
||||||
if (empty($html)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert escaped newlines to actual newlines
|
|
||||||
$html = str_replace(['\\n', '\\r\\n', '\\r'], "\n", $html);
|
|
||||||
|
|
||||||
// Decode HTML entities
|
|
||||||
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
||||||
|
|
||||||
// Clean up excessive whitespace but preserve paragraph structure
|
|
||||||
$html = preg_replace('/\s*\n\s*/', ' ', $html);
|
|
||||||
$html = preg_replace('/[ \t]+/', ' ', $html);
|
|
||||||
|
|
||||||
// Ensure paragraphs have proper spacing
|
|
||||||
$html = str_replace('</p><p>', '</p>' . "\n" . '<p>', $html);
|
|
||||||
|
|
||||||
return trim($html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\Entry;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
class ImportImages extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'app:import-images {--entry-id=1}';
|
|
||||||
protected $description = 'Import images into the media library';
|
|
||||||
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$entryId = $this->option('entry-id');
|
|
||||||
$entry = Entry::find($entryId);
|
|
||||||
|
|
||||||
if (!$entry) {
|
|
||||||
$this->error("Entry with ID {$entryId} not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = Storage::disk('public')->files('imported_images');
|
|
||||||
|
|
||||||
if (empty($files)) {
|
|
||||||
$this->info('No files found in storage/app/public/imported_images/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info("Found " . count($files) . " files to import");
|
|
||||||
|
|
||||||
foreach ($files as $filePath) {
|
|
||||||
try {
|
|
||||||
$fullPath = storage_path('app/public/' . $filePath);
|
|
||||||
$fileName = pathinfo($fullPath, PATHINFO_BASENAME);
|
|
||||||
|
|
||||||
if (!file_exists($fullPath)) {
|
|
||||||
$this->error("File not found: {$fullPath}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already exists
|
|
||||||
$existingMedia = $entry->getMedia()->where('file_name', $fileName)->first();
|
|
||||||
if ($existingMedia) {
|
|
||||||
$this->info("Skipping existing: {$fileName}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$media = $entry->addMedia($fullPath)
|
|
||||||
->toMediaCollection('default');
|
|
||||||
|
|
||||||
$this->info("Imported: {$fileName}");
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error("Failed to import {$filePath}: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Import completed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
app/Console/Commands/MoveMediaToPublic.php
Normal file
112
app/Console/Commands/MoveMediaToPublic.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
|
class MoveMediaToPublic extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'media:move-to-public {--dry-run : Show what would be moved without actually moving files}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Move all media files from private/local disk to public disk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->warn('DRY RUN MODE - No files will actually be moved');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all media records using local disk
|
||||||
|
$mediaRecords = Media::where('disk', 'local')->get();
|
||||||
|
|
||||||
|
if ($mediaRecords->isEmpty()) {
|
||||||
|
$this->info('No media records found using local disk.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$mediaRecords->count()} media records to migrate.");
|
||||||
|
|
||||||
|
$progressBar = $this->output->createProgressBar($mediaRecords->count());
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
$moved = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
foreach ($mediaRecords as $media) {
|
||||||
|
// Use relative path: {id}/{filename}
|
||||||
|
$relativePath = $media->id . '/' . $media->file_name;
|
||||||
|
|
||||||
|
// Check if source file exists
|
||||||
|
if (!Storage::disk('local')->exists($relativePath)) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->error("Source file not found: {$relativePath}");
|
||||||
|
$errors++;
|
||||||
|
$progressBar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$dryRun) {
|
||||||
|
// Copy file from local to public disk
|
||||||
|
$fileContent = Storage::disk('local')->get($relativePath);
|
||||||
|
Storage::disk('public')->put($relativePath, $fileContent);
|
||||||
|
|
||||||
|
// Verify the file was copied successfully
|
||||||
|
if (Storage::disk('public')->exists($relativePath)) {
|
||||||
|
// Update the database record
|
||||||
|
$media->update([
|
||||||
|
'disk' => 'public',
|
||||||
|
'conversions_disk' => 'public',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete the old file from local disk
|
||||||
|
Storage::disk('local')->delete($relativePath);
|
||||||
|
|
||||||
|
$moved++;
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Failed to copy file to public disk");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->newLine();
|
||||||
|
$this->line("Would move: local:{$relativePath} -> public:{$relativePath}");
|
||||||
|
$moved++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->error("Error moving {$relativePath}: {$e->getMessage()}");
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info("DRY RUN: Would move {$moved} files, {$errors} errors encountered.");
|
||||||
|
$this->info("Run without --dry-run to actually perform the migration.");
|
||||||
|
} else {
|
||||||
|
$this->info("Successfully moved {$moved} files, {$errors} errors encountered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
use function Livewire\str;
|
|
||||||
|
|
||||||
class RemediateBlogS3Images extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'app:remediate-blog-s3-images';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Command description';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$stringToFind = 'src="http://127.0.0.1:8000/storage';
|
|
||||||
$stringToReplace = 'src="https://your-s3-bucket-here/yours-site-static-media-dir-here';
|
|
||||||
|
|
||||||
Log::info('RemediateBlogS3Images command executed.');
|
|
||||||
foreach (Entry::all() as $entry) {
|
|
||||||
|
|
||||||
$this->info('Entry ID: ' . $entry->id);
|
|
||||||
if (str_contains($entry->content, $stringToFind)) {
|
|
||||||
$this->info(' - Found occurrence in entry ID: ' . $entry->id);
|
|
||||||
// Extract all image srcs that match the pattern
|
|
||||||
preg_match_all('/src=\"http:\/\/127.0.0.1:8000\/storage([^\"]*)/', $entry->content, $matches);
|
|
||||||
if (!empty($matches[0])) {
|
|
||||||
foreach ($matches[0] as $i => $foundUrl) {
|
|
||||||
$this->info(' - Found image src: ' . $foundUrl);
|
|
||||||
// Compute the replacement for this specific image
|
|
||||||
$relativePath = $matches[1][$i] ?? '';
|
|
||||||
$newUrl = 'src="https://your-s3-bucket-here/yours-site-static-media-dir-here' . $relativePath;
|
|
||||||
$this->info(' - Will replace with: ' . $newUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$updatedContent = \str_replace($stringToFind, $stringToReplace, $entry->content);
|
|
||||||
// uncomment the following when your sure about the changes
|
|
||||||
// $entry->content = $updatedContent;
|
|
||||||
// $entry->save();
|
|
||||||
// $this->info(' - Updated entry ID: ' . $entry->id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Events\PreviewSiteBuilt;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class SendPreviewSiteBuiltNotification extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'test:preview-site-built {--message=Preview site is built}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Send a test notification that the preview site is built';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$message = $this->option('message');
|
|
||||||
|
|
||||||
$this->info("Command :: Broadcasting preview site built notification: {$message}");
|
|
||||||
|
|
||||||
PreviewSiteBuilt::dispatch($message, 'success');
|
|
||||||
|
|
||||||
$this->info('Notification broadcasted successfully!');
|
|
||||||
$this->info('Check your Filament admin panel for the toast notification.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use Illuminate\Broadcasting\Channel;
|
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class PreviewSiteBuilt implements ShouldBroadcast
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $message = 'Preview site is built',
|
|
||||||
public string $type = 'success'
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function broadcastOn(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
new Channel('filament-notifications'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function broadcastAs(): string
|
|
||||||
{
|
|
||||||
return 'preview-site.built';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use Illuminate\Broadcasting\Channel;
|
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class TestEvent implements ShouldBroadcast
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $message = 'Test message from Laravel'
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function broadcastOn(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
new Channel('test-channel'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function broadcastAs(): string
|
|
||||||
{
|
|
||||||
return 'test.message';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Assets\Pages\CreateAsset;
|
|
||||||
use App\Filament\Resources\Assets\Pages\EditAsset;
|
|
||||||
use App\Filament\Resources\Assets\Pages\ListAssets;
|
|
||||||
use App\Filament\Resources\Assets\Schemas\AssetForm;
|
|
||||||
use App\Filament\Resources\Assets\Tables\AssetsTable;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Icons\Heroicon;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class AssetResource extends Resource
|
|
||||||
{
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $model = Asset::class;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpTray;
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return AssetForm::configure($schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return AssetsTable::configure($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => ListAssets::route('/'),
|
|
||||||
'create' => CreateAsset::route('/create'),
|
|
||||||
'edit' => EditAsset::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Assets\AssetResource;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
|
||||||
|
|
||||||
class CreateAsset extends CreateRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = AssetResource::class;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Assets\AssetResource;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditAsset extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = AssetResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
DeleteAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Assets\AssetResource;
|
|
||||||
use Filament\Actions\CreateAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListAssets extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = AssetResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
CreateAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets\Schemas;
|
|
||||||
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
|
|
||||||
class AssetForm
|
|
||||||
{
|
|
||||||
public static function configure(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
TextInput::make('alt_text'),
|
|
||||||
SpatieMediaLibraryFileUpload::make('image')
|
|
||||||
->collection('default')
|
|
||||||
->image()
|
|
||||||
->disk('s3')
|
|
||||||
->visibility('public')
|
|
||||||
->label('Upload Image'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Assets\Tables;
|
|
||||||
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class AssetsTable
|
|
||||||
{
|
|
||||||
public static function configure(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('alt_text')
|
|
||||||
->searchable(),
|
|
||||||
TextColumn::make('created_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
TextColumn::make('updated_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->recordActions([
|
|
||||||
EditAction::make(),
|
|
||||||
])
|
|
||||||
->toolbarActions([
|
|
||||||
BulkActionGroup::make([
|
|
||||||
DeleteBulkAction::make(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Categroys\Pages\CreateCategroy;
|
|
||||||
use App\Filament\Resources\Categroys\Pages\EditCategroy;
|
|
||||||
use App\Filament\Resources\Categroys\Pages\ListCategroys;
|
|
||||||
use App\Filament\Resources\Categroys\Schemas\CategroyForm;
|
|
||||||
use App\Filament\Resources\Categroys\Tables\CategroysTable;
|
|
||||||
use App\Models\Category;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Icons\Heroicon;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class CategroyResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Category::class;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::RectangleGroup;
|
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'name';
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return CategroyForm::configure($schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return CategroysTable::configure($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => ListCategroys::route('/'),
|
|
||||||
'create' => CreateCategroy::route('/create'),
|
|
||||||
'edit' => EditCategroy::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Categroys\CategroyResource;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
|
||||||
|
|
||||||
class CreateCategroy extends CreateRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = CategroyResource::class;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Categroys\CategroyResource;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditCategroy extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = CategroyResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
DeleteAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Categroys\CategroyResource;
|
|
||||||
use Filament\Actions\CreateAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListCategroys extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = CategroyResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
CreateAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys\Schemas;
|
|
||||||
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
|
|
||||||
class CategroyForm
|
|
||||||
{
|
|
||||||
public static function configure(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
TextInput::make('name')
|
|
||||||
->label('Category Name')
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Categroys\Tables;
|
|
||||||
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class CategroysTable
|
|
||||||
{
|
|
||||||
public static function configure(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('name')
|
|
||||||
->label('Category Name')
|
|
||||||
->sortable()
|
|
||||||
->searchable(),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->recordActions([
|
|
||||||
EditAction::make(),
|
|
||||||
])
|
|
||||||
->toolbarActions([
|
|
||||||
BulkActionGroup::make([
|
|
||||||
DeleteBulkAction::make(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Changes\Pages\CreateChange;
|
|
||||||
use App\Filament\Resources\Changes\Pages\EditChange;
|
|
||||||
use App\Filament\Resources\Changes\Pages\ListChanges;
|
|
||||||
use App\Filament\Resources\Changes\Schemas\ChangeForm;
|
|
||||||
use App\Filament\Resources\Changes\Tables\ChangesTable;
|
|
||||||
use App\Models\Change as ModelsChange;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Icons\Heroicon;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class ChangeResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = ModelsChange::class;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpOnSquareStack;
|
|
||||||
|
|
||||||
public static function getNavigationGroup(): ?string
|
|
||||||
{
|
|
||||||
return 'Settings';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return ChangeForm::configure($schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return ChangesTable::configure($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => ListChanges::route('/'),
|
|
||||||
'create' => CreateChange::route('/create'),
|
|
||||||
'edit' => EditChange::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Changes\ChangeResource;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
|
||||||
|
|
||||||
class CreateChange extends CreateRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = ChangeResource::class;
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeCreate(array $data): array
|
|
||||||
{
|
|
||||||
$data['user_id'] = auth()->id();
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Changes\ChangeResource;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditChange extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = ChangeResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\Changes\ChangeResource;
|
|
||||||
use Filament\Actions\CreateAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListChanges extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = ChangeResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
CreateAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes\Schemas;
|
|
||||||
|
|
||||||
use Filament\Forms\Components\Select as FormSelect;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
|
|
||||||
class ChangeForm
|
|
||||||
{
|
|
||||||
public static function configure(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
Textarea::make('note')
|
|
||||||
->label('Note')
|
|
||||||
->required(),
|
|
||||||
FormSelect::make('type')
|
|
||||||
->label('Type')
|
|
||||||
->options([
|
|
||||||
'go-live' => 'Go Live',
|
|
||||||
'general' => 'General',
|
|
||||||
])
|
|
||||||
->default('go-live')
|
|
||||||
->required(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Changes\Tables;
|
|
||||||
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class ChangesTable
|
|
||||||
{
|
|
||||||
public static function configure(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('note')
|
|
||||||
->label('Note')
|
|
||||||
->wrap(),
|
|
||||||
TextColumn::make('type')
|
|
||||||
->label('Type'),
|
|
||||||
TextColumn::make('user.name')
|
|
||||||
->label('User'),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->recordActions([])
|
|
||||||
->toolbarActions([
|
|
||||||
BulkActionGroup::make([
|
|
||||||
DeleteBulkAction::make(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ class EntryResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Entry::class;
|
protected static ?string $model = Entry::class;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::PencilSquare;
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'title';
|
protected static ?string $recordTitleAttribute = 'title';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,16 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\Entries\Schemas;
|
namespace App\Filament\Resources\Entries\Schemas;
|
||||||
|
|
||||||
use App\Models\Category;
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\RichEditor;
|
use Filament\Forms\Components\RichEditor;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
use Filament\Forms\Components\SpatieTagsInput;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
|
|
@ -23,16 +21,6 @@ class EntryForm
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Select::make('type')
|
|
||||||
->options([
|
|
||||||
'article' => 'Article',
|
|
||||||
'card' => 'Card',
|
|
||||||
'text' => 'Text',
|
|
||||||
'image' => 'Image',
|
|
||||||
])
|
|
||||||
->default('article')
|
|
||||||
->required()
|
|
||||||
->live(),
|
|
||||||
TextInput::make('title')
|
TextInput::make('title')
|
||||||
->required()
|
->required()
|
||||||
->live(onBlur: true)
|
->live(onBlur: true)
|
||||||
|
|
@ -41,283 +29,32 @@ class EntryForm
|
||||||
}),
|
}),
|
||||||
TextInput::make('slug')
|
TextInput::make('slug')
|
||||||
->required()
|
->required()
|
||||||
->visible(fn($get) => $get('type') === 'article')
|
|
||||||
->dehydrated()
|
->dehydrated()
|
||||||
->readOnly(),
|
->readOnly(),
|
||||||
Textarea::make('description')
|
Textarea::make('description')
|
||||||
->visible(fn($get) => $get('type') === 'article')
|
|
||||||
->columnSpanFull(),
|
|
||||||
SpatieTagsInput::make('tags')
|
|
||||||
->type('entry-tags')
|
|
||||||
->visible(fn($get) => $get('type') === 'article')
|
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
SpatieMediaLibraryFileUpload::make('featured_image')
|
SpatieMediaLibraryFileUpload::make('featured_image')
|
||||||
->multiple() // <- force array handling for Filament v4 bug
|
|
||||||
->visible(
|
|
||||||
fn($get) =>
|
|
||||||
$get('type') === 'article' || $get('type') === 'image'
|
|
||||||
)
|
|
||||||
->collection('featured-image')
|
->collection('featured-image')
|
||||||
->image()
|
->image()
|
||||||
->imageEditor()
|
->imageEditor()
|
||||||
->disk(config('media-library.disk_name', 'public'))
|
->disk('public')
|
||||||
->visibility('public')
|
->visibility('public')
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->dehydrated(false)
|
->dehydrated(false)
|
||||||
->saveUploadedFileUsing(function ($file, $record) {
|
|
||||||
if (is_array($file)) {
|
|
||||||
$file = reset($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate upload object early
|
|
||||||
if (
|
|
||||||
! is_object($file) ||
|
|
||||||
! (method_exists($file, 'getRealPath') || method_exists($file, 'getPathname') || method_exists($file, 'getStream') || method_exists($file, 'store'))
|
|
||||||
) {
|
|
||||||
Log::error('Invalid upload object', ['type' => gettype($file)]);
|
|
||||||
throw new \Exception('Invalid upload object provided to saveUploadedFileUsing');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use safe variables for further calls
|
|
||||||
$realPath = method_exists($file, 'getRealPath') ? $file->getRealPath() : null;
|
|
||||||
$exists = $realPath ? file_exists($realPath) : false;
|
|
||||||
$name = method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : null;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Log::info('TemporaryUploadedFile Debug', [
|
|
||||||
'path' => $file->getRealPath(),
|
|
||||||
'exists' => file_exists($file->getRealPath()),
|
|
||||||
'name' => $file->getClientOriginalName(),
|
|
||||||
'temp_dir' => sys_get_temp_dir(),
|
|
||||||
'disk_root' => config('filesystems.disks.local.root'),
|
|
||||||
'is_readable' => is_readable($file->getRealPath()),
|
|
||||||
'is_writable' => is_writable($file->getRealPath()),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Additional debug: Check if the file is being moved to livewire-tmp
|
|
||||||
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
|
||||||
Log::info('Livewire Temp Directory Debug', [
|
|
||||||
'livewire_tmp_path' => $livewireTmpPath,
|
|
||||||
'exists' => file_exists($livewireTmpPath),
|
|
||||||
'is_writable' => is_writable($livewireTmpPath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check if the file is being moved
|
|
||||||
$tempFilePath = $file->getRealPath();
|
|
||||||
$newFilePath = $livewireTmpPath . '/' . $file->getClientOriginalName();
|
|
||||||
if (file_exists($tempFilePath)) {
|
|
||||||
Log::info('File exists in temp directory', ['temp_file_path' => $tempFilePath]);
|
|
||||||
} else {
|
|
||||||
Log::error('File does not exist in temp directory', ['temp_file_path' => $tempFilePath]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// $diskName = config('media-library.disk_name', 'public');
|
|
||||||
$diskName = config('media-library.disk_name');
|
|
||||||
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('Featured Image Upload Debug', [
|
|
||||||
'disk' => $diskName,
|
|
||||||
'file_name' => $file->getClientOriginalName(),
|
|
||||||
'file_size' => $file->getSize(),
|
|
||||||
'file_mime' => $file->getMimeType(),
|
|
||||||
'file_path' => $file->getRealPath(),
|
|
||||||
'record_id' => $record?->id,
|
|
||||||
'aws_config' => [
|
|
||||||
'bucket' => config('filesystems.disks.s3.bucket'),
|
|
||||||
'region' => config('filesystems.disks.s3.region'),
|
|
||||||
'key_exists' => !empty(config('filesystems.disks.s3.key')),
|
|
||||||
'secret_exists' => !empty(config('filesystems.disks.s3.secret')),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!$record) {
|
|
||||||
throw new \Exception('Record not found during upload');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test S3 connection if using S3
|
|
||||||
if ($diskName === 's3') {
|
|
||||||
$disk = \Storage::disk('s3');
|
|
||||||
|
|
||||||
// Test basic S3 connectivity
|
|
||||||
$testFile = 'test-' . time() . '.txt';
|
|
||||||
$disk->put($testFile, 'test content');
|
|
||||||
$disk->delete($testFile);
|
|
||||||
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('S3 connectivity test passed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use addMedia with the file directly, not addMediaFromRequest
|
|
||||||
// Generate secure filename similar to Livewire temp files
|
|
||||||
$originalName = $file->getClientOriginalName();
|
|
||||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
|
||||||
$baseName = pathinfo($originalName, PATHINFO_FILENAME);
|
|
||||||
|
|
||||||
// Generate secure filename with encoded original name
|
|
||||||
$encodedName = base64_encode($originalName);
|
|
||||||
$secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Resolve possibly-relative Livewire temp path + safe fallbacks
|
|
||||||
$realPath = $realPath ?: (method_exists($file, 'getRealPath') ? $file->getRealPath() : null);
|
|
||||||
$candidates = [];
|
|
||||||
|
|
||||||
if ($realPath && str_starts_with($realPath, '/')) {
|
|
||||||
$candidates[] = $realPath;
|
|
||||||
} else {
|
|
||||||
$candidates[] = sys_get_temp_dir() . '/' . ltrim((string)$realPath, '/');
|
|
||||||
$candidates[] = storage_path('framework/' . ltrim((string)$realPath, '/'));
|
|
||||||
$candidates[] = storage_path(ltrim((string)$realPath, '/'));
|
|
||||||
$candidates[] = base_path(ltrim((string)$realPath, '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($realPath) {
|
|
||||||
$candidates[] = storage_path('framework/livewire-tmp/' . basename($realPath));
|
|
||||||
$candidates[] = sys_get_temp_dir() . '/' . basename($realPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) Try storing to local disk (creates an absolute path we control)
|
|
||||||
$stored = null;
|
|
||||||
if (method_exists($file, 'store')) {
|
|
||||||
try {
|
|
||||||
$stored = $file->store('livewire-temp', 'local'); // storage/app/livewire-temp/...
|
|
||||||
if ($stored && \Storage::disk('local')->exists($stored)) {
|
|
||||||
$resolved = \Storage::disk('local')->path($stored);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::debug('store() fallback failed', ['err' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) If not resolved, check candidates
|
|
||||||
if (! isset($resolved)) {
|
|
||||||
foreach ($candidates as $p) {
|
|
||||||
if ($p && file_exists($p)) {
|
|
||||||
$resolved = $p;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) If still not resolved, try stream -> temp file copy
|
|
||||||
$is_tmp_copy = false;
|
|
||||||
if (! isset($resolved)) {
|
|
||||||
try {
|
|
||||||
$stream = null;
|
|
||||||
if (method_exists($file, 'getStream')) {
|
|
||||||
$stream = $file->getStream();
|
|
||||||
} elseif (method_exists($file, 'getRealPath') && is_readable($file->getRealPath())) {
|
|
||||||
$stream = fopen($file->getRealPath(), 'r');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stream) {
|
|
||||||
$tmpPath = tempnam(sys_get_temp_dir(), 'filament-upload-');
|
|
||||||
$out = fopen($tmpPath, 'w');
|
|
||||||
stream_copy_to_stream($stream, $out);
|
|
||||||
fclose($out);
|
|
||||||
if (is_resource($stream)) {
|
|
||||||
@fclose($stream);
|
|
||||||
}
|
|
||||||
$resolved = $tmpPath;
|
|
||||||
$is_tmp_copy = true;
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::debug('stream fallback failed', ['err' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Still nothing -> error
|
|
||||||
if (empty($resolved)) {
|
|
||||||
Log::error('Featured Image Upload: could not resolve temp path', [
|
|
||||||
'original' => $realPath,
|
|
||||||
'checked_candidates' => $candidates,
|
|
||||||
'stored' => $stored,
|
|
||||||
]);
|
|
||||||
throw new \Exception("File `{$realPath}` does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) Use resolved absolute path
|
|
||||||
$media = $record->addMedia($resolved)
|
|
||||||
->usingName($baseName)
|
|
||||||
->usingFileName($secureFileName)
|
|
||||||
->toMediaCollection('featured-image', $diskName);
|
|
||||||
|
|
||||||
// 6) Cleanup short-lived artifacts
|
|
||||||
if (! empty($is_tmp_copy) && file_exists($resolved)) {
|
|
||||||
@unlink($resolved);
|
|
||||||
}
|
|
||||||
if (! empty($stored) && \Storage::disk('local')->exists($stored)) {
|
|
||||||
\Storage::disk('local')->delete($stored);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('Featured image resolved', ['resolved' => $resolved, 'media_id' => $media->id ?? null]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('Featured Image Upload Success', [
|
|
||||||
'media_id' => $media->id,
|
|
||||||
'media_url' => $media->getUrl(),
|
|
||||||
'media_path' => $media->getPathRelativeToRoot(),
|
|
||||||
'disk' => $media->disk
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $media->getUrl();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Featured Image Upload Failed', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
'disk' => $diskName,
|
|
||||||
'file_name' => $file->getClientOriginalName()
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
// Also show error to user
|
|
||||||
\Filament\Notifications\Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title('Upload Failed')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->hintAction(
|
->hintAction(
|
||||||
Action::make('featured_picker')
|
Action::make('featured_picker')
|
||||||
->label('Featured Image from Gallery')
|
->label('Pick from Gallery')
|
||||||
->icon('heroicon-m-photo')
|
->icon('heroicon-m-photo')
|
||||||
->extraAttributes(['id' => 'featured-picker-button'])
|
|
||||||
->schema([
|
->schema([
|
||||||
Select::make('image_id')
|
Select::make('image_id')
|
||||||
->label('Select an existing image')
|
->label('Select an existing image')
|
||||||
->allowHtml()
|
->allowHtml()
|
||||||
->options(function () {
|
->options(function () {
|
||||||
$currentDisk = config('media-library.disk_name', 'public');
|
return Media::where('model_type', 'temp')
|
||||||
return Media::where('disk', $currentDisk)
|
->where('model_id', 0)
|
||||||
|
->where('disk', 'public')
|
||||||
->latest()
|
->latest()
|
||||||
->limit(50)
|
->limit(30)
|
||||||
->get(['id', 'file_name', 'name', 'disk'])
|
->get(['id', 'file_name', 'name', 'disk'])
|
||||||
->mapWithKeys(function (Media $item) {
|
->mapWithKeys(function (Media $item) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -330,7 +67,7 @@ class EntryForm
|
||||||
"<div class='flex flex-col'>" .
|
"<div class='flex flex-col'>" .
|
||||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||||
'</div></div>';
|
"</div></div>";
|
||||||
|
|
||||||
return [$item->id => $html];
|
return [$item->id => $html];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -344,22 +81,12 @@ class EntryForm
|
||||||
])
|
])
|
||||||
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
|
->action(function (array $data, SpatieMediaLibraryFileUpload $component): void {
|
||||||
$record = $component->getRecord();
|
$record = $component->getRecord();
|
||||||
$diskName = config('media-library.disk_name', 'public');
|
|
||||||
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('Featured Image Picker Action Debug', [
|
|
||||||
'disk' => $diskName,
|
|
||||||
'record_id' => $record?->id,
|
|
||||||
'image_id' => $data['image_id'] ?? null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$record) {
|
if (!$record) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->warning()
|
->warning()
|
||||||
->title('Save the entry first')
|
->title('Save the entry first')
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,47 +95,19 @@ class EntryForm
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceMedia = Media::find($data['image_id']);
|
$sourceMedia = Media::find($data['image_id']);
|
||||||
if (! $sourceMedia) {
|
if (!$sourceMedia || !file_exists($sourceMedia->getPath())) {
|
||||||
Log::error('Source media not found', ['image_id' => $data['image_id']]);
|
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
->title('Source image not found in database')
|
->title('Image file not found')
|
||||||
->send();
|
->send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// For S3, we need to handle file copying differently
|
|
||||||
if ($sourceMedia->disk === 's3') {
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('Copying S3 media to new collection', [
|
|
||||||
'source_disk' => $sourceMedia->disk,
|
|
||||||
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
|
||||||
'target_disk' => $diskName
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy from S3 to S3 or download temporarily
|
|
||||||
$sourceDisk = \Storage::disk($sourceMedia->disk);
|
|
||||||
$sourceContent = $sourceDisk->get($sourceMedia->getPathRelativeToRoot());
|
|
||||||
|
|
||||||
if (!$sourceContent) {
|
|
||||||
throw new \Exception('Could not read source file from S3');
|
|
||||||
}
|
|
||||||
|
|
||||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
|
||||||
file_put_contents($tempCopy, $sourceContent);
|
|
||||||
} else {
|
|
||||||
// Local file handling
|
|
||||||
$sourceFile = $sourceMedia->getPath();
|
$sourceFile = $sourceMedia->getPath();
|
||||||
if (! file_exists($sourceFile)) {
|
|
||||||
throw new \Exception('Source file not found on disk');
|
|
||||||
}
|
|
||||||
|
|
||||||
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
$tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name;
|
||||||
copy($sourceFile, $tempCopy);
|
copy($sourceFile, $tempCopy);
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
// Verify record has ID
|
// Verify record has ID
|
||||||
if (!$record->id) {
|
if (!$record->id) {
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
|
|
@ -422,15 +121,7 @@ class EntryForm
|
||||||
$newMedia = $record->addMedia($tempCopy)
|
$newMedia = $record->addMedia($tempCopy)
|
||||||
->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME))
|
->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME))
|
||||||
->usingFileName($sourceMedia->file_name)
|
->usingFileName($sourceMedia->file_name)
|
||||||
->toMediaCollection('featured-image', $diskName);
|
->toMediaCollection('featured-image', 'public');
|
||||||
|
|
||||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
|
||||||
Log::info('Featured Image Picker Success', [
|
|
||||||
'new_media_id' => $newMedia->id,
|
|
||||||
'new_media_disk' => $newMedia->disk,
|
|
||||||
'new_media_url' => $newMedia->getUrl()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch event for app.js to handle
|
// Dispatch event for app.js to handle
|
||||||
$component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]);
|
$component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]);
|
||||||
|
|
@ -440,20 +131,12 @@ class EntryForm
|
||||||
->title('Image added to featured image')
|
->title('Image added to featured image')
|
||||||
->send();
|
->send();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Featured Image Picker Failed', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
'source_media_id' => $data['image_id'],
|
|
||||||
'disk' => $diskName
|
|
||||||
]);
|
|
||||||
|
|
||||||
\Filament\Notifications\Notification::make()
|
\Filament\Notifications\Notification::make()
|
||||||
->danger()
|
->danger()
|
||||||
->title('Error: ' . $e->getMessage())
|
->title('Error: ' . $e->getMessage())
|
||||||
->persistent()
|
|
||||||
->send();
|
->send();
|
||||||
} finally {
|
} finally {
|
||||||
if (isset($tempCopy) && file_exists($tempCopy)) {
|
if (file_exists($tempCopy)) {
|
||||||
unlink($tempCopy);
|
unlink($tempCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -463,30 +146,8 @@ class EntryForm
|
||||||
->required(),
|
->required(),
|
||||||
Toggle::make('is_featured')
|
Toggle::make('is_featured')
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('priority')
|
DatePicker::make('published_at'),
|
||||||
->label('Priority')
|
|
||||||
->numeric()
|
|
||||||
->default(0)
|
|
||||||
->required(),
|
|
||||||
DatePicker::make('published_at')
|
|
||||||
->visible(fn($get) => $get('type') === 'article'),
|
|
||||||
Select::make('category_id')
|
|
||||||
->label('Category')
|
|
||||||
->options(function () {
|
|
||||||
return Category::all()
|
|
||||||
->pluck('name', 'id')
|
|
||||||
->toArray();
|
|
||||||
})
|
|
||||||
->searchable(),
|
|
||||||
TextInput::make('call_to_action_text')
|
|
||||||
->label('Call to Action Text')
|
|
||||||
->visible(fn($get) => $get('type') !== 'article'),
|
|
||||||
TextInput::make('call_to_action_link')
|
|
||||||
->label('Call to Action URL')
|
|
||||||
->visible(fn($get) => $get('type') !== 'article'),
|
|
||||||
|
|
||||||
RichEditor::make('content')
|
RichEditor::make('content')
|
||||||
->visible(fn($get) => $get('type') !== 'image')
|
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->hintAction(
|
->hintAction(
|
||||||
Action::make('picker')
|
Action::make('picker')
|
||||||
|
|
@ -521,7 +182,7 @@ class EntryForm
|
||||||
"<div class='flex flex-col'>" .
|
"<div class='flex flex-col'>" .
|
||||||
"<span class='font-medium text-sm'>{$name}</span>" .
|
"<span class='font-medium text-sm'>{$name}</span>" .
|
||||||
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
"<span class='text-xs text-gray-500'>{$fileName}</span>" .
|
||||||
'</div></div>';
|
"</div></div>";
|
||||||
|
|
||||||
return [$url => $html];
|
return [$url => $html];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
|
||||||
|
|
@ -17,33 +17,18 @@ class EntriesTable
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('id')
|
|
||||||
->label('ID')
|
|
||||||
->sortable(query: function ($query, $direction) {
|
|
||||||
$query->orderBy('id', $direction);
|
|
||||||
}),
|
|
||||||
SpatieMediaLibraryImageColumn::make('featured_image')
|
SpatieMediaLibraryImageColumn::make('featured_image')
|
||||||
->collection('featured-image')
|
->collection('featured-image')
|
||||||
->label('Image')
|
|
||||||
->circular()
|
->circular()
|
||||||
->stacked()
|
->stacked()
|
||||||
->limit(3),
|
->limit(3),
|
||||||
TextColumn::make('title')
|
TextColumn::make('title')
|
||||||
->searchable()
|
->searchable(),
|
||||||
->sortable(),
|
TextColumn::make('slug')
|
||||||
TextColumn::make('category.name')
|
|
||||||
->label('Category')
|
|
||||||
->sortable(query: function ($query, $direction) {
|
|
||||||
$query->join('categories', 'entries.category_id', '=', 'categories.id')
|
|
||||||
->orderBy('categories.name', $direction)
|
|
||||||
->select('entries.*');
|
|
||||||
})
|
|
||||||
->searchable(),
|
->searchable(),
|
||||||
IconColumn::make('is_published')
|
IconColumn::make('is_published')
|
||||||
->label('pub')
|
|
||||||
->boolean(),
|
->boolean(),
|
||||||
IconColumn::make('is_featured')
|
IconColumn::make('is_featured')
|
||||||
->label('feat')
|
|
||||||
->boolean(),
|
->boolean(),
|
||||||
TextColumn::make('published_at')
|
TextColumn::make('published_at')
|
||||||
->date()
|
->date()
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@ class MediaResource extends Resource
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'file_name';
|
protected static ?string $recordTitleAttribute = 'file_name';
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::Photo
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class ListMedia extends ListRecords
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// CreateAction::make(),
|
CreateAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ use Filament\Forms\Components\Hidden;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class MediaForm
|
class MediaForm
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +24,6 @@ class MediaForm
|
||||||
Hidden::make('disk')
|
Hidden::make('disk')
|
||||||
->default('public'),
|
->default('public'),
|
||||||
FileUpload::make('file')
|
FileUpload::make('file')
|
||||||
->multiple() // workaround for Filament v4 single-file bug
|
|
||||||
->label('File')
|
->label('File')
|
||||||
->imageEditor()
|
->imageEditor()
|
||||||
->imageEditorAspectRatios([
|
->imageEditorAspectRatios([
|
||||||
|
|
@ -35,23 +32,19 @@ class MediaForm
|
||||||
'1:1',
|
'1:1',
|
||||||
])
|
])
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->disk('s3')
|
->disk('public')
|
||||||
->directory('media')
|
->directory('media')
|
||||||
->visibility('public')
|
->visibility('public')
|
||||||
->acceptedFileTypes(['image/*', 'application/pdf'])
|
->acceptedFileTypes(['image/*', 'application/pdf'])
|
||||||
->maxSize(10240)
|
->maxSize(10240)
|
||||||
->required(fn ($context) => $context === 'create')
|
->required(fn ($context) => $context === 'create')
|
||||||
|
|
||||||
|
|
||||||
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
||||||
Log::info('MediaForm afterStateHydrated invoked', ['record_id' => $record?->id, 'state' => $state]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (! $record) {
|
if (! $record) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$media = $record;
|
$media = $record;
|
||||||
|
|
||||||
if (! $media instanceof SpatieMedia) {
|
if (! $media instanceof SpatieMedia) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -59,32 +52,8 @@ class MediaForm
|
||||||
// Construct the correct path: {media_id}/{filename}
|
// Construct the correct path: {media_id}/{filename}
|
||||||
$path = $media->id.'/'.$media->file_name;
|
$path = $media->id.'/'.$media->file_name;
|
||||||
|
|
||||||
try {
|
|
||||||
$disk = $media->disk ?? 'public';
|
|
||||||
if (Storage::disk($disk)->exists($path)) {
|
|
||||||
$component->state($path);
|
$component->state($path);
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('MediaForm afterStateHydrated storage check failed', [
|
|
||||||
'err' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
'disk' => $media->disk ?? null,
|
|
||||||
'path' => $path,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('MediaForm afterStateHydrated unhandled', [
|
|
||||||
'err' => $e->getMessage(),
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
'record' => $record?->id,
|
|
||||||
'state' => $state,
|
|
||||||
]);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\Media\Tables;
|
namespace App\Filament\Resources\Media\Tables;
|
||||||
|
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Actions\BulkActionGroup;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
|
@ -24,8 +23,7 @@ class MediaTable
|
||||||
->columns([
|
->columns([
|
||||||
ImageColumn::make('url')
|
ImageColumn::make('url')
|
||||||
->label('Preview')
|
->label('Preview')
|
||||||
->getStateUsing(
|
->getStateUsing(fn ($record) =>
|
||||||
fn($record) =>
|
|
||||||
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
||||||
// fall back to Spatie's getUrl() when no stored_path exists.
|
// fall back to Spatie's getUrl() when no stored_path exists.
|
||||||
($record->getCustomProperty('stored_path'))
|
($record->getCustomProperty('stored_path'))
|
||||||
|
|
@ -54,7 +52,7 @@ class MediaTable
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
// EditAction::make(),
|
EditAction::make(),
|
||||||
DeleteAction::make()
|
DeleteAction::make()
|
||||||
->action(function (Media $record) {
|
->action(function (Media $record) {
|
||||||
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
||||||
|
|
@ -69,13 +67,6 @@ class MediaTable
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->toolbarActions([
|
||||||
|
|
||||||
Action::make('add')
|
|
||||||
->label('Add')
|
|
||||||
->icon('heroicon-o-plus')
|
|
||||||
->url(fn() => url('admin/assets/create'))
|
|
||||||
->color('primary'),
|
|
||||||
|
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make()
|
DeleteBulkAction::make()
|
||||||
->action(function (Collection $records) {
|
->action(function (Collection $records) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
|
||||||
|
|
||||||
class CreateTextWidget extends CreateRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = TextWidgetResource::class;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditTextWidget extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = TextWidgetResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
DeleteAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TextWidgets\TextWidgetResource;
|
|
||||||
use Filament\Actions\CreateAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListTextWidgets extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = TextWidgetResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
CreateAction::make(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Schemas;
|
|
||||||
|
|
||||||
use App\Models\Category;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
|
|
||||||
class TextWidgetForm
|
|
||||||
{
|
|
||||||
public static function configure(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
TextInput::make('title')
|
|
||||||
->required()
|
|
||||||
->live(onBlur: true),
|
|
||||||
TextInput::make('description')
|
|
||||||
->nullable(),
|
|
||||||
Textarea::make('content')
|
|
||||||
->rows(5)
|
|
||||||
->columnSpanFull(),
|
|
||||||
Select::make('category_id')
|
|
||||||
->label('Category')
|
|
||||||
->options(function () {
|
|
||||||
return Category::all()
|
|
||||||
->pluck('name', 'id')
|
|
||||||
->toArray();
|
|
||||||
})
|
|
||||||
->searchable(),
|
|
||||||
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets\Tables;
|
|
||||||
|
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class TextWidgetsTable
|
|
||||||
{
|
|
||||||
public static function configure(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('title')
|
|
||||||
->label('Title')
|
|
||||||
->sortable()
|
|
||||||
->searchable(),
|
|
||||||
TextColumn::make('created_at')
|
|
||||||
->label('Created')
|
|
||||||
->dateTime('M d, Y H:i')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('updated_at')
|
|
||||||
->label('Updated')
|
|
||||||
->dateTime('M d, Y H:i')
|
|
||||||
->sortable(),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->recordActions([
|
|
||||||
EditAction::make(),
|
|
||||||
])
|
|
||||||
->toolbarActions([
|
|
||||||
BulkActionGroup::make([
|
|
||||||
DeleteBulkAction::make(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\TextWidgets;
|
|
||||||
|
|
||||||
use App\Filament\Resources\TextWidgets\Pages\CreateTextWidget;
|
|
||||||
use App\Filament\Resources\TextWidgets\Pages\EditTextWidget;
|
|
||||||
use App\Filament\Resources\TextWidgets\Pages\ListTextWidgets;
|
|
||||||
use App\Filament\Resources\TextWidgets\Schemas\TextWidgetForm;
|
|
||||||
use App\Filament\Resources\TextWidgets\Tables\TextWidgetsTable;
|
|
||||||
use App\Models\TextWidget;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Icons\Heroicon;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
|
|
||||||
class TextWidgetResource extends Resource
|
|
||||||
{
|
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $model = TextWidget::class;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'title';
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return TextWidgetForm::configure($schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return TextWidgetsTable::configure($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => ListTextWidgets::route('/'),
|
|
||||||
'create' => CreateTextWidget::route('/create'),
|
|
||||||
'edit' => EditTextWidget::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\StoreCategoryRequest;
|
|
||||||
use App\Http\Requests\UpdateCategoryRequest;
|
|
||||||
use App\Http\Resources\CategoryResource;
|
|
||||||
use App\Models\Category;
|
|
||||||
|
|
||||||
class CategoryController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
return CategoryResource::collection(Category::all());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(StoreCategoryRequest $request)
|
|
||||||
{
|
|
||||||
return new CategoryResource(Category::create($request->validated()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(Category $category)
|
|
||||||
{
|
|
||||||
return new CategoryResource($category);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(UpdateCategoryRequest $request, Category $category)
|
|
||||||
{
|
|
||||||
$category->update($request->validated());
|
|
||||||
|
|
||||||
return new CategoryResource($category);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(Category $category)
|
|
||||||
{
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
if (! $user || $user->email !== config('app.admin_email')) {
|
|
||||||
return response()->json(['message' => 'Forbidden'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$category->delete();
|
|
||||||
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Resources\EntryResource;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class EntryController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
return EntryResource::collection(Entry::with('category')->get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user || $user->email !== config('app.admin_email')) {
|
|
||||||
return response()->json(['message' => 'Forbidden'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validate([
|
|
||||||
'title' => 'required|string|max:255',
|
|
||||||
'content' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$validated['slug'] = $this->generateUniqueSlug($validated['title']);
|
|
||||||
|
|
||||||
return new EntryResource(Entry::create($validated));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateUniqueSlug(string $title): string
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
$slug = Str::slug($title).'-'.Str::random(8);
|
|
||||||
} while (Entry::where('slug', $slug)->exists());
|
|
||||||
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(Entry $entry)
|
|
||||||
{
|
|
||||||
$this->authorize('view', $entry);
|
|
||||||
|
|
||||||
return new EntryResource($entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, Entry $entry)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'title' => 'sometimes|required|string|max:255',
|
|
||||||
'content' => 'sometimes|required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$entry->update($validated);
|
|
||||||
|
|
||||||
return new EntryResource($entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(Entry $entry)
|
|
||||||
{
|
|
||||||
$entry->delete();
|
|
||||||
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return Auth::check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Events\PreviewSiteBuilt;
|
|
||||||
use App\Http\Requests\SendPreviewSiteBuiltNotificationRequest;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
class NotificationController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Send preview site built notification.
|
|
||||||
*/
|
|
||||||
public function sendPreviewSiteBuilt(SendPreviewSiteBuiltNotificationRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$message = $request->validated()['message'];
|
|
||||||
|
|
||||||
PreviewSiteBuilt::dispatch($message, 'success');
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Preview site built notification sent successfully',
|
|
||||||
'data' => [
|
|
||||||
'notification_message' => $message,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Resources\TextWidgetResource;
|
|
||||||
use App\Models\TextWidget;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class TextWidgetController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
return response()->json([
|
|
||||||
'data' => TextWidget::with('category')->get()->map(fn ($tw) => new TextWidgetResource($tw))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user || $user->email !== config('app.admin_email')) {
|
|
||||||
return response()->json(['message' => 'Forbidden'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validate([
|
|
||||||
'title' => 'required|string|max:255',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'content' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new TextWidgetResource(TextWidget::create($validated));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(TextWidget $textWidget)
|
|
||||||
{
|
|
||||||
return new TextWidgetResource($textWidget->load('category'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, TextWidget $textWidget)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'title' => 'sometimes|required|string|max:255',
|
|
||||||
'description' => 'sometimes|nullable|string',
|
|
||||||
'content' => 'sometimes|required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$textWidget->update($validated);
|
|
||||||
|
|
||||||
return new TextWidgetResource($textWidget->load('category'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(TextWidget $textWidget)
|
|
||||||
{
|
|
||||||
$textWidget->delete();
|
|
||||||
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class SendPreviewSiteBuiltNotificationRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'message' => 'required|string|max:255',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreCategoryRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'required|string|max:255|unique:categories,name',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreEntryRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'title' => 'required|string|max:255',
|
|
||||||
'content' => 'required|string',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpdateCategoryRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'sometimes|required|string|max:255|unique:categories,name,'.$this->category->id,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpdateEntryRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'title' => 'sometimes|required|string|max:255',
|
|
||||||
'content' => 'sometimes|required|string',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class CategoryResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Transform the resource into an array.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'name' => $this->name,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class EntryResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Transform the resource into an array.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'title' => $this->title,
|
|
||||||
'slug' => $this->slug,
|
|
||||||
'description' => $this->description,
|
|
||||||
'is_published' => $this->is_published,
|
|
||||||
'is_featured' => $this->is_featured,
|
|
||||||
'published_at' => $this->published_at,
|
|
||||||
'content' => $this->content,
|
|
||||||
'category' => $this->category->name ?? null,
|
|
||||||
'featured_image_url' => $this->getFirstMediaUrl('featured-image') ?: null,
|
|
||||||
'call_to_action_text' => $this->call_to_action_text,
|
|
||||||
'call_to_action_link' => $this->call_to_action_link,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
'priority' => $this->priority,
|
|
||||||
'type' => $this->type,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class TextWidgetResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Transform the resource into an array.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'title' => $this->title,
|
|
||||||
'description' => $this->description,
|
|
||||||
'content' => $this->content,
|
|
||||||
'category' => $this->category->name ?? null,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Services\ProcessUpdateService;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class ProcessChangeUpdate implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public int $changeId,
|
|
||||||
public string $action,
|
|
||||||
public string $type = 'change_update_'
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the job.
|
|
||||||
*/
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
Log::info("Processing change update: changeId={$this->changeId}, action={$this->action}");
|
|
||||||
(new ProcessUpdateService)->processUpdate($this->changeId, $this->action, $this->type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Services\ProcessUpdateService;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class ProcessEntryUpdate implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public int $entryId,
|
|
||||||
public string $action,
|
|
||||||
public string $type = 'entry_update_'
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the job.
|
|
||||||
*/
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
(new ProcessUpdateService)->processUpdate($this->entryId, $this->action, $this->type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
|
||||||
|
|
||||||
class Asset extends Model implements HasMedia
|
|
||||||
{
|
|
||||||
use InteractsWithMedia;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'alt_text',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Category extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = ['name'];
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Change extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = [
|
|
||||||
'note',
|
|
||||||
'user_id',
|
|
||||||
'type',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,38 +5,27 @@ namespace App\Models;
|
||||||
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
||||||
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
||||||
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\Tags\HasTags;
|
|
||||||
|
|
||||||
/*
|
class Entry extends Model implements HasRichContent, HasMedia
|
||||||
Entry model with rich content and media library integration
|
|
||||||
This is the main article / blog rich content model
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Entry extends Model implements HasMedia, HasRichContent
|
|
||||||
{
|
{
|
||||||
use HasFactory, HasTags, InteractsWithMedia, InteractsWithRichContent;
|
|
||||||
|
use InteractsWithMedia, InteractsWithRichContent;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'title',
|
'title',
|
||||||
'type',
|
|
||||||
'slug',
|
'slug',
|
||||||
'description',
|
'description',
|
||||||
'is_published',
|
'is_published',
|
||||||
'is_featured',
|
'is_featured',
|
||||||
'published_at',
|
'published_at',
|
||||||
'content',
|
'content',
|
||||||
'call_to_action_link',
|
|
||||||
'call_to_action_text',
|
|
||||||
'category_id',
|
|
||||||
'priority',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up rich content configuration for media library integration
|
* Set up rich content configuration for media library integration
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,19 +39,4 @@ class Entry extends Model implements HasMedia, HasRichContent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function boot()
|
|
||||||
{
|
|
||||||
parent::boot();
|
|
||||||
|
|
||||||
static::creating(function ($entry) {
|
|
||||||
if (empty($entry->slug)) {
|
|
||||||
$entry->slug = Str::slug($entry->title);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function category(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Category::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class TextWidget extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'title',
|
|
||||||
'description',
|
|
||||||
'content',
|
|
||||||
'category_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function category(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Category::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,20 +3,16 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
|
||||||
use Filament\Panel;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
|
||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
@ -65,17 +61,4 @@ class User extends Authenticatable implements FilamentUser
|
||||||
->map(fn ($word) => Str::substr($word, 0, 1))
|
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||||
->implode('');
|
->implode('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the user can access Filament admin panel.
|
|
||||||
*/
|
|
||||||
public function canAccessPanel(Panel $panel): bool
|
|
||||||
{
|
|
||||||
return $this->email === config('app.admin_email') || $this->role === 'admin';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function changes()
|
|
||||||
{
|
|
||||||
return $this->hasMany(Change::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Jobs\ProcessChangeUpdate;
|
|
||||||
use App\Models\Change;
|
|
||||||
|
|
||||||
class ChangeObserver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the Change "created" event.
|
|
||||||
*/
|
|
||||||
public function created(Change $change): void
|
|
||||||
{
|
|
||||||
ProcessChangeUpdate::dispatch($change->id, 'created');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Change "updated" event.
|
|
||||||
*/
|
|
||||||
public function updated(Change $change): void
|
|
||||||
{
|
|
||||||
ProcessChangeUpdate::dispatch($change->id, 'updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Change "deleted" event.
|
|
||||||
*/
|
|
||||||
public function deleted(Change $change): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Change "restored" event.
|
|
||||||
*/
|
|
||||||
public function restored(Change $change): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Change "force deletecURLd" event.
|
|
||||||
*/
|
|
||||||
public function forceDeleted(Change $change): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Jobs\ProcessEntryUpdate;
|
|
||||||
use App\Models\Entry;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class EntryObserver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the Entry "created" event.
|
|
||||||
*/
|
|
||||||
public function created(Entry $entry): void
|
|
||||||
{
|
|
||||||
ProcessEntryUpdate::dispatch($entry->id, 'created');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Entry "updated" event.
|
|
||||||
*/
|
|
||||||
public function updated(Entry $entry): void
|
|
||||||
{
|
|
||||||
ProcessEntryUpdate::dispatch($entry->id, 'updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Entry "deleted" event.
|
|
||||||
*/
|
|
||||||
public function deleted(Entry $entry): void
|
|
||||||
{
|
|
||||||
ProcessEntryUpdate::dispatch($entry->id, 'deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Entry "restored" event.
|
|
||||||
*/
|
|
||||||
public function restored(Entry $entry): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the Entry "force deleted" event.
|
|
||||||
*/
|
|
||||||
public function forceDeleted(Entry $entry): void
|
|
||||||
{
|
|
||||||
// ProcessEntryUpdate::dispatch($entry, 'force deleted');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,12 +2,7 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\Entry;
|
|
||||||
use App\Observers\EntryObserver;
|
|
||||||
use App\Models\Change;
|
|
||||||
use App\Observers\ChangeObserver;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -24,13 +19,6 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
// Ensure the livewire-tmp directory exists
|
//
|
||||||
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
|
||||||
if (!File::exists($livewireTmpPath)) {
|
|
||||||
File::makeDirectory($livewireTmpPath, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry::observe(EntryObserver::class);
|
|
||||||
Change::observe(ChangeObserver::class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,39 +25,14 @@ class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
public function panel(Panel $panel): Panel
|
public function panel(Panel $panel): Panel
|
||||||
{
|
{
|
||||||
$previewSiteUrl = env('PREVIEW_SITE_URL');
|
|
||||||
$liveSiteUrl = env('LIVE_SITE_URL');
|
|
||||||
|
|
||||||
$navigationItems = [];
|
|
||||||
if ($previewSiteUrl && $liveSiteUrl) {
|
|
||||||
$navigationItems = [
|
|
||||||
\Filament\Navigation\NavigationItem::make('Preview Site')
|
|
||||||
->url($previewSiteUrl, shouldOpenInNewTab: true)
|
|
||||||
->icon('heroicon-o-eye')
|
|
||||||
->group('External Links')
|
|
||||||
->sort(1),
|
|
||||||
\Filament\Navigation\NavigationItem::make('Live Site')
|
|
||||||
->url($liveSiteUrl, shouldOpenInNewTab: true)
|
|
||||||
->icon('heroicon-o-rocket-launch')
|
|
||||||
->group('External Links')
|
|
||||||
->sort(2),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $panel
|
return $panel
|
||||||
->default()
|
->default()
|
||||||
->sidebarCollapsibleOnDesktop()
|
|
||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->login()
|
->login()
|
||||||
->colors([
|
->colors([
|
||||||
'primary' => Color::Blue,
|
'primary' => Color::Blue,
|
||||||
])
|
])
|
||||||
->resources([
|
|
||||||
\App\Filament\Resources\Entries\EntryResource::class,
|
|
||||||
\App\Filament\Resources\Media\MediaResource::class,
|
|
||||||
\App\Filament\Resources\Categroys\CategroyResource::class,
|
|
||||||
])
|
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
|
|
@ -68,7 +43,6 @@ class AdminPanelProvider extends PanelProvider
|
||||||
AccountWidget::class,
|
AccountWidget::class,
|
||||||
FilamentInfoWidget::class,
|
FilamentInfoWidget::class,
|
||||||
])
|
])
|
||||||
->navigationItems($navigationItems)
|
|
||||||
->middleware([
|
->middleware([
|
||||||
EncryptCookies::class,
|
EncryptCookies::class,
|
||||||
AddQueuedCookiesToResponse::class,
|
AddQueuedCookiesToResponse::class,
|
||||||
|
|
@ -91,144 +65,5 @@ class AdminPanelProvider extends PanelProvider
|
||||||
PanelsRenderHook::BODY_END,
|
PanelsRenderHook::BODY_END,
|
||||||
fn (): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'),
|
fn (): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'),
|
||||||
);
|
);
|
||||||
|
|
||||||
FilamentView::registerRenderHook(
|
|
||||||
PanelsRenderHook::BODY_END,
|
|
||||||
function (): string {
|
|
||||||
return '
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
if (window.Echo) {
|
|
||||||
console.log("Setting up Filament Reverb notifications...");
|
|
||||||
console.log("Available globals:", {
|
|
||||||
FilamentNotification: typeof FilamentNotification,
|
|
||||||
Livewire: typeof window.Livewire,
|
|
||||||
filament: typeof window.filament
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for preview site built events
|
|
||||||
window.Echo.channel("filament-notifications")
|
|
||||||
.listen("preview-site.built", function(event) {
|
|
||||||
console.log("🎉 Received preview site built event:", event);
|
|
||||||
|
|
||||||
// Use Filament v4 notification system
|
|
||||||
if (typeof FilamentNotification !== "undefined") {
|
|
||||||
new FilamentNotification()
|
|
||||||
.title(event.message)
|
|
||||||
.success()
|
|
||||||
.duration(5000)
|
|
||||||
.send();
|
|
||||||
console.log("✅ Sent via FilamentNotification v4");
|
|
||||||
} else {
|
|
||||||
console.warn("FilamentNotification not available");
|
|
||||||
// Fallback notification
|
|
||||||
const notification = document.createElement("div");
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed !important;
|
|
||||||
top: 20px !important;
|
|
||||||
right: 20px !important;
|
|
||||||
z-index: 999999 !important;
|
|
||||||
background: #10b981 !important;
|
|
||||||
color: white !important;
|
|
||||||
padding: 16px 20px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
|
|
||||||
font-family: system-ui, -apple-system, sans-serif !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
max-width: 350px !important;
|
|
||||||
display: block !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
`;
|
|
||||||
|
|
||||||
notification.innerHTML = `
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
||||||
<div style="margin-right: 15px;">
|
|
||||||
✓ ${event.message}
|
|
||||||
</div>
|
|
||||||
<button onclick="this.parentElement.parentElement.remove()"
|
|
||||||
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
console.log("✅ Created fallback notification");
|
|
||||||
|
|
||||||
// Auto-remove after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.remove();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.listen(".preview-site.built", function(event) {
|
|
||||||
console.log("🔄 Also listening with dot prefix (the working one):", event);
|
|
||||||
console.log("message:", event.message);
|
|
||||||
|
|
||||||
// Use Filament v4 notification system
|
|
||||||
if (typeof FilamentNotification !== "undefined") {
|
|
||||||
new FilamentNotification()
|
|
||||||
.title(event.message)
|
|
||||||
.success()
|
|
||||||
.duration(5000)
|
|
||||||
.send();
|
|
||||||
console.log("✅ Sent via FilamentNotification v4 (from dot prefix)");
|
|
||||||
} else {
|
|
||||||
console.warn("FilamentNotification not available, creating fallback");
|
|
||||||
// Fallback notification
|
|
||||||
const notification = document.createElement("div");
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed !important;
|
|
||||||
top: 20px !important;
|
|
||||||
right: 20px !important;
|
|
||||||
z-index: 999999 !important;
|
|
||||||
background: #3b82f6 !important;
|
|
||||||
color: white !important;
|
|
||||||
padding: 16px 20px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
|
|
||||||
font-family: system-ui, -apple-system, sans-serif !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
max-width: 350px !important;
|
|
||||||
display: block !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
`;
|
|
||||||
|
|
||||||
notification.innerHTML = `
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
||||||
<div style="margin-right: 15px;">
|
|
||||||
🔄 ${event.message}
|
|
||||||
</div>
|
|
||||||
<button onclick="this.parentElement.parentElement.remove()"
|
|
||||||
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
console.log("✅ Created dot prefix fallback notification");
|
|
||||||
|
|
||||||
// Auto-remove after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.remove();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ Event listeners set up for filament-notifications channel");
|
|
||||||
} else {
|
|
||||||
console.error("❌ Echo is not available for Filament notifications");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>';
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
use Symfony\Component\Console\Exception\RuntimeException;
|
|
||||||
|
|
||||||
class ProcessUpdateService
|
|
||||||
{
|
|
||||||
public function processUpdate(int $entryId, string $action, string $type = 'entry_update_'): void
|
|
||||||
{
|
|
||||||
$subject = env('NATS_SUBJECT', 'entry_update');
|
|
||||||
$appUrl = env('APP_URL', 'http://localhost');
|
|
||||||
|
|
||||||
$incoming = [
|
|
||||||
'id' => $entryId,
|
|
||||||
'action' => $action,
|
|
||||||
'subject' => $subject,
|
|
||||||
'app_url' => $appUrl,
|
|
||||||
'type' => $type,
|
|
||||||
];
|
|
||||||
|
|
||||||
$jsonData = json_encode($incoming, JSON_THROW_ON_ERROR);
|
|
||||||
$tempFile = tempnam(sys_get_temp_dir(), $type);
|
|
||||||
file_put_contents($tempFile, $jsonData);
|
|
||||||
|
|
||||||
$scriptPath = env('HANDLE_ENTRY_UPDATES_SCRIPT', base_path('cmd/handle_cms_updates.sh'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Log what we're about to execute
|
|
||||||
Log::info("Executing script: {$scriptPath} with action: {$action}", [
|
|
||||||
'entry_id' => $entryId,
|
|
||||||
'temp_file' => $tempFile,
|
|
||||||
'script_exists' => file_exists($scriptPath),
|
|
||||||
'script_executable' => is_executable($scriptPath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = Process::run([
|
|
||||||
'bash',
|
|
||||||
$scriptPath,
|
|
||||||
$action,
|
|
||||||
$tempFile,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($result->failed()) {
|
|
||||||
$errorDetails = [
|
|
||||||
'exit_code' => $result->exitCode(),
|
|
||||||
'stdout' => $result->output(),
|
|
||||||
'stderr' => $result->errorOutput(),
|
|
||||||
'command' => ['bash', $scriptPath, $action, $tempFile],
|
|
||||||
'script_path' => $scriptPath,
|
|
||||||
'script_exists' => file_exists($scriptPath),
|
|
||||||
'temp_file_exists' => file_exists($tempFile),
|
|
||||||
'temp_file_contents' => file_exists($tempFile) ? file_get_contents($tempFile) : 'N/A',
|
|
||||||
];
|
|
||||||
|
|
||||||
Log::error('Script execution failed', $errorDetails);
|
|
||||||
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Script execution failed with exit code {$result->exitCode()}. " .
|
|
||||||
'STDOUT: ' . ($result->output() ?: 'empty') . ' ' .
|
|
||||||
'STDERR: ' . ($result->errorOutput() ?: 'empty')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('Script executed successfully', [
|
|
||||||
'stdout' => $result->output(),
|
|
||||||
'entry_id' => $entryId,
|
|
||||||
'action' => $action,
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
// Clean up temp file
|
|
||||||
if (file_exists($tempFile)) {
|
|
||||||
unlink($tempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,9 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
api: __DIR__.'/../routes/api.php',
|
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
channels: __DIR__.'/../routes/channels.php',
|
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
#SQLITE_DB_PATH="../share-lt/database/database.sqlite"
|
|
||||||
SQLITE_DB_PATH="../share-lt/database/database.sqlite.backup5.recovery"
|
|
||||||
BACKUP_DIR="database/backups"
|
|
||||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
|
||||||
BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.sql"
|
|
||||||
TABLES="taggables media tags categories text_widgets entries users"
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
|
|
||||||
for TABLE in $TABLES; do
|
|
||||||
echo "Backing up table: $TABLE"
|
|
||||||
sqlite3 "$SQLITE_DB_PATH" ".dump $TABLE" > "$BACKUP_DIR/${TABLE}_backup_$TIMESTAMP.sql"
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
|
||||||
CONTAINER_LABEL="0.0.7"
|
|
||||||
CACHE="--no-cache"
|
|
||||||
CACHE=""
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
$CACHE \
|
|
||||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} \
|
|
||||||
-f Dockerfile .
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
|
||||||
CONTAINER_LABEL="v0.0.8"
|
|
||||||
CACHE="--no-cache"
|
|
||||||
# CACHE=""
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
$CACHE \
|
|
||||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} .
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# tokens need to be created with tinker or similar method
|
|
||||||
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/categories'
|
|
||||||
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# tokens need to be created with tinker or similar method
|
|
||||||
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/categories'
|
|
||||||
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# tokens need to be created with tinker or similar method
|
|
||||||
|
|
||||||
|
|
||||||
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# this should fail, no token provided
|
|
||||||
# users need to be authenticated and have been
|
|
||||||
# granted access to view entries by being given
|
|
||||||
# a token
|
|
||||||
|
|
||||||
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# tokens need to be created with tinker or similar method
|
|
||||||
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
|
||||||
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# this should fail, no token provided
|
|
||||||
# users need to be authenticated and have been
|
|
||||||
# granted access to view text widgets by being given
|
|
||||||
# a token
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
|
||||||
|
|
||||||
curl -s -X GET \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
$URL
|
|
||||||
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# only the admin user can create entries so this should
|
|
||||||
# fail unless .env has ADMIN_EMAIL set to the user that
|
|
||||||
# the token belongs to
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/entries'
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"title": "Yet Another New Entry Title",
|
|
||||||
"content": "This is the content yet again of the new entry."
|
|
||||||
}' \
|
|
||||||
$URL
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# this should fail as no token is provided
|
|
||||||
# user is not authenticated
|
|
||||||
# no token has been granted
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/entries'
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"title": "Yet Another New Entry Title",
|
|
||||||
"content": "This is the content yet again of the new entry."
|
|
||||||
}' \
|
|
||||||
$URL
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# TOKEN="your_api_token_here"
|
|
||||||
# ensure to have set TOKEN to a valid value before running
|
|
||||||
# ideally add this to an .envrc file and source it
|
|
||||||
# only the admin user can create entries so this should
|
|
||||||
# fail unless .env has ADMIN_EMAIL set to the user that
|
|
||||||
# the token belongs to
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"title": "Yet Another New text widget Title",
|
|
||||||
"content": "This is the content yet again of the new text widget."
|
|
||||||
}' \
|
|
||||||
$URL
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# this should fail as no token is provided
|
|
||||||
# user is not authenticated
|
|
||||||
# no token has been granted
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/text-widgets'
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"title": "Yet Another New Entry Title",
|
|
||||||
"content": "This is the content yet again of the new entry."
|
|
||||||
}' \
|
|
||||||
$URL
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# URL='http://127.0.0.1:8000'
|
|
||||||
|
|
||||||
curl -X POST $URL/api/notifications/preview-site-built \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message": "Published to LIVE 💯🚀🎯 - site notification!"}'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Ensure APP_KEY is set and persisted
|
|
||||||
PERSISTED_KEY="/var/www/storage/.app_key"
|
|
||||||
|
|
||||||
if [ -z "$APP_KEY" ]; then
|
|
||||||
if [ -f "$PERSISTED_KEY" ]; then
|
|
||||||
echo "Using persisted APP_KEY from: $PERSISTED_KEY"
|
|
||||||
export APP_KEY=$(cat "$PERSISTED_KEY")
|
|
||||||
else
|
|
||||||
# Generate key, strip "base64:", and save
|
|
||||||
NEW_KEY=$(php artisan key:generate --show --no-interaction)
|
|
||||||
echo "Generated new APP_KEY to: $PERSISTED_KEY"
|
|
||||||
echo "$NEW_KEY" > "$PERSISTED_KEY"
|
|
||||||
export APP_KEY="$NEW_KEY"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check to see if /var/www/database/database.sqlite exists
|
|
||||||
# if not, run migrations
|
|
||||||
if [ ! -f /var/www/database/database.sqlite ]; then
|
|
||||||
php artisan migrate --force
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check to see if /var/www/public/storage exists
|
|
||||||
# if not, run storage:link
|
|
||||||
if [ ! -d /var/www/public/storage ]; then
|
|
||||||
php artisan storage:link
|
|
||||||
fi
|
|
||||||
|
|
||||||
php artisan config:clear
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Start supervisord directly
|
|
||||||
exec "$@"
|
|
||||||
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
LOG_FILE="/tmp/logfile.log"
|
|
||||||
|
|
||||||
# Redirect all output to both stdout and the log file
|
|
||||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
||||||
|
|
||||||
# Read the first two command line arguments
|
|
||||||
ACTION=$1
|
|
||||||
FILENAME=$2
|
|
||||||
|
|
||||||
# Check if the file exists and echo its contents
|
|
||||||
if [[ -f "$FILENAME" ]]; then
|
|
||||||
echo "Contents of the file $FILENAME:"
|
|
||||||
cat "$FILENAME" | jq
|
|
||||||
else
|
|
||||||
echo "Error: File $FILENAME does not exist."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
# Read and print command line arguments
|
|
||||||
echo "=============================="
|
|
||||||
echo "ACTION: $ACTION"
|
|
||||||
echo "FILENAME: $FILENAME"
|
|
||||||
echo "=============================="
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Publish message and check return code
|
|
||||||
if nats pub $NATS_SUBJECT "$(cat "$FILENAME")" --server $NATS_URL --user $NATS_USERNAME --password $NATS_PASSWORD; then
|
|
||||||
echo "Success: Message published to NATS successfully."
|
|
||||||
else
|
|
||||||
echo "Error: Failed to publish message to NATS."
|
|
||||||
fi
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Define the backup directory and the SQLite database file
|
|
||||||
BACKUP_DIR="database/backups"
|
|
||||||
DB_FILE="database/database.sqlite"
|
|
||||||
# DATE="20260124_115644"
|
|
||||||
DATE="20260124_123340"
|
|
||||||
|
|
||||||
# Ensure the DATE variable is set
|
|
||||||
if [ -z "$DATE" ]; then
|
|
||||||
echo "DATE variable is not set. Please set the DATE variable to match the backup file date."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the backup directory exists
|
|
||||||
if [ ! -d "$BACKUP_DIR" ]; then
|
|
||||||
echo "Backup directory does not exist: $BACKUP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the SQLite database file exists
|
|
||||||
if [ ! -f "$DB_FILE" ]; then
|
|
||||||
echo "SQLite database file does not exist: $DB_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if there are any matching backup files
|
|
||||||
if ! ls "$BACKUP_DIR"/*"$DATE".sql 1> /dev/null 2>&1; then
|
|
||||||
echo "No backup files found for the date: $DATE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting restore process from backups dated: $DATE"
|
|
||||||
echo "Using database file: $DB_FILE"
|
|
||||||
|
|
||||||
# Loop through each file in the backup directory
|
|
||||||
for file in "$BACKUP_DIR"/*"$DATE".sql; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
echo "Restoring data from $file into $DB_FILE..."
|
|
||||||
|
|
||||||
# Import the SQL file into the SQLite database and log output
|
|
||||||
sqlite3 "$DB_FILE" < "$file" 2>> restore_errors.log
|
|
||||||
|
|
||||||
# Check for errors in the restore process
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Successfully restored $file"
|
|
||||||
else
|
|
||||||
echo "Failed to restore $file. Check restore_errors.log for details."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Debugging: Print the last 10 lines of the database to verify data
|
|
||||||
# echo "Last 10 rows of the database after restoring $file:"
|
|
||||||
# sqlite3 "$DB_FILE" "SELECT * FROM sqlite_master WHERE type='table';" 2>> restore_errors.log
|
|
||||||
# sqlite3 "$DB_FILE" "SELECT * FROM entries ORDER BY rowid DESC LIMIT 10;" 2>> restore_errors.log
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Restore process completed."
|
|
||||||
|
|
@ -8,22 +8,15 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"filament/filament": "^4.0",
|
"filament/filament": "^4.0",
|
||||||
"filament/spatie-laravel-media-library-plugin": "^4.4",
|
|
||||||
"filament/spatie-laravel-tags-plugin": "^4.0",
|
|
||||||
"laravel/fortify": "^1.30",
|
"laravel/fortify": "^1.30",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/reverb": "^1.0",
|
|
||||||
"laravel/sanctum": "^4.0",
|
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
|
||||||
"livewire/flux": "^2.9.0",
|
"livewire/flux": "^2.9.0",
|
||||||
"spatie/laravel-medialibrary": "^11.17",
|
"spatie/laravel-medialibrary": "^11.17"
|
||||||
"spatie/laravel-tags": "^4.10"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/boost": "^1.8",
|
"laravel/boost": "^1.8",
|
||||||
"laravel/dusk": "^8.3",
|
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.24",
|
||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
|
|
|
||||||
1859
composer.lock
generated
1859
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -123,6 +123,4 @@ return [
|
||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'admin_email' => env('ADMIN_EMAIL', ''),
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Broadcaster
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default broadcaster that will be used by the
|
|
||||||
| framework when an event needs to be broadcast. You may set this to
|
|
||||||
| any of the connections defined in the "connections" array below.
|
|
||||||
|
|
|
||||||
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('BROADCAST_CONNECTION', 'null'),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Broadcast Connections
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define all of the broadcast connections that will be used
|
|
||||||
| to broadcast events to other systems or over WebSockets. Samples of
|
|
||||||
| each available type of connection are provided inside this array.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connections' => [
|
|
||||||
|
|
||||||
'reverb' => [
|
|
||||||
'driver' => 'reverb',
|
|
||||||
'key' => env('REVERB_APP_KEY'),
|
|
||||||
'secret' => env('REVERB_APP_SECRET'),
|
|
||||||
'app_id' => env('REVERB_APP_ID'),
|
|
||||||
'options' => [
|
|
||||||
'host' => env('REVERB_API_HOST', '127.0.0.1'),
|
|
||||||
'port' => env('REVERB_API_PORT', 9001),
|
|
||||||
'scheme' => env('REVERB_API_SCHEME', 'http'),
|
|
||||||
'useTLS' => env('REVERB_API_SCHEME', 'http') !== 'https',
|
|
||||||
],
|
|
||||||
'client_options' => [
|
|
||||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'pusher' => [
|
|
||||||
'driver' => 'pusher',
|
|
||||||
'key' => env('PUSHER_APP_KEY'),
|
|
||||||
'secret' => env('PUSHER_APP_SECRET'),
|
|
||||||
'app_id' => env('PUSHER_APP_ID'),
|
|
||||||
'options' => [
|
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
|
||||||
'host' => env('PUSHER_HOST') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
|
|
||||||
'port' => env('PUSHER_PORT', 443),
|
|
||||||
'scheme' => env('PUSHER_SCHEME', 'https'),
|
|
||||||
'encrypted' => true,
|
|
||||||
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
|
||||||
],
|
|
||||||
'client_options' => [
|
|
||||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'ably' => [
|
|
||||||
'driver' => 'ably',
|
|
||||||
'key' => env('ABLY_KEY'),
|
|
||||||
],
|
|
||||||
|
|
||||||
'log' => [
|
|
||||||
'driver' => 'log',
|
|
||||||
],
|
|
||||||
|
|
||||||
'null' => [
|
|
||||||
'driver' => 'null',
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
@ -16,18 +16,18 @@ return [
|
||||||
|
|
||||||
'broadcasting' => [
|
'broadcasting' => [
|
||||||
|
|
||||||
'echo' => [
|
// 'echo' => [
|
||||||
'broadcaster' => 'reverb',
|
// 'broadcaster' => 'pusher',
|
||||||
'key' => env('VITE_REVERB_APP_KEY'),
|
// 'key' => env('VITE_PUSHER_APP_KEY'),
|
||||||
'cluster' => env('VITE_REVERB_APP_CLUSTER'),
|
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
|
||||||
'wsHost' => env('VITE_REVERB_HOST'),
|
// 'wsHost' => env('VITE_PUSHER_HOST'),
|
||||||
'wsPort' => env('VITE_REVERB_PORT'),
|
// 'wsPort' => env('VITE_PUSHER_PORT'),
|
||||||
'wssPort' => env('VITE_REVERB_PORT'),
|
// 'wssPort' => env('VITE_PUSHER_PORT'),
|
||||||
'authEndpoint' => '/broadcasting/auth',
|
// 'authEndpoint' => '/broadcasting/auth',
|
||||||
'disableStats' => true,
|
// 'disableStats' => true,
|
||||||
'encrypted' => env('VITE_REVERB_SCHEME', 'https') === 'https',
|
// 'encrypted' => true,
|
||||||
'forceTLS' => env('VITE_REVERB_SCHEME', 'https') === 'https',
|
// 'forceTLS' => true,
|
||||||
],
|
// ],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ return [
|
||||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
'region' => env('AWS_DEFAULT_REGION'),
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
'bucket' => env('AWS_BUCKET'),
|
'bucket' => env('AWS_BUCKET'),
|
||||||
'root' => env('AWS_DIRECTORY', ''),
|
|
||||||
'url' => env('AWS_URL'),
|
'url' => env('AWS_URL'),
|
||||||
'endpoint' => env('AWS_ENDPOINT'),
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Reverb Server
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default server used by Reverb to handle
|
|
||||||
| incoming messages as well as broadcasting message to all your
|
|
||||||
| connected clients. At this time only "reverb" is supported.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('REVERB_SERVER', 'reverb'),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Reverb Servers
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define details for each of the supported Reverb servers.
|
|
||||||
| Each server has its own configuration options that are defined in
|
|
||||||
| the array below. You should ensure all the options are present.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'servers' => [
|
|
||||||
|
|
||||||
'reverb' => [
|
|
||||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
|
|
||||||
'port' => env('REVERB_SERVER_PORT', 8080),
|
|
||||||
'path' => env('REVERB_SERVER_PATH', ''),
|
|
||||||
'hostname' => env('REVERB_HOST'),
|
|
||||||
'options' => [
|
|
||||||
'tls' => [],
|
|
||||||
],
|
|
||||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
|
|
||||||
'scaling' => [
|
|
||||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
|
||||||
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
|
|
||||||
'server' => [
|
|
||||||
'url' => env('REDIS_URL'),
|
|
||||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
|
||||||
'port' => env('REDIS_PORT', '6379'),
|
|
||||||
'username' => env('REDIS_USERNAME'),
|
|
||||||
'password' => env('REDIS_PASSWORD'),
|
|
||||||
'database' => env('REDIS_DB', '0'),
|
|
||||||
'timeout' => env('REDIS_TIMEOUT', 60),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
|
||||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Reverb Applications
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define how Reverb applications are managed. If you choose
|
|
||||||
| to use the "config" provider, you may define an array of apps which
|
|
||||||
| your server will support, including their connection credentials.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'apps' => [
|
|
||||||
|
|
||||||
'provider' => 'config',
|
|
||||||
|
|
||||||
'apps' => [
|
|
||||||
[
|
|
||||||
'key' => env('REVERB_APP_KEY'),
|
|
||||||
'secret' => env('REVERB_APP_SECRET'),
|
|
||||||
'app_id' => env('REVERB_APP_ID'),
|
|
||||||
'options' => [
|
|
||||||
'host' => env('REVERB_HOST'),
|
|
||||||
'port' => env('REVERB_PORT', 443),
|
|
||||||
'scheme' => env('REVERB_SCHEME', 'https'),
|
|
||||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
|
||||||
],
|
|
||||||
'allowed_origins' => ['*'],
|
|
||||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
|
||||||
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
|
||||||
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
|
||||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Laravel\Sanctum\Sanctum;
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Stateful Domains
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Requests from the following domains / hosts will receive stateful API
|
|
||||||
| authentication cookies. Typically, these should include your local
|
|
||||||
| and production domains which access your API via a frontend SPA.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
|
||||||
'%s%s',
|
|
||||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
|
||||||
Sanctum::currentApplicationUrlWithPort(),
|
|
||||||
// Sanctum::currentRequestHost(),
|
|
||||||
))),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Sanctum Guards
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This array contains the authentication guards that will be checked when
|
|
||||||
| Sanctum is trying to authenticate a request. If none of these guards
|
|
||||||
| are able to authenticate the request, Sanctum will use the bearer
|
|
||||||
| token that's present on an incoming request for authentication.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'guard' => ['web'],
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Expiration Minutes
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value controls the number of minutes until an issued token will be
|
|
||||||
| considered expired. This will override any values set in the token's
|
|
||||||
| "expires_at" attribute, but first-party sessions are not affected.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'expiration' => null,
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Token Prefix
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
|
||||||
| security scanning initiatives maintained by open source platforms
|
|
||||||
| that notify developers if they commit tokens into repositories.
|
|
||||||
|
|
|
||||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Sanctum Middleware
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When authenticating your first-party SPA with Sanctum you may need to
|
|
||||||
| customize some of the middleware Sanctum uses while processing the
|
|
||||||
| request. You may change the middleware listed below as required.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'middleware' => [
|
|
||||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
|
||||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
|
||||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
@ -26,7 +26,6 @@ return [
|
||||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
'directory' => env('AWS_DIRECTORY'),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'slack' => [
|
'slack' => [
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue