Archived
Two unrelated things the bakery hit on the same afternoon. THE FAVICON was not missing, it was unusable. The head pointed rel=icon at the 1000x1000 logo PNGs, so a browser fetched 71 KB to paint 16 square pixels, and the mark is a vine branch drawn in hairlines -- strokes thinner than one pixel at that size -- which arrives as a grey smudge. Replaced with a real icon set built from one leaf of that branch, filled rather than stroked, because at 16px a silhouette survives and an outline does not. A midrib was drawn first and cut the leaf into two pale slivers at tab size, so it went; the tilt, the two points and the stem carry the shape. The SVG answers prefers-color-scheme itself, which a .ico cannot, so the dark tab strip gets sage on bakery-900 instead of a glowing cream tile. The .ico is listed first on purpose: a browser takes the last format it understands, so reversing the two would hand Chrome the bitmap. PHOTO UPLOADS failed on anything over 1 MB, which is every photo a phone takes. The cause was an absence: nothing configured spring.servlet.multipart, so Boot's 1 MB default applied and the container rejected the file with FileSizeLimitExceededException before it reached ProductPhotoService -- the class whose entire job is turning "whatever came off a phone" into a resized webp. The pipeline could never run on the input it was written for. Now 15 MB a file and 60 MB a request, the latter because the file input is `multiple`. The failure was also ugly, and that is fixed separately: parsed eagerly, an over-sized part throws from inside Tomcat's parameter parsing where no @ExceptionHandler can reach it, so the request died as a 500 and then died again forwarding to /error, because that forward re-parsed the same too-large request (the paired "Exception Processing [ErrorPage...]" lines in the log). resolve-lazily moves the throw into argument binding, where AdminController now catches it and returns the same `problem` flash the domain's other refusals use. max-swallow-size lets the body be discarded so the browser receives that redirect rather than a connection reset. The multipart numbers are asserted rather than trusted, because a default that was never set is exactly the kind of thing that comes back silently.
126 lines
5.6 KiB
YAML
126 lines
5.6 KiB
YAML
spring:
|
|
application:
|
|
name: itsthevine
|
|
datasource:
|
|
url: ${DB_URL:jdbc:postgresql://localhost:5432/itsthevine}
|
|
username: ${DB_USER:itsthevine}
|
|
password: ${DB_PASSWORD:changeme}
|
|
jpa:
|
|
hibernate:
|
|
ddl-auto: validate
|
|
open-in-view: false
|
|
flyway:
|
|
enabled: true
|
|
servlet:
|
|
multipart:
|
|
# THIS BLOCK IS THE WHOLE REASON PHOTO UPLOADS FAILED. Unset, Boot defaults to a 1 MB max-file-size,
|
|
# and a photo off a phone is 3-12 MB — so every real upload died with FileSizeLimitExceededException
|
|
# before it reached ProductPhotoService, which exists precisely to resize "whatever came off a phone".
|
|
# The resizing pipeline could never run on the input it was written for.
|
|
max-file-size: 15MB
|
|
# The file input is `multiple`, so one submit can carry several photos; this bounds the whole request
|
|
# rather than each part. Four full-size photos at once is a realistic morning's worth of new stock.
|
|
max-request-size: 60MB
|
|
# Parse when the controller asks for the files, not while Tomcat is reading parameters. Eagerly, an
|
|
# oversize part throws from inside the container's parameter parsing, which no @ExceptionHandler can
|
|
# reach — the request dies as a 500 and then the forward to /error re-parses and throws again (the
|
|
# "Exception Processing [ErrorPage...]" pairs in the log). Lazily, it surfaces as a
|
|
# MaxUploadSizeExceededException during argument binding, where AdminController can catch it.
|
|
resolve-lazily: true
|
|
|
|
mail:
|
|
host: ${SMTP_SERVER:localhost}
|
|
port: ${SMTP_PORT:25}
|
|
username: ${SMTP_USERNAME:}
|
|
password: ${SMTP_TOKEN:}
|
|
properties:
|
|
mail:
|
|
smtp:
|
|
# Only authenticate when we were actually given credentials — the LAN relay takes mail
|
|
# from the docker network without them.
|
|
auth: ${SMTP_AUTH:false}
|
|
starttls:
|
|
enable: ${SMTP_STARTTLS:false}
|
|
# The local relay / Proton Bridge presents a self-signed cert (CN=127.0.0.1). This trusts
|
|
# only the configured host, not every server we might ever talk to.
|
|
ssl:
|
|
trust: ${SMTP_SERVER:localhost}
|
|
|
|
server:
|
|
tomcat:
|
|
# Read and discard the rest of an over-sized body instead of resetting the connection, so the browser
|
|
# actually receives the redirect and the message rather than "connection reset". Only reachable now for
|
|
# a genuinely enormous file, but that is exactly when a clear answer matters.
|
|
max-swallow-size: -1
|
|
|
|
platform:
|
|
web:
|
|
spa:
|
|
# OFF. The platform's fallback forwards every extension-less path to /index.html so a React SPA can
|
|
# own routing; this site is server-rendered, and /index.html is now only the admin shell. Left on,
|
|
# a mistyped URL would answer with a blank JavaScript page and a 200 instead of the site's own 404.
|
|
# SiteController maps /admin to the shell explicitly — that one route is all the SPA is for.
|
|
enabled: false
|
|
data:
|
|
auditing:
|
|
enabled: true
|
|
contact:
|
|
to: ${CONTACT_TO:}
|
|
from: ${CONTACT_FROM:}
|
|
hub-url: ${CONTACT_HUB_URL:}
|
|
security:
|
|
# Unset means the platform's permit-all chain, which is what a brochure site wants and what the
|
|
# web contract expects (an unknown /api path must 404, not 401). Set SECURITY_MODE=OIDC in the
|
|
# deployment to turn on Authentik login — that, and only that, brings the admin endpoints into
|
|
# existence. Leaving it unset in dev keeps `mvn spring-boot:run` working with no identity provider.
|
|
mode: ${SECURITY_MODE:NONE}
|
|
permit-paths:
|
|
- /api/products
|
|
- /api/categories
|
|
- /api/contact
|
|
- /actuator/health/**
|
|
authenticated-paths:
|
|
- /api/admin/**
|
|
# The admin screen itself, not just its API. The platform sends /api/** a bare 401 (right for
|
|
# fetch) but bounces everything else to Authentik, so protecting the page means a browser that
|
|
# opens /admin lands on the login form and comes back signed in — rather than loading an editor
|
|
# whose every request immediately fails. It also keeps the page out of strangers' hands entirely.
|
|
- /admin/**
|
|
storage:
|
|
# Only consulted when someone uploads a photo, i.e. only in a deployment that also set OIDC above.
|
|
# Blank endpoint leaves the storage auto-config switched off, so tests and local runs boot without
|
|
# MinIO credentials.
|
|
endpoint: ${STORAGE_ENDPOINT:}
|
|
access-key: ${STORAGE_ACCESS_KEY:}
|
|
secret-key: ${STORAGE_SECRET_KEY:}
|
|
|
|
# Absolute URLs for og:url. Only matters to link-preview scrapers, which need a full URL.
|
|
site:
|
|
base-url: ${SITE_BASE_URL:https://itsthevine.com}
|
|
# Hung on the stylesheet URL as ?v=… The SPA's bundles had content hashes in their filenames; one
|
|
# hand-written stylesheet does not, so without this a returning visitor keeps the CSS they cached
|
|
# before the deploy. The CI build already passes the commit sha to the image.
|
|
build: ${GIT_SHA:dev}
|
|
assets:
|
|
# Where uploaded photos land. The key stored on a product is bucket-relative and excludes the
|
|
# prefix, because ProductCatalog re-adds `/images/` when it builds the public URL.
|
|
bucket: ${STORAGE_BUCKET:itsthevine}
|
|
key-prefix: images/
|
|
|
|
management:
|
|
endpoints:
|
|
web:
|
|
exposure:
|
|
include: health,info
|
|
endpoint:
|
|
health:
|
|
probes:
|
|
enabled: true
|
|
health:
|
|
mail:
|
|
# OFF deliberately. Boot's mail contributor opens an SMTP connection on every health check, so a
|
|
# relay outage would report the container unhealthy and get it restarted — taking a perfectly
|
|
# good website down over a side feature. Enquiries are persisted either way, and a failed send
|
|
# is already surfaced to the visitor and the log.
|
|
enabled: false
|