prepare app for public app store distribution

- rebrand from 습관 트래커 to 해빗랩 across templates, manifest, service worker
- add HTTPS redirect middleware for reverse-proxied deployments
- add public landing page (/) and privacy policy page with real data
  handling disclosures
- add in-app account deletion (Apple review requirement)
- add Android TWA Digital Asset Links support (/.well-known/assetlinks.json)
- add SFTP deployment script for the Synology-hosted server

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:41:50 +09:00
co-authored by Claude Sonnet 5
parent 34a128a79b
commit ef2a3ea082
26 changed files with 534 additions and 25 deletions
+18 -2
View File
@@ -2,7 +2,7 @@ import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.sessions import SessionMiddleware
@@ -24,12 +24,22 @@ async def lifespan(app: FastAPI):
scheduler_service.shutdown_scheduler()
app = FastAPI(title="습관 트래커", lifespan=lifespan)
app = FastAPI(title="해빗랩", lifespan=lifespan)
# 구글 OAuth 핸드셰이크 중 state/nonce를 임시로 저장하는 데만 쓰는 세션이다.
# 로그인 유지용 쿠키(habit_session)와는 별개.
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key, session_cookie="oauth_session", max_age=600)
@app.middleware("http")
async def redirect_http_to_https(request: Request, call_next):
# 리버스 프록시가 X-Forwarded-Proto로 원래 스킴을 알려줄 때만 동작한다
# (로컬 uvicorn 직접 실행 시에는 이 헤더가 없어 그냥 통과).
if request.headers.get("x-forwarded-proto") == "http":
return RedirectResponse(str(request.url.replace(scheme="https")), status_code=301)
return await call_next(request)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(auth.router)
@@ -75,3 +85,9 @@ async def unhandled_exception_handler(request: Request, exc: Exception):
def service_worker():
# 서비스워커 scope가 앱 전체를 커버하려면 /static/ 하위가 아닌 루트 경로로 서빙해야 한다.
return FileResponse("app/static/service-worker.js", media_type="application/javascript")
@app.get("/.well-known/assetlinks.json")
def assetlinks():
# Android TWA(kr.co.alphalok.habit.twa)의 도메인 소유권 증명용 — 반드시 /.well-known/ 루트 경로여야 한다.
return FileResponse("app/static/.well-known/assetlinks.json", media_type="application/json")