feat: give the frontend a deployment artifact, and show its logo

The repository had no container image and no production-shaped serving
configuration. `dist/server.mjs` is a preview server that applies neither
the security headers nor the cache policy `config/hosting/` declares, so a
deployment had nothing correct to run.

`scripts/generate-nginx-config.ts` derives the server block from
`dist/tech-log-serving-contract.json` plus the two hosting policy files, so
the served headers and cache lifetimes cannot drift from what the contract
declares. It emits no TLS and no proxy blocks: the edge terminates TLS and
routes /api, and baking a backend address into the image would tie the
bundle to one deployment. Static surfaces use `alias` because a base-path
build serves /dev/assets/... out of dist/assets/..., which `root` plus URI
would look for one directory too deep.

The image copies that config next to the bundle and normalises permissions:
the build writes config.json 0600, which nginx cannot read, so the container
came up healthy and answered 403 for the one file the SPA needs to boot.

index.html never referenced public/favicon.svg. The file shipped and nginx
served it, but browsers asked for /favicon.ico, got a 404, and fell back to
the default icon. `%BASE_URL%` rather than an absolute path so a prefixed
deployment points at its own copy.

development.json moves to the HTTP Studio source; the mock source has no
backend to authenticate against, which is the whole point of that profile.
This commit is contained in:
DongHyeonka
2026-08-20 16:14:09 +09:00
parent 5e2b1a5586
commit 83409bef7a
7 changed files with 591 additions and 1 deletions
+24
View File
@@ -0,0 +1,24 @@
# Keycloak realm
`tech-log-realm.json` is imported by the `keycloak` service at start
(`--import-realm`). It exists because the realm was previously created by hand,
which meant §27 of the release checklist — "Keycloak Realm 설정을 복원할 수
있다" — had no answer: nothing in either repository described the realm.
What it declares, and why each part is load-bearing:
- **`studio-author` realm role.** `StudioAuthzEnvironmentPostProcessor` maps this
name to `studio:read` and `studio:write`. The name is configurable through
`APP_STUDIO_AUTHOR_ROLE`; if you change it here, change it there too.
- **`tech-log-bff` confidential client.** The Authorization Code flow belongs to
the backend, not the browser — the SPA never holds a token. `redirectUris` is
relative so the same realm works on any origin the deployment is served from.
- **`realm-roles` protocol mapper.** Without it the roles never reach the token,
the registry resolves zero permissions, and every Studio call answers 403.
## Values that must be replaced
`CHANGE_ME_BFF_SECRET` and `CHANGE_ME_STUDIO_PASSWORD` are placeholders, and the
deploy script substitutes them from the environment before import. They are left
visible rather than pre-filled so a realm file committed with a real secret is an
obvious mistake rather than a quiet one.
+57
View File
@@ -0,0 +1,57 @@
{
"realm": "tech-log",
"enabled": true,
"sslRequired": "none",
"registrationAllowed": false,
"loginTheme": "keycloak",
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"roles": {
"realm": [
{ "name": "studio-author", "description": "Tech Log Studio 편집 권한 (studio:read + studio:write)" }
]
},
"clients": [
{
"clientId": "tech-log-bff",
"name": "Tech Log BFF",
"description": "백엔드가 소유하는 Authorization Code 클라이언트. SPA 는 토큰을 직접 들지 않는다.",
"enabled": true,
"publicClient": false,
"secret": "CHANGE_ME_BFF_SECRET",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": ["/login/oauth2/code/*"],
"webOrigins": ["+"],
"protocolMappers": [
{
"name": "realm-roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"config": {
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true",
"access.token.claim": "true",
"id.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "studio",
"enabled": true,
"emailVerified": true,
"email": "studio@tech-log.local",
"firstName": "Studio",
"lastName": "Author",
"credentials": [{ "type": "password", "value": "CHANGE_ME_STUDIO_PASSWORD", "temporary": false }],
"realmRoles": ["default-roles-tech-log", "studio-author"]
}
]
}