From 8a38befb1cb6cb671ff7bf9cbe41c8729c82d821 Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Thu, 22 Jan 2026 12:46:28 -0800 Subject: [PATCH] fix cert and image display issues --- .env.example | 4 +- Dockerfile.dev | 21 ++++++ certs/README.md | 105 +++++++++++++++++++++++++++++ certs/localhost.crt | 40 ++++++----- certs/localhost.key | 52 +++++++------- certs/mkcert-ca.crt | 27 ++++++++ docker/nginx/dev.conf | 30 ++++++++- docs/FLYER-URL-CONFIGURATION.md | 97 ++++++++++++++++++++++++-- docs/development/DEBUGGING.md | 72 ++++++++++++++++++++ docs/getting-started/INSTALL.md | 25 +++++++ docs/getting-started/QUICKSTART.md | 11 +++ sql/update_flyer_urls.sql | 24 +++++-- src/db/seed.ts | 4 +- src/tests/utils/testHelpers.ts | 6 +- 14 files changed, 453 insertions(+), 65 deletions(-) create mode 100644 certs/README.md create mode 100644 certs/mkcert-ca.crt diff --git a/.env.example b/.env.example index 59ff921e..a3f8fc9c 100644 --- a/.env.example +++ b/.env.example @@ -36,10 +36,10 @@ NODE_ENV=development FRONTEND_URL=http://localhost:3000 # Flyer Base URL - used for seed data and flyer image URLs -# Dev container: http://127.0.0.1 +# Dev container: https://localhost (NOT 127.0.0.1 - avoids SSL mixed-origin issues) # Test: https://flyer-crawler-test.projectium.com # Production: https://flyer-crawler.projectium.com -FLYER_BASE_URL=http://127.0.0.1 +FLYER_BASE_URL=https://localhost # =================== # Authentication diff --git a/Dockerfile.dev b/Dockerfile.dev index 2c0a611d..af06176a 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -63,6 +63,27 @@ RUN wget -O /usr/local/bin/mkcert https://github.com/FiloSottile/mkcert/releases && chmod +x /usr/local/bin/mkcert # Create certificates directory and generate localhost certificates +# ============================================================================ +# IMPORTANT: Certificate includes MULTIPLE hostnames (SANs) +# ============================================================================ +# The certificate is generated for 'localhost', '127.0.0.1', AND '::1' because: +# +# 1. Users may access the site via https://localhost/ OR https://127.0.0.1/ +# 2. Database stores image URLs using one hostname (typically 127.0.0.1) +# 3. The seed script uses https://127.0.0.1 for image URLs (database constraint) +# 4. NGINX is configured to accept BOTH hostnames (see docker/nginx/dev.conf) +# +# Without all hostnames in the certificate's Subject Alternative Names (SANs), +# browsers would show ERR_CERT_AUTHORITY_INVALID when loading images or other +# resources that use a different hostname than the one in the address bar. +# +# The mkcert command below creates a certificate valid for all three: +# - localhost (IPv4 hostname) +# - 127.0.0.1 (IPv4 address) +# - ::1 (IPv6 loopback) +# +# See also: docker/nginx/dev.conf, docs/FLYER-URL-CONFIGURATION.md +# ============================================================================ RUN mkdir -p /app/certs \ && cd /app/certs \ && mkcert -install \ diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 00000000..4c1a3ab9 --- /dev/null +++ b/certs/README.md @@ -0,0 +1,105 @@ +# Development SSL Certificates + +This directory contains SSL certificates for the development container HTTPS setup. + +## Files + +| File | Purpose | Generated By | +| --------------- | ---------------------------------------------------- | -------------------------- | +| `localhost.crt` | SSL certificate for localhost and 127.0.0.1 | mkcert (in Dockerfile.dev) | +| `localhost.key` | Private key for localhost.crt | mkcert (in Dockerfile.dev) | +| `mkcert-ca.crt` | Root CA certificate for trusting mkcert certificates | mkcert | + +## Certificate Details + +The `localhost.crt` certificate includes the following Subject Alternative Names (SANs): + +- `DNS:localhost` +- `IP Address:127.0.0.1` +- `IP Address:::1` (IPv6 localhost) + +This allows the development server to be accessed via both `https://localhost/` and `https://127.0.0.1/` without SSL errors. + +## Installing the CA Certificate (Recommended) + +To avoid SSL certificate warnings in your browser, install the mkcert CA certificate on your system. + +### Windows + +1. Double-click `mkcert-ca.crt` +2. Click **"Install Certificate..."** +3. Select **"Local Machine"** > Next +4. Select **"Place all certificates in the following store"** +5. Click **Browse** > Select **"Trusted Root Certification Authorities"** > OK +6. Click **Next** > **Finish** +7. Restart your browser + +### macOS + +```bash +sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certs/mkcert-ca.crt +``` + +### Linux + +```bash +# Ubuntu/Debian +sudo cp certs/mkcert-ca.crt /usr/local/share/ca-certificates/mkcert-ca.crt +sudo update-ca-certificates + +# Fedora/RHEL +sudo cp certs/mkcert-ca.crt /etc/pki/ca-trust/source/anchors/ +sudo update-ca-trust +``` + +### Firefox (All Platforms) + +Firefox uses its own certificate store: + +1. Open Firefox Settings +2. Search for "Certificates" +3. Click **"View Certificates"** +4. Go to **"Authorities"** tab +5. Click **"Import..."** +6. Select `certs/mkcert-ca.crt` +7. Check **"Trust this CA to identify websites"** +8. Click **OK** + +## After Installation + +Once the CA certificate is installed: + +- Your browser will trust all mkcert certificates without warnings +- Access `https://localhost/` with no security warnings +- Images from `https://127.0.0.1/flyer-images/` will load without SSL errors + +## Regenerating Certificates + +If you need to regenerate the certificates (e.g., after rebuilding the container): + +```bash +# Inside the container +cd /app/certs +mkcert localhost 127.0.0.1 ::1 +mv localhost+2.pem localhost.crt +mv localhost+2-key.pem localhost.key +nginx -s reload + +# Copy the new CA to the host +podman cp flyer-crawler-dev:/app/certs/mkcert-ca.crt ./certs/mkcert-ca.crt +``` + +Then reinstall the CA certificate as described above. + +## Security Note + +**DO NOT** commit the private key (`localhost.key`) to version control in production projects. For this development-only project, the certificates are checked in for convenience since they're only used locally with self-signed certificates. + +The certificates in this directory are automatically generated by the Dockerfile.dev and should not be used in production. + +## See Also + +- [Dockerfile.dev](../Dockerfile.dev) - Certificate generation (line ~69) +- [docker/nginx/dev.conf](../docker/nginx/dev.conf) - NGINX SSL configuration +- [docs/FLYER-URL-CONFIGURATION.md](../docs/FLYER-URL-CONFIGURATION.md) - URL configuration details +- [docs/development/DEBUGGING.md](../docs/development/DEBUGGING.md) - SSL troubleshooting diff --git a/certs/localhost.crt b/certs/localhost.crt index 1317ce29..4e893ba2 100644 --- a/certs/localhost.crt +++ b/certs/localhost.crt @@ -1,19 +1,25 @@ -----BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIUHhZUK1vmww2wCepWPuVcU6d27hMwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDExODAyMzM0NFoXDTI3MDEx -ODAyMzM0NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAuUJGtSZzd+ZpLi+efjrkxJJNfVxVz2VLhknNM2WKeOYx -JTK/VaTYq5hrczy6fEUnMhDAJCgEPUFlOK3vn1gFJKNMN8m7arkLVk6PYtrx8CTw -w78Q06FLITr6hR0vlJNpN4MsmGxYwUoUpn1j5JdfZF7foxNAZRiwoopf7ZJxltDu -PIuFjmVZqdzR8c6vmqIqdawx/V6sL9fizZr+CDH3oTsTUirn2qM+1ibBtPDiBvfX -omUsr6MVOcTtvnMvAdy9NfV88qwF7MEWBGCjXkoT1bKCLD8hjn8l7GjRmPcmMFE2 -GqWEvfJiFkBK0CgSHYEUwzo0UtVNeQr0k0qkDRub6QIDAQABo1MwUTAdBgNVHQ4E -FgQU5VeD67yFLV0QNYbHaJ6u9cM6UbkwHwYDVR0jBBgwFoAU5VeD67yFLV0QNYbH -aJ6u9cM6UbkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABueA -8ujAD+yjeP5dTgqQH1G0hlriD5LmlJYnktaLarFU+y+EZlRFwjdORF/vLPwSG+y7 -CLty/xlmKKQop70QzQ5jtJcsWzUjww8w1sO3AevfZlIF3HNhJmt51ihfvtJ7DVCv -CNyMeYO0pBqRKwOuhbG3EtJgyV7MF8J25UEtO4t+GzX3jcKKU4pWP+kyLBVfeDU3 -MQuigd2LBwBQQFxZdpYpcXVKnAJJlHZIt68ycO1oSBEJO9fIF0CiAlC6ITxjtYtz -oCjd6cCLKMJiC6Zg7t1Q17vGl+FdGyQObSsiYsYO9N3CVaeDdpyGCH0Rfa0+oZzu -a5U9/l1FHlvpX980bw== +MIIEJDCCAoygAwIBAgIQfbdj1KSREvW82wxWZcDRsDANBgkqhkiG9w0BAQsFADBf +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExGjAYBgNVBAsMEXJvb3RA +OTIxZjA1YmRkNDk4MSEwHwYDVQQDDBhta2NlcnQgcm9vdEA5MjFmMDViZGQ0OTgw +HhcNMjYwMTIyMjAwMzIwWhcNMjgwNDIyMjAwMzIwWjBFMScwJQYDVQQKEx5ta2Nl +cnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxGjAYBgNVBAsMEXJvb3RANzk2OWU1 +ZjA2MGM4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2u+HpnTr+Ecj +X6Ur4I8YHVKiEIgOFozJWWAeCPSZg0lr9/z3UDl7QPfRKaDk11OMN7DB0Y/ZAIHj +kMFdYc+ODOUAKGv/MvlM41OeRUXhrt0Gr/TJoDe9RLzs9ffASpqTHUNNrmyj/fLP +M5VZU+4Y2POFq8qji6Otkvqr6wp+2CTS/fkAtenFS2X+Z5u6BBq1/KBSWZhUFSuJ +sAGN9u+l20Cj/Sxp2nD1npvMEvPehFKEK2tZGgFr+X426jJ4Znd19EyZoI/xetG6 +ybSzBdQk1KbhWFa3LPuOG814m9Qh21vaL0Hj2hpqC3KEQ2jiuCovjRS+RBGatltl +5m47Cj0UrQIDAQABo3YwdDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB +BQUHAwEwHwYDVR0jBBgwFoAUjf0NOmUuoqSSD1Mbu/3G+wYxvjEwLAYDVR0RBCUw +I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB +CwUAA4IBgQAorThUyu4FxHkOafMp/4CmvptfNvYeYP81DW0wpJxGL68Bz+idvXKv +JopX/ZAr+dDhS/TLeSnzt83W7TaBBHY+usvsiBx9+pZOVZIpreXRamPu7utmuC46 +dictMNGlRNX9bwAApOJ24NCpOVSIIKtjHcjl4idwUHqLVGS+3wsmxIILYxigzkuT +fcK5vs0ItZWeuunsBAGb/U/Iu9zZ71rtmBejxNPyEvd8+bZC0m2mtV8C0Lpn58jZ +FiEf5OHiOdWG9O/uh3QeVWkuKLmaH6a8VdKRSIlOxEEZdkUYlwuVvzeWgFw4kjm8 +rNWz0aIPovmcgLXoUG1d1D8klveGd3plF7N2p3xWqKmS6R6FJHx7MIxH6XmBATii +x/193Sgzqe8mwXQr14ulg2M/B3ZWleNdD6SeieADSgvRKjnlO7xwUmcFrffnEKEG +WcSPoGIuZ8V2pkYLh7ipPN+tFUbhmrWnsao5kQ0sqLlfscyO9hYQeQRtTjtC3P8r +FJYOdBMOCzE= -----END CERTIFICATE----- diff --git a/certs/localhost.key b/certs/localhost.key index a0a4de3c..8f547883 100644 --- a/certs/localhost.key +++ b/certs/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Qka1JnN35mku -L55+OuTEkk19XFXPZUuGSc0zZYp45jElMr9VpNirmGtzPLp8RScyEMAkKAQ9QWU4 -re+fWAUko0w3ybtquQtWTo9i2vHwJPDDvxDToUshOvqFHS+Uk2k3gyyYbFjBShSm -fWPkl19kXt+jE0BlGLCiil/tknGW0O48i4WOZVmp3NHxzq+aoip1rDH9Xqwv1+LN -mv4IMfehOxNSKufaoz7WJsG08OIG99eiZSyvoxU5xO2+cy8B3L019XzyrAXswRYE -YKNeShPVsoIsPyGOfyXsaNGY9yYwUTYapYS98mIWQErQKBIdgRTDOjRS1U15CvST -SqQNG5vpAgMBAAECggEAAnv0Dw1Mv+rRy4ZyxtObEVPXPRzoxnDDXzHP4E16BTye -Fc/4pSBUIAUn2bPvLz0/X8bMOa4dlDcIv7Eu9Pvns8AY70vMaUReA80fmtHVD2xX -1PCT0X3InnxRAYKstSIUIGs+aHvV5Z+iJ8F82soOStN1MU56h+JLWElL5deCPHq3 -tLZT8wM9aOZlNG72kJ71+DlcViahynQj8+VrionOLNjTJ2Jv/ByjM3GMIuSdBrgd -Sl4YAcdn6ontjJGoTgI+e+qkBAPwMZxHarNGQgbS0yNVIJe7Lq4zIKHErU/ZSmpD -GzhdVNzhrjADNIDzS7G+pxtz+aUxGtmRvOyopy8GAQKBgQDEPp2mRM+uZVVT4e1j -pkKO1c3O8j24I5mGKwFqhhNs3qGy051RXZa0+cQNx63GokXQan9DIXzc/Il7Y72E -z9bCFbcSWnlP8dBIpWiJm+UmqLXRyY4N8ecNnzL5x+Tuxm5Ij+ixJwXgdz/TLNeO -MBzu+Qy738/l/cAYxwcF7mR7AQKBgQDxq1F95HzCxBahRU9OGUO4s3naXqc8xKCC -m3vbbI8V0Exse2cuiwtlPPQWzTPabLCJVvCGXNru98sdeOu9FO9yicwZX0knOABK -QfPyDeITsh2u0C63+T9DNn6ixI/T68bTs7DHawEYbpS7bR50BnbHbQrrOAo6FSXF -yC7+Te+o6QKBgQCXEWSmo/4D0Dn5Usg9l7VQ40GFd3EPmUgLwntal0/I1TFAyiom -gpcLReIogXhCmpSHthO1h8fpDfZ/p+4ymRRHYBQH6uHMKugdpEdu9zVVpzYgArp5 -/afSEqVZJwoSzWoELdQA23toqiPV2oUtDdiYFdw5nDccY1RHPp8nb7amAQKBgQDj -f4DhYDxKJMmg21xCiuoDb4DgHoaUYA0xpii8cL9pq4KmBK0nVWFO1kh5Robvsa2m -PB+EfNjkaIPepLxWbOTUEAAASoDU2JT9UoTQcl1GaUAkFnpEWfBB14TyuNMkjinH -lLpvn72SQFbm8VvfoU4jgfTrZP/LmajLPR1v6/IWMQKBgBh9qvOTax/GugBAWNj3 -ZvF99rHOx0rfotEdaPcRN66OOiSWILR9yfMsTvwt1V0VEj7OqO9juMRFuIyB57gd -Hs/zgbkuggqjr1dW9r22P/UpzpodAEEN2d52RSX8nkMOkH61JXlH2MyRX65kdExA -VkTDq6KwomuhrU3z0+r/MSOn +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDa74emdOv4RyNf +pSvgjxgdUqIQiA4WjMlZYB4I9JmDSWv3/PdQOXtA99EpoOTXU4w3sMHRj9kAgeOQ +wV1hz44M5QAoa/8y+UzjU55FReGu3Qav9MmgN71EvOz198BKmpMdQ02ubKP98s8z +lVlT7hjY84WryqOLo62S+qvrCn7YJNL9+QC16cVLZf5nm7oEGrX8oFJZmFQVK4mw +AY3276XbQKP9LGnacPWem8wS896EUoQra1kaAWv5fjbqMnhmd3X0TJmgj/F60brJ +tLMF1CTUpuFYVrcs+44bzXib1CHbW9ovQePaGmoLcoRDaOK4Ki+NFL5EEZq2W2Xm +bjsKPRStAgMBAAECggEBALFhpHwe+xh7OpPBlR0pkpYfXyMZuKBYjMIW9/61frM6 +B3oywIWFLPFkV1js/Lvg+xgb48zQSTb6BdBAelJHAYY8+7XEWk2IYt1D4FWr2r/8 +X/Cr2bgvsO9CSpK2mltXhZ4N66BIcU3NLkdS178Ch6svErwvP/ZhNL6Czktug3rG +S2fxpKqoVQhqWiEBV0vBWpw2oskvcpP2Btev2eaJeDmP1IkCaKIU9jJzmSg8UKj3 +AxJJ8lJvlDi2Z8mfB0BVIFZSI1s44LqbuPdITsWvG49srdAhyLkifcbY2r7eH8+s +rFNswCaqqNwmzZXHMFedfvgVJHtCTGX/U8G55dcG/YECgYEA6PvcX+axT4aSaG+/ +fQpyW8TmNmIwS/8HD8rA5dSBB2AE+RnqbxxB1SWWk4w47R/riYOJCy6C88XUZWLn +05cYotR9lA6tZOYqIPUIxfCHl4vDDBnOGAB0ga3z5vF3X7HV6uspPFcrGVJ9k42S +BK1gk7Kj8RxVQMqXSqkR/pXJ4l0CgYEA8JBkZ4MRi1D74SAU+TY1rnUCTs2EnP1U +TKAI9qHyvNJOFZaObWPzegkZryZTm+yTblwvYkryMjvsiuCdv6ro/VKc667F3OBs +dJ/8+ylO0lArP9a23QHViNSvQmyi3bsw2UpIGQRGq2C4LxDceo5cYoVOWIlLl8u3 +LUuL0IfxdpECgYEAyMd0DPljyGLyfSoAXaPJFajDtA4+DOAEl+lk/yt43oAzCPD6 +hTJW0XcJIrJuxHsDoohGa+pzU90iwxTPMBtAUeLJLfTQHOn1WF2SZ/J3B3ScbCs4 +3ppVzQO580YYV9GLxl1ONf/w1muuaKBSO9GmLuJ+QeTm22U7qE23gixXxMkCgYEA +3R7cK4l+huBZpgUnQitiDInhJS4jx2nUItq3YnxZ8tYckBtjr4lAM9xJj4VbNOew +XLC/nUnmdeY+9yif153xq2hUdQ6hMPXYuxqUHwlJOmgWWQez7lHRRYS51ASnb8iw +jgqJWvVjQAQXSKvm/X/9y1FdQmRw54aJSUk3quZKPQECgYA5DqFXn/jYOUCGfCUD +RENWe+3fNZSJCWmr4yvjEy9NUT5VcA+/hZHbbLUnVHfAHxJmdGhTzFe+ZGWFAGwi +Wo62BDu1fqHHegCWaFFluDnz8iZTxXtHMEGSBZnXC4f5wylcxRtCPUbrLMRTPo4O +t85qeBu1dMq1XQtU6ab3w3W0nw== -----END PRIVATE KEY----- diff --git a/certs/mkcert-ca.crt b/certs/mkcert-ca.crt new file mode 100644 index 00000000..a7ccfba6 --- /dev/null +++ b/certs/mkcert-ca.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEjjCCAvagAwIBAgIRAJtq2Z0+W981Ct2dMVPb3bQwDQYJKoZIhvcNAQELBQAw +XzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMRowGAYDVQQLDBFyb290 +QDkyMWYwNWJkZDQ5ODEhMB8GA1UEAwwYbWtjZXJ0IHJvb3RAOTIxZjA1YmRkNDk4 +MB4XDTI2MDEyMjA5NDcxNVoXDTM2MDEyMjA5NDcxNVowXzEeMBwGA1UEChMVbWtj +ZXJ0IGRldmVsb3BtZW50IENBMRowGAYDVQQLDBFyb290QDkyMWYwNWJkZDQ5ODEh +MB8GA1UEAwwYbWtjZXJ0IHJvb3RAOTIxZjA1YmRkNDk4MIIBojANBgkqhkiG9w0B +AQEFAAOCAY8AMIIBigKCAYEAxXR5gXDwv5cfQSud1eEhwDuaSaf5kf8NtPnucZXY +AN+/QW1+OEKJFuawj/YrSbL/yIB8sUSJToEYNJ4LAgzIZ4+TPYVvOIPqQnimfj98 +2AKCt73U/AMvoEpts7U0s37f5wF8o+BHXfChxyN//z96+wsQ+2+Q9QBGjirvScF+ +8FRnupcygDeGZ8x3JQVaEfEV6iYyXFl/4tEDVr9QX4avyUlf0vp1Y90TG3L42JYQ +xDU37Ct9dqsxPCPOPjmkQi9HV5TeqLTs/4NdrEYOSk7bOVMzL8EHs2prRL7sWzYJ +gRT+VXFPpoSCkZs1gS3FNXukTGx5LNsstyJZRa99cGgDcqvNseig06KUzZrRnCig +kARLF/n8VTpHETEuTdxdnXJO3i2N/99mG/2/lej9HNDMaqg45ur5EhaFhHarXMtc +wy7nfGoThzscZvpFbVHorhgRxzsUqRHTMHa9mUOYtShMA0IwFccHcczY3CDxXLg9 +hC+24pdiCxtRmi23JB10Th+nAgMBAAGjRTBDMA4GA1UdDwEB/wQEAwICBDASBgNV +HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSN/Q06ZS6ipJIPUxu7/cb7BjG+MTAN +BgkqhkiG9w0BAQsFAAOCAYEAghQL80XpPs70EbCmETYri5ryjJPbsxK3Z3QgBYoy +/P/021eK5woLx4EDeEUyrRayOtLlDWiIMdV7FmlY4GpEdonVbqFJY2gghkIuQpkw +lWqbk08F+iXe8PSGD1qz4y6enioCRAx+RaZ6jnVJtaC8AR257FVhGIzDBiOA+LDM +L1yq6Bxxij17q9s5HL9KuzxWRMuXACHmaGBXHpl/1n4dIxi2lXRgp+1xCR/VPNPt +/ZRy29kncd8Fxx+VEtc0muoJvRo4ttVWhBvAVJrkAeukjYKpcDDfRU6Y22o54jD/ +mseDb+0UPwSxaKbnGJlCcRbbh6i14bA4KfPq93bZX+Tlq9VCp8LvQSl3oU+23RVc +KjBB9EJnoBBNGIY7VmRI+QowrnlP2wtg2fTNaPqULtjqA9frbMTP0xTlumLzGB+6 +9Da7/+AE2in3Aa8Xyry4BiXbk2L6c1xz/Cd1ZpFrSXAOEi1Xt7so/Ck0yM3c2KWK +5aSfCjjOzONMPJyY1oxodJ1p +-----END CERTIFICATE----- diff --git a/docker/nginx/dev.conf b/docker/nginx/dev.conf index 79725071..3a5da250 100644 --- a/docker/nginx/dev.conf +++ b/docker/nginx/dev.conf @@ -9,13 +9,39 @@ # - Frontend accessible on https://localhost (port 443) # - Backend API on http://localhost:3001 # - Port 80 redirects to HTTPS +# +# IMPORTANT: Dual Hostname Configuration (localhost AND 127.0.0.1) +# ============================================================================ +# The server_name directive includes BOTH 'localhost' and '127.0.0.1' to +# prevent SSL certificate errors when resources use different hostnames. +# +# Problem Scenario: +# 1. User accesses site via https://localhost/ +# 2. Database stores image URLs as https://127.0.0.1/flyer-images/... +# 3. Browser treats these as different origins, showing ERR_CERT_AUTHORITY_INVALID +# +# Solution: +# - mkcert generates certificates valid for: localhost, 127.0.0.1, ::1 +# - NGINX accepts requests to BOTH hostnames using the same certificate +# - Users can access via either hostname without SSL warnings +# +# The self-signed certificate is generated in Dockerfile.dev with: +# mkcert localhost 127.0.0.1 ::1 +# +# This creates a certificate with Subject Alternative Names (SANs) for all +# three hostnames, allowing NGINX to serve valid HTTPS for each. +# +# See also: +# - Dockerfile.dev (certificate generation, ~line 69) +# - docs/FLYER-URL-CONFIGURATION.md (URL configuration details) +# - docs/development/DEBUGGING.md (SSL troubleshooting) # ============================================================================ # HTTPS Server (main) server { listen 443 ssl; listen [::]:443 ssl; - server_name localhost; + server_name localhost 127.0.0.1; # SSL Configuration (self-signed certificates from mkcert) ssl_certificate /app/certs/localhost.crt; @@ -80,7 +106,7 @@ server { server { listen 80; listen [::]:80; - server_name localhost; + server_name localhost 127.0.0.1; return 301 https://$host$request_uri; } diff --git a/docs/FLYER-URL-CONFIGURATION.md b/docs/FLYER-URL-CONFIGURATION.md index 7f086b11..206ff468 100644 --- a/docs/FLYER-URL-CONFIGURATION.md +++ b/docs/FLYER-URL-CONFIGURATION.md @@ -12,6 +12,87 @@ Flyer image and icon URLs are environment-specific to ensure they point to the c | Test | `https://flyer-crawler-test.projectium.com` | `https://flyer-crawler-test.projectium.com/flyer-images/safeway-flyer.jpg` | | Production | `https://flyer-crawler.projectium.com` | `https://flyer-crawler.projectium.com/flyer-images/safeway-flyer.jpg` | +**Note:** The dev container accepts connections to **both** `https://localhost/` and `https://127.0.0.1/` thanks to the SSL certificate and NGINX configuration. See [SSL Certificate Configuration](#ssl-certificate-configuration-dev-container) below. + +## SSL Certificate Configuration (Dev Container) + +The dev container uses self-signed certificates generated by [mkcert](https://github.com/FiloSottile/mkcert) to enable HTTPS locally. This configuration solves a common mixed-origin SSL issue. + +### The Problem + +When users access the site via `https://localhost/` but image URLs in the database use `https://127.0.0.1/...`, browsers treat these as different origins. This causes `ERR_CERT_AUTHORITY_INVALID` errors when loading images, even though both hostnames point to the same server. + +### The Solution + +1. **Certificate Generation** (`Dockerfile.dev`): + + ```bash + mkcert localhost 127.0.0.1 ::1 + ``` + + This creates a certificate with Subject Alternative Names (SANs) for all three hostnames. + +2. **NGINX Configuration** (`docker/nginx/dev.conf`): + + ```nginx + server_name localhost 127.0.0.1; + ``` + + NGINX accepts requests to both hostnames using the same SSL certificate. + +### How It Works + +| Component | Configuration | +| -------------------- | ---------------------------------------------------- | +| SSL Certificate SANs | `localhost`, `127.0.0.1`, `::1` | +| NGINX `server_name` | `localhost 127.0.0.1` | +| Seed Script URLs | Uses `https://127.0.0.1` (works with DB constraints) | +| User Access | Either `https://localhost/` or `https://127.0.0.1/` | + +### Why This Matters + +- **Database Constraints**: The `flyers` table has CHECK constraints requiring URLs to start with `http://` or `https://`. Relative URLs are not allowed. +- **Consistent Behavior**: Users can access the site using either hostname without SSL warnings. +- **Same Certificate**: Both hostnames use the same self-signed certificate, eliminating mixed-content errors. + +### Verifying the Configuration + +```bash +# Check certificate SANs +podman exec flyer-crawler-dev openssl x509 -in /app/certs/localhost.crt -text -noout | grep -A1 "Subject Alternative Name" + +# Expected output: +# X509v3 Subject Alternative Name: +# DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1 + +# Test both hostnames respond +curl -k https://localhost/health +curl -k https://127.0.0.1/health +``` + +### Troubleshooting SSL Issues + +If you encounter `ERR_CERT_AUTHORITY_INVALID`: + +1. **Check NGINX is running**: `podman exec flyer-crawler-dev nginx -t` +2. **Verify certificate exists**: `podman exec flyer-crawler-dev ls -la /app/certs/` +3. **Ensure both hostnames are in server_name**: Check `/etc/nginx/sites-available/default` +4. **Rebuild container if needed**: The certificate is generated at build time + +### Permanent Fix: Install CA Certificate (Recommended) + +To permanently eliminate SSL certificate warnings, install the mkcert CA certificate on your system. This is optional but provides a better development experience. + +The CA certificate is located at `certs/mkcert-ca.crt` in the project root. See [`certs/README.md`](../certs/README.md) for platform-specific installation instructions (Windows, macOS, Linux, Firefox). + +After installation: + +- Your browser will trust all mkcert certificates without warnings +- Both `https://localhost/` and `https://127.0.0.1/` will work without SSL errors +- Flyer images will load without `ERR_CERT_AUTHORITY_INVALID` errors + +See also: [Debugging Guide - SSL Issues](development/DEBUGGING.md#ssl-certificate-issues) + ## NGINX Static File Serving All environments serve flyer images as static files with browser caching: @@ -41,7 +122,7 @@ Set `FLYER_BASE_URL` in your environment configuration: ```bash # Dev container (.env) -FLYER_BASE_URL=https://127.0.0.1 +FLYER_BASE_URL=https://localhost # Test environment FLYER_BASE_URL=https://flyer-crawler-test.projectium.com @@ -58,7 +139,7 @@ The seed script ([src/db/seed.ts](../src/db/seed.ts)) automatically uses the cor 2. `NODE_ENV` value: - `production` → `https://flyer-crawler.projectium.com` - `test` → `https://flyer-crawler-test.projectium.com` - - Default → `https://127.0.0.1` + - Default → `https://localhost` The seed script also copies test images from `src/tests/assets/` to `public/flyer-images/`: @@ -78,8 +159,8 @@ podman exec -it flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev # Run the update (dev container uses HTTPS with self-signed certs) UPDATE flyers SET - image_url = REPLACE(image_url, 'example.com', '127.0.0.1'), - icon_url = REPLACE(icon_url, 'example.com', '127.0.0.1') + image_url = REPLACE(image_url, 'example.com', 'localhost'), + icon_url = REPLACE(icon_url, 'example.com', 'localhost') WHERE image_url LIKE '%example.com%' OR icon_url LIKE '%example.com%'; @@ -131,8 +212,10 @@ export const getFlyerBaseUrl = (): string => { } // Check if we're in dev container (DB_HOST=postgres is typical indicator) + // Use 'localhost' instead of '127.0.0.1' to match the hostname users access + // This avoids SSL certificate mixed-origin issues in browsers if (process.env.DB_HOST === 'postgres' || process.env.DB_HOST === '127.0.0.1') { - return 'https://127.0.0.1'; + return 'https://localhost'; } if (process.env.NODE_ENV === 'production') { @@ -169,13 +252,13 @@ This approach ensures tests work correctly in all environments (dev container, C | `docker/nginx/dev.conf` | Added `/flyer-images/` location block for static file serving | | `.env.example` | Added `FLYER_BASE_URL` variable | | `sql/update_flyer_urls.sql` | SQL script for updating existing data | -| Test files | Updated mock data to use `https://127.0.0.1` | +| Test files | Updated mock data to use `https://localhost` | ## Summary - Seed script now uses environment-specific HTTPS URLs - Seed script copies test images from `src/tests/assets/` to `public/flyer-images/` - NGINX serves `/flyer-images/` as static files with 7-day cache -- Test files updated with `https://127.0.0.1` +- Test files updated with `https://localhost` (not `127.0.0.1` to avoid SSL mixed-origin issues) - SQL script provided for updating existing data - Documentation updated for each environment diff --git a/docs/development/DEBUGGING.md b/docs/development/DEBUGGING.md index 35fabccc..010308bd 100644 --- a/docs/development/DEBUGGING.md +++ b/docs/development/DEBUGGING.md @@ -11,6 +11,7 @@ Common debugging strategies and troubleshooting patterns for Flyer Crawler. - [API Errors](#api-errors) - [Authentication Problems](#authentication-problems) - [Background Job Issues](#background-job-issues) +- [SSL Certificate Issues](#ssl-certificate-issues) - [Frontend Issues](#frontend-issues) - [Performance Problems](#performance-problems) - [Debugging Tools](#debugging-tools) @@ -494,6 +495,77 @@ pm2 logs flyer-crawler-worker --lines 100 --- +## SSL Certificate Issues + +### Images Not Loading (ERR_CERT_AUTHORITY_INVALID) + +**Symptom**: Flyer images fail to load with `ERR_CERT_AUTHORITY_INVALID` in browser console + +**Cause**: Mixed hostname origins - user accesses via `localhost` but images use `127.0.0.1` (or vice versa) + +**Debug**: + +```bash +# Check which hostname images are using +podman exec flyer-crawler-dev psql -U postgres -d flyer_crawler_dev \ + -c "SELECT image_url FROM flyers LIMIT 1;" + +# Verify certificate includes both hostnames +podman exec flyer-crawler-dev openssl x509 -in /app/certs/localhost.crt -text -noout | grep -A1 "Subject Alternative Name" + +# Check NGINX accepts both hostnames +podman exec flyer-crawler-dev grep "server_name" /etc/nginx/sites-available/default +``` + +**Solution**: The dev container is configured to handle both hostnames: + +1. Certificate includes SANs for `localhost`, `127.0.0.1`, and `::1` +2. NGINX `server_name` directive includes both `localhost` and `127.0.0.1` + +If you still see errors: + +```bash +# Rebuild container to regenerate certificate +podman-compose down +podman-compose build --no-cache flyer-crawler-dev +podman-compose up -d +``` + +See [FLYER-URL-CONFIGURATION.md](../FLYER-URL-CONFIGURATION.md#ssl-certificate-configuration-dev-container) for full details. + +### Self-Signed Certificate Not Trusted + +**Symptom**: Browser shows security warning for `https://localhost` + +**Temporary Workaround**: Accept the warning by clicking "Advanced" > "Proceed to localhost" + +**Permanent Fix (Recommended)**: Install the mkcert CA certificate to eliminate all SSL warnings. + +The CA certificate is located at `certs/mkcert-ca.crt` in the project root. See [`certs/README.md`](../../certs/README.md) for platform-specific installation instructions (Windows, macOS, Linux, Firefox). + +After installation: + +- Your browser will trust all mkcert certificates without warnings +- Both `https://localhost/` and `https://127.0.0.1/` will work without SSL errors +- Flyer images will load without `ERR_CERT_AUTHORITY_INVALID` errors + +### NGINX SSL Configuration Test + +**Debug**: + +```bash +# Test NGINX configuration +podman exec flyer-crawler-dev nginx -t + +# Check if NGINX is listening on 443 +podman exec flyer-crawler-dev netstat -tlnp | grep 443 + +# Verify certificate files exist +podman exec flyer-crawler-dev ls -la /app/certs/ +``` + +--- + ## Frontend Issues ### Hot Reload Not Working diff --git a/docs/getting-started/INSTALL.md b/docs/getting-started/INSTALL.md index ed4a7dea..e4090aeb 100644 --- a/docs/getting-started/INSTALL.md +++ b/docs/getting-started/INSTALL.md @@ -119,6 +119,31 @@ npm run dev - **Frontend**: http://localhost:5173 - **Backend API**: http://localhost:3001 +### Dev Container with HTTPS (Full Stack) + +When using the full dev container stack with NGINX (via `compose.dev.yml`), access the application over HTTPS: + +- **Frontend**: https://localhost or https://127.0.0.1 +- **Backend API**: http://localhost:3001 + +**SSL Certificate Notes:** + +- The dev container uses self-signed certificates generated by mkcert +- Both `localhost` and `127.0.0.1` are valid hostnames (certificate includes both as SANs) +- If images fail to load with SSL errors, see [FLYER-URL-CONFIGURATION.md](../FLYER-URL-CONFIGURATION.md#ssl-certificate-configuration-dev-container) + +**Eliminate SSL Warnings (Recommended):** + +To avoid browser security warnings for self-signed certificates, install the mkcert CA certificate on your system. The CA certificate is located at `certs/mkcert-ca.crt` in the project root. + +See [`certs/README.md`](../../certs/README.md) for platform-specific installation instructions (Windows, macOS, Linux, Firefox). + +After installation: + +- Your browser will trust all mkcert certificates without warnings +- Both `https://localhost/` and `https://127.0.0.1/` will work without SSL errors +- Flyer images will load without `ERR_CERT_AUTHORITY_INVALID` errors + ### Managing the Container | Action | Command | diff --git a/docs/getting-started/QUICKSTART.md b/docs/getting-started/QUICKSTART.md index d72ed1a4..d0f3779a 100644 --- a/docs/getting-started/QUICKSTART.md +++ b/docs/getting-started/QUICKSTART.md @@ -86,6 +86,17 @@ npm run dev - **Backend API**: http://localhost:3001 - **Health Check**: http://localhost:3001/health +### Dev Container (HTTPS) + +When using the full dev container with NGINX, access via HTTPS: + +- **Frontend**: https://localhost or https://127.0.0.1 +- **Backend API**: http://localhost:3001 + +**Note:** The dev container accepts both `localhost` and `127.0.0.1` for HTTPS connections. The self-signed certificate is valid for both hostnames. + +**SSL Certificate Warnings:** To eliminate browser security warnings for self-signed certificates, install the mkcert CA certificate. See [`certs/README.md`](../../certs/README.md) for platform-specific installation instructions. This is optional but recommended for a better development experience. + ## Verify Installation ```bash diff --git a/sql/update_flyer_urls.sql b/sql/update_flyer_urls.sql index da0f31d2..622eeb2f 100644 --- a/sql/update_flyer_urls.sql +++ b/sql/update_flyer_urls.sql @@ -1,21 +1,31 @@ -- Update flyer URLs from example.com to environment-specific URLs -- -- This script should be run after determining the correct base URL for the environment: --- - Dev container: http://127.0.0.1 +-- - Dev container: https://localhost (NOT 127.0.0.1 - avoids SSL mixed-origin issues) -- - Test environment: https://flyer-crawler-test.projectium.com -- - Production: https://flyer-crawler.projectium.com -- For dev container (run in dev database): +-- Uses 'localhost' instead of '127.0.0.1' to match how users access the site. +-- This avoids ERR_CERT_AUTHORITY_INVALID errors when images are loaded from a +-- different origin than the page. UPDATE flyers SET - image_url = REPLACE(image_url, 'example.com', '127.0.0.1'), - image_url = REPLACE(image_url, 'https://', 'http://'), - icon_url = REPLACE(icon_url, 'example.com', '127.0.0.1'), - icon_url = REPLACE(icon_url, 'https://', 'http://') + image_url = REPLACE(image_url, 'example.com', 'localhost'), + icon_url = REPLACE(icon_url, 'example.com', 'localhost') WHERE image_url LIKE '%example.com%' OR icon_url LIKE '%example.com%'; +-- Also fix any existing 127.0.0.1 URLs to use localhost: +UPDATE flyers +SET + image_url = REPLACE(image_url, '127.0.0.1', 'localhost'), + icon_url = REPLACE(icon_url, '127.0.0.1', 'localhost') +WHERE + image_url LIKE '%127.0.0.1%' + OR icon_url LIKE '%127.0.0.1%'; + -- For test environment (run in test database): -- UPDATE flyers -- SET @@ -37,7 +47,7 @@ WHERE -- Verify the changes: SELECT flyer_id, image_url, icon_url FROM flyers -WHERE image_url LIKE '%127.0.0.1%' - OR icon_url LIKE '%127.0.0.1%' +WHERE image_url LIKE '%localhost%' + OR icon_url LIKE '%localhost%' OR image_url LIKE '%flyer-crawler%' OR icon_url LIKE '%flyer-crawler%'; diff --git a/src/db/seed.ts b/src/db/seed.ts index b48acbc7..5ac27ca7 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -12,8 +12,8 @@ import path from 'node:path'; import bcrypt from 'bcrypt'; import { logger } from '../services/logger.server'; -// Determine base URL based on environment -// Dev container: https://127.0.0.1 +// Determine base URL for flyer images based on environment +// Dev container: https://127.0.0.1 (NGINX now accepts both localhost and 127.0.0.1) // Test: https://flyer-crawler-test.projectium.com // Production: https://flyer-crawler.projectium.com const BASE_URL = diff --git a/src/tests/utils/testHelpers.ts b/src/tests/utils/testHelpers.ts index b0524a4e..3451914b 100644 --- a/src/tests/utils/testHelpers.ts +++ b/src/tests/utils/testHelpers.ts @@ -15,7 +15,7 @@ export const getTestBaseUrl = (): string => { /** * Get the flyer base URL for test data based on environment. * Uses FLYER_BASE_URL if set, otherwise detects environment: - * - Dev container: http://127.0.0.1 + * - Dev container: https://localhost (NOT 127.0.0.1 - avoids SSL mixed-origin issues) * - Test: https://flyer-crawler-test.projectium.com * - Production: https://flyer-crawler.projectium.com * - Default: https://example.com (for unit tests) @@ -26,8 +26,10 @@ export const getFlyerBaseUrl = (): string => { } // Check if we're in dev container (DB_HOST=postgres is typical indicator) + // Use 'localhost' instead of '127.0.0.1' to match the hostname users access + // This avoids SSL certificate mixed-origin issues in browsers if (process.env.DB_HOST === 'postgres' || process.env.DB_HOST === '127.0.0.1') { - return 'http://127.0.0.1'; + return 'https://localhost'; } if (process.env.NODE_ENV === 'production') {