RewriteEngine On
RewriteBase /traderalign/

# Directory listing was found enabled server-wide during a pre-launch
# security audit (confirmed live: any directory with no index file, e.g.
# /assets/, /dead-files/, /public/uploads/trade-screenshots/<userId>/,
# rendered Apache's full auto-index page). For the uploads directories in
# particular this defeats the "unguessable random filename" protection
# those features rely on — a listable directory hands over every real
# filename directly, no guessing needed. Nothing in this app depends on
# directory listing being available anywhere.
Options -Indexes

# Block direct web access to any dotfile/dot-directory (.git, .gitignore,
# .env, .htaccess itself, etc.) — found during a pre-launch security audit
# that .git/ was directly readable over HTTP (confirmed live: .git/HEAD
# and .git/config both returned real content), meaning the entire repo
# history — including any committed secrets in earlier revisions — was
# downloadable with standard ".git dumping" tools. /.well-known/ is
# excluded so a future ACME/Let's Encrypt HTTPS certificate challenge
# still works once this app is deployed with a real TLS cert.
RewriteRule "^(?!\.well-known/)(?:.*/)?\.\w" - [F,L]

# 2026-08-21 IP-exposure fix: calculator-source.html holds the Calculator's
# client-side algorithm and internal engineering comments. Being a real,
# on-disk file, the generic "only rewrite to index.php if the path ISN'T a
# real file" rule below always skipped it, so Apache served it directly
# with zero authentication — anyone with the URL could open it straight,
# no login required. index.php already has an auth-gated route case for
# this exact path (added at the same time as this rule) that was, until
# now, permanently unreachable dead code. This forces every request for
# this one path through the router unconditionally, ahead of the generic
# rule below, so that case actually runs. The <Files "calculator-source.
# html"> header block further down in this file no longer applies once
# this fires (Apache now serves index.php for this URL, not the literal
# file) — index.php's own route case sets the equivalent headers itself.
RewriteRule ^calculator-source\.html$ index.php [QSA,L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

# Block direct web access to internal/non-asset files that live alongside
# the app but were never meant to be fetched over HTTP — found during a
# pre-launch security audit to be fully public (confirmed live: every one
# of the .sql schema-migration files at the project root, the full
# internal architecture doc CLAUDE.md, a stray debug log containing a
# real trade's data, and this very audit's own spec file were all
# downloadable with no authentication, since the RewriteCond above only
# intercepts requests for paths that AREN'T a real file — none of these
# are routed through index.php, Apache just serves them directly). This
# is an extension blocklist, not a rewrite, so it applies regardless of
# which directory the file sits in.
<FilesMatch "\.(sql|md|log|docx?|xlsx?|pdf|ini|bak|zip|tar|gz|7z|yml|yaml|env|config)$">
    <IfModule mod_authz_core.c>
        Require all denied
    </IfModule>
    <IfModule !mod_authz_core.c>
        Order deny,allow
        Deny from all
    </IfModule>
</FilesMatch>

# landing.html and calculator-source.html are real files, so — like every
# other case in this .htaccess — a request for their own literal filename
# (/traderalign/landing.html, /traderalign/calculator-source.html) is
# served directly by Apache and never reaches index.php at all, meaning
# none of the security headers set there (X-Frame-Options, CSP, etc.) ever
# applied to them (confirmed live during a pre-launch security audit: a
# direct request for either returned zero of those headers). index.php's
# own router case for /calculator-source.html has therefore always been
# dead code for a request shaped that way — only reachable if the real
# file were ever deleted. Headers mirrored here instead, since this is the
# only place Apache will actually apply them for these two paths.
# calculator-source.html is deliberately SAMEORIGIN (not DENY) — it's
# legitimately embedded in an <iframe> by public/calculator.php from this
# same origin; landing.html is never framed by anything, so it keeps DENY.
<IfModule mod_headers.c>
    <Files "landing.html">
        Header always set X-Content-Type-Options "nosniff"
        Header always set X-Frame-Options "DENY"
        Header always set Referrer-Policy "strict-origin-when-cross-origin"
        Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
    </Files>
    <Files "calculator-source.html">
        Header always set X-Content-Type-Options "nosniff"
        Header always set X-Frame-Options "SAMEORIGIN"
        Header always set Referrer-Policy "strict-origin-when-cross-origin"
        # connect-src FIX (reported live by Kevin, same day as the CSP was
        # first added): the CSP below was written as "the real, complete
        # list of external hosts this app loads anything from," but that
        # audit checked script/style/font src=/href= tags and missed the
        # 4 external hosts calculator-source.html's own fetchAllRates()
        # calls from JS (api.frankfurter.app -> api.exchangerate-api.com
        # -> www.freeforexapi.com -> cdn.jsdelivr.net's currency-api,
        # first-success-wins, confirmed via a direct grep of this exact
        # file for every https:// URL it references) for the calculator's
        # live cross-currency rate fetch. connect-src 'self' silently
        # blocked all four with no visible error beyond the browser
        # console — the calculator degraded to its own "type the rate
        # manually" fallback for every user, indistinguishable from a
        # real no-internet case. Added all 4 explicitly rather than
        # loosening connect-src generally.
        Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://api.frankfurter.app https://api.exchangerate-api.com https://www.freeforexapi.com https://cdn.jsdelivr.net; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"
    </Files>
</IfModule>

# Long-lived caching for static images (Trader Profile illustrations and
# any future static image assets) — these are served directly by Apache
# (the RewriteCond above only rewrites requests that AREN'T a real file),
# so this is the only place cache headers for them can be set. Filenames
# are content-stable (a future artwork refresh replaces the same path),
# so a long max-age is safe; scoped to image extensions only, guarded so
# a XAMPP install without these modules enabled doesn't break anything.
<IfModule mod_expires.c>
    ExpiresActive On
    <FilesMatch "\.(webp|png|jpe?g|gif|svg|ico|avif)$">
        ExpiresDefault "access plus 30 days"
    </FilesMatch>
</IfModule>
<IfModule mod_headers.c>
    <FilesMatch "\.(webp|png|jpe?g|gif|svg|ico|avif)$">
        Header set Cache-Control "public, max-age=2592000"
    </FilesMatch>
</IfModule>