diff --git a/.env.local b/.env.local index 548117c..cd4579d 100644 --- a/.env.local +++ b/.env.local @@ -1,6 +1,26 @@ -# Local Development MinIO Configuration -# Copy to .env.local and update with your MinIO credentials +# Local Development Configuration +# Copy to .env.local and fill in your values. -# MinIO credentials (get from your production server) -MINIO_KEY=secret_key +# ── MinIO credentials ───────────────────────────────────────────────────────── +MINIO_KEY=your_minio_root_user MINIO_SECRET=your_minio_root_password + +# ── SSH tunnel (remote DB) ──────────────────────────────────────────────────── +# Only needed when starting with --profile ssh-db. +# NOTE: no inline comments allowed after values — Docker reads them literally. + +SSH_HOST=136.114.183.15 +SSH_PORT=22 +SSH_USER=webmaster +# Must be an absolute path — Docker Compose does NOT expand ~ +SSH_KEY_PATH=/Users/webmaster/.ssh/id_ed25519_crew_webmaster + +SSH_DB_REMOTE_HOST=127.0.0.1 +SSH_DB_REMOTE_PORT=3306 + +# Tell the app to route DB traffic through the tunnel container: +DB_HOST=db-tunnel +DB_PORT=3306 +DB_DATABASE=custom_designs +DB_USERNAME=root +DB_PASSWORD=VeryStrongRootPass2025! diff --git a/.env.local.example b/.env.local.example index 40e9437..f04858a 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,6 +1,26 @@ -# Local Development MinIO Configuration -# Copy to .env.local and update with your MinIO credentials +# Local Development Configuration +# Copy to .env.local and fill in your values. -# MinIO credentials (get from your production server) +# ── MinIO credentials ───────────────────────────────────────────────────────── MINIO_KEY=your_minio_root_user MINIO_SECRET=your_minio_root_password + +# ── SSH tunnel (remote DB) ──────────────────────────────────────────────────── +# Only needed when starting with --profile ssh-db. +# IMPORTANT: no inline comments after values — Docker Compose reads them literally. +# IMPORTANT: SSH_KEY_PATH must be an absolute path — ~ is NOT expanded by Docker Compose. +# +SSH_HOST=your.server.ip.or.hostname +SSH_PORT=22 +SSH_USER=root +SSH_KEY_PATH=/absolute/path/to/your/private/key +# +SSH_DB_REMOTE_HOST=127.0.0.1 # DB host as seen from the SSH server +SSH_DB_REMOTE_PORT=3306 # DB port as seen from the SSH server +# +# Tell the app to route DB traffic through the tunnel container: +DB_HOST=db-tunnel +DB_PORT=3306 +DB_DATABASE=your_remote_db_name +DB_USERNAME=your_remote_db_user +DB_PASSWORD=your_remote_db_password diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..11f2a9b --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +COMPOSE_LOCAL = docker compose -f docker-compose.local.yml + +# ── Local stack (local MariaDB) ─────────────────────────────────────────────── +up: + $(COMPOSE_LOCAL) up --build + +down: + $(COMPOSE_LOCAL) down + +# ── Local stack with SSH tunnel to remote DB ────────────────────────────────── +up-ssh: + $(COMPOSE_LOCAL) --env-file .env.local --profile ssh-db up --build + +down-ssh: + $(COMPOSE_LOCAL) --env-file .env.local --profile ssh-db down + +.PHONY: up down up-ssh down-ssh diff --git a/SSH_KEYS_SETUP.md b/SSH_KEYS_SETUP.md index 5fefae2..ff066cb 100644 --- a/SSH_KEYS_SETUP.md +++ b/SSH_KEYS_SETUP.md @@ -107,6 +107,63 @@ Then update `filesystems.php`: 'privateKey' => '/var/keys/root.pem', ``` +--- + +## Local Development — Remote DB via SSH Tunnel + +Use the `ssh-db` profile to connect the local app to a **remote database** through an SSH tunnel, authenticated with a private key. + +### 1. Configure `.env.local` + +```dotenv +# SSH jump host +SSH_HOST=your.server.ip.or.hostname +SSH_PORT=22 +SSH_USER=root +SSH_KEY_PATH=~/.ssh/id_rsa # path to your private key on the Mac host + +# DB endpoint as seen from the SSH server +SSH_DB_REMOTE_HOST=127.0.0.1 +SSH_DB_REMOTE_PORT=3306 + +# Tell the app to route through the tunnel container +DB_HOST=db-tunnel +DB_PORT=3306 +DB_DATABASE=your_remote_db_name +DB_USERNAME=your_remote_db_user +DB_PASSWORD=your_remote_db_password +``` + +### 2. Start the stack with the tunnel profile + +```bash +docker compose -f docker-compose.local.yml --profile ssh-db up --build +``` + +This starts a `db-tunnel` sidecar container (Alpine + openssh-client) that creates: + +``` +Mac host → [SSH tunnel] → SSH_HOST → DB (SSH_DB_REMOTE_HOST:SSH_DB_REMOTE_PORT) +``` + +The app container connects to `db-tunnel:3306`, which forwards all traffic through the encrypted tunnel. + +### 3. Key requirements + +| Requirement | Detail | +|---|---| +| Key format | OpenSSH (`id_rsa`, `id_ed25519`) — **not** `.ppk` | +| Key permissions | `chmod 600 ~/.ssh/id_rsa` | +| SSH server | Authorised key must be in `~/.ssh/authorized_keys` on `SSH_HOST` | + +> **Tip:** If your key is in PuTTY (`.ppk`) format, convert it first: +> ```bash +> puttygen root.ppk -O private-openssh -o ~/.ssh/id_rsa +> chmod 600 ~/.ssh/id_rsa +> ``` + +--- + ## Security Best Practices ✅ **DO:** diff --git a/docker-compose.local.yml b/docker-compose.local.yml index d582ece..8a7359b 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,3 +1,19 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Local development stack +# +# Default (local MariaDB): +# docker compose -f docker-compose.local.yml up --build +# +# Remote DB via SSH private-key tunnel: +# 1. Set SSH_HOST, SSH_USER, SSH_KEY_PATH, SSH_DB_REMOTE_HOST, +# SSH_DB_REMOTE_PORT (and DB_* creds) in .env.local +# 2. docker compose -f docker-compose.local.yml --profile ssh-db up --build +# The app will talk to db-tunnel (port 3306) instead of the local db. +# +# App: http://localhost:8082 +# phpMyAdmin: http://localhost:8083 +# ───────────────────────────────────────────────────────────────────────────── + services: db: image: mariadb:10.6 @@ -29,11 +45,7 @@ services: - APP_DEBUG=true - APP_URL=http://localhost:8082 - DB_CONNECTION=mysql - - DB_HOST=db - - DB_PORT=3306 - - DB_DATABASE=crewsportswear - - DB_USERNAME=crewsportswear - - DB_PASSWORD=secret + - DB_PORT=${DB_PORT:-3306} - PROD_PRIVATE=http://localhost:8082 - IMAGES_URL=http://localhost:8082 - UPLOAD_URL=http://localhost:8082/uploads/ @@ -56,15 +68,65 @@ services: - MINIO_REGION=us-east-1 - MINIO_USE_PATH_STYLE=false - MINIO_URL=https://minio.crewsportswear.app + # DB_HOST defaults to local container; set to db-tunnel in .env.local for SSH mode + - DB_HOST=${DB_HOST:-db} + - DB_DATABASE=${DB_DATABASE:-crewsportswear} + - DB_USERNAME=${DB_USERNAME:-crewsportswear} + - DB_PASSWORD=${DB_PASSWORD:-secret} + env_file: + - path: .env.local + required: false volumes: - ./:/var/www/html - ./storage:/var/www/html/storage - ./public/uploads:/var/www/html/public/uploads + # Keep the vendor/ directory from the image — prevents the bind-mount + # from wiping out the composer install done during docker build. + - vendor_local:/var/www/html/vendor depends_on: - db networks: - crewsportswear-local + # ── SSH tunnel to a remote database ──────────────────────────────────────── + # Activated only with: --profile ssh-db + # Requires SSH_HOST, SSH_USER, SSH_KEY_PATH (and optionally SSH_PORT, + # SSH_DB_REMOTE_HOST, SSH_DB_REMOTE_PORT) set in .env.local. + db-tunnel: + profiles: + - ssh-db + image: alpine:3.19 + container_name: crewsportswear_db_tunnel + restart: unless-stopped + environment: + - SSH_HOST=${SSH_HOST} + - SSH_PORT=${SSH_PORT:-22} + - SSH_USER=${SSH_USER:-root} + - SSH_DB_REMOTE_HOST=${SSH_DB_REMOTE_HOST:-127.0.0.1} + - SSH_DB_REMOTE_PORT=${SSH_DB_REMOTE_PORT:-3306} + volumes: + # Mount your private key read-only; path configured in .env.local + - ${SSH_KEY_PATH:-~/.ssh/id_rsa}:/ssh/id_rsa:ro + command: + - sh + - -c + - | + apk add --no-cache openssh-client + cp /ssh/id_rsa /tmp/id_rsa + chmod 600 /tmp/id_rsa + echo "Starting SSH tunnel to $$SSH_HOST..." + exec ssh -N \ + -o StrictHostKeyChecking=no \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + -i /tmp/id_rsa \ + -L "0.0.0.0:3306:$$SSH_DB_REMOTE_HOST:$$SSH_DB_REMOTE_PORT" \ + -p "$$SSH_PORT" \ + "$$SSH_USER@$$SSH_HOST" + networks: + - crewsportswear-local + phpmyadmin: image: arm64v8/phpmyadmin platform: linux/arm64 @@ -87,3 +149,4 @@ networks: volumes: db_data: + vendor_local: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7d70888..9b948a2 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -13,5 +13,11 @@ mkdir -p bootstrap/cache chown -R www-data:www-data storage bootstrap/cache chmod -R 775 storage bootstrap/cache +# Install/update Composer dependencies if vendor is missing or composer.json changed +if [ ! -f vendor/autoload.php ]; then + echo "vendor/autoload.php not found — running composer install..." + composer install --no-interaction --prefer-dist +fi + # Execute the main command exec "$@" diff --git a/resources/views/teamstore-sublayouts/index.blade.php b/resources/views/teamstore-sublayouts/index.blade.php index 6599ea0..5fc4196 100644 --- a/resources/views/teamstore-sublayouts/index.blade.php +++ b/resources/views/teamstore-sublayouts/index.blade.php @@ -127,6 +127,126 @@ overflow: hidden; text-overflow: ellipsis; } + + /* ── Category nav ──────────────────────────────────────────────────────── */ + .cat-nav { + background: #f8f8f8; + border-bottom: 2px solid #e0e0e0; + padding: 0; + margin-bottom: 24px; + } + .cat-nav ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + } + .cat-nav > ul > li { + position: relative; + } + .cat-nav > ul > li > a { + display: block; + padding: 14px 20px; + font-weight: 700; + font-size: 13px; + text-transform: uppercase; + color: #222; + text-decoration: none; + letter-spacing: .5px; + border-bottom: 3px solid transparent; + transition: border-color .2s, color .2s; + } + .cat-nav > ul > li > a:hover, + .cat-nav > ul > li > a.active { + color: #4B8E4B; + border-bottom-color: #4B8E4B; + } + /* sub-category dropdown */ + .cat-nav .sub-menu { + display: none; + position: absolute; + top: 100%; + left: 0; + min-width: 200px; + background: #fff; + border: 1px solid #ddd; + border-top: 3px solid #4B8E4B; + box-shadow: 0 4px 12px rgba(0,0,0,.12); + z-index: 999; + list-style: none; + margin: 0; + padding: 6px 0; + } + .cat-nav > ul > li:hover .sub-menu { + display: block; + } + .cat-nav .sub-menu li a { + display: block; + padding: 9px 18px; + font-size: 13px; + color: #333; + text-decoration: none; + white-space: nowrap; + } + .cat-nav .sub-menu li a:hover, + .cat-nav .sub-menu li a.active { + background: #f0f9f0; + color: #4B8E4B; + } + .cat-count { + display: inline-block; + background: #ddd; + border-radius: 10px; + font-size: 11px; + padding: 1px 7px; + margin-left: 4px; + vertical-align: middle; + } + + /* ── League / conference pill filter (sub-sub-category) ─────────────────── */ + .league-filter { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 10px 0 14px; + margin-bottom: 18px; + border-bottom: 1px solid #e8e8e8; + align-items: center; + } + .league-filter .lf-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + color: #888; + letter-spacing: .5px; + margin-right: 4px; + white-space: nowrap; + } + .league-filter a { + display: inline-block; + padding: 5px 14px; + border-radius: 20px; + background: #f0f0f0; + color: #333; + font-size: 12px; + font-weight: 600; + text-decoration: none; + white-space: nowrap; + border: 1px solid #ddd; + transition: background .2s, color .2s, border-color .2s; + } + .league-filter a:hover { + background: #e0f2e0; + color: #4B8E4B; + border-color: #4B8E4B; + } + .league-filter a.active { + background: #4B8E4B; + color: #fff; + border-color: #4B8E4B; + } + [v-cloak] { display: none; }
@@ -150,80 +270,277 @@

FEATURED PRODUCTS

-
+ + {{-- ── Vue category + product grid ─────────────────────────────────────── --}} + + +
+ + {{-- ── Category nav ── --}} + + + {{-- ── League / conference pill filter ── --}} + + + {{-- ── Announcements (kept outside loop) ── --}} @if ($announcement->IsActive) +

Shop Announcements:

{!! nl2br(e($announcement->Announcement)) !!}
-
+
+
@endif - @if($store_array[0]->Id == 174 || $store_array[0]->Id == 175 || $store_array[0]->Id == 178 || $store_array[0]->Id == 179 || $store_array[0]->Id == 177 || $store_array[0]->Id == 189 || $store_array[0]->Id == 176 || $store_array[0]->Id == 190 || $store_array[0]->Id == 191 || $store_array[0]->Id == 192 || $store_array[0]->Id == 194) -
-
-

Please read:

- 1. All orders will be batch shipped to your school for pick up.
- 2. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.
- 3. Masks and gaiters sold on Crew are not intended for medical use. Crew does not make any medical or health claims.
- 4. Refunds and exchanges are not allowed due to the hygenic nature of the product.
- @if($store_array[0]->Id == 175) - 5. $1 from every item sold will benefit the 2020-2021 Maine South Schoolwide Fundraiser. - @endif -
-
- @else -
-
+ @if($store_array[0]->Id == 174 || $store_array[0]->Id == 175 || $store_array[0]->Id == 178 || $store_array[0]->Id == 179 || $store_array[0]->Id == 177 || $store_array[0]->Id == 189 || $store_array[0]->Id == 176 || $store_array[0]->Id == 190 || $store_array[0]->Id == 191 || $store_array[0]->Id == 192 || $store_array[0]->Id == 194) +
+
+

Please read:

- 1. Items purchased are made on demand. Orders will be shipped based on dates set by your store administrator.
- 2. Store payments are processed through PayPal. A PayPal account is not required to make a purchase.
- @if($store_array[0]->Id == 222) - 3. Orders will be batch processed on a monthly basis, please allow 4-6 weeks for delivery.
- @else - 3. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.
+ 1. All orders will be batch shipped to your school for pick up.
+ 2. Orders will be batch processed on a weekly basis, please allow 2-3 weeks for delivery.
+ 3. Masks and gaiters sold on Crew are not intended for medical use. Crew does not make any medical or health claims.
+ 4. Refunds and exchanges are not allowed due to the hygenic nature of the product.
+ @if($store_array[0]->Id == 175) + 5. $1 from every item sold will benefit the 2020-2021 Maine South Schoolwide Fundraiser. @endif - 4. We are currently only shipping to US locations. For international orders, please contact orders@crewsportswear.com if you'd like to place an order.
- 5. Expect shipping delays due to COVID-19.
- 6. All sales are final. No returns or exchanges will be accepted.

- - DISCLAIMER: Masks and gaiters sold by Crew Sportswear are not intended for medical use. Crew Sportswear does not make any medical or health claims. -
-
- @endif - - - @foreach($product_array as $i => $product) - @if($product->PrivacyStatus == "public") - @foreach($thumbnails as $t => $thumb) - @if($thumb['product_id'] == $product->Id) - @define $storeFolder = $thumb['folder'] - @define $filename = $thumb['thumb'] - @endif - @endforeach - -
- - - {{ $product->ProductName }} - -

{{ $product->ProductName }}

-
-
-
-

{{ $product->ProductPrice }} {{ $store_array[0]->StoreCurrency }}

-
- - -
-
- @endif - @endforeach - - -
-
+
+
+ @endif + + {{-- ── Product grid ── --}} +
+
+

No products found in this category.

+
+
+ + + + +

@{{ p.name }}

+
+
+
+

@{{ p.price }} @{{ store.currency }}

+
+ +
+
+
+
+ + {{-- /ts-app --}} + + + + +{{-- /container --}} @endsection