Initial commit: habit tracker PWA with Google OAuth, push notifications

FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine
server-rendered frontend. Multi-user via Google OAuth, daily habit
tracking, monthly/weekly history views, Web Push reminders via
APScheduler, and PWA support (manifest, service worker, offline caching).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 18:06:17 +09:00
co-authored by Claude Sonnet 5
commit cee589bb3e
80 changed files with 4695 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
from authlib.integrations.starlette_client import OAuth
from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
from app.models.user import User
from app.security import SESSION_MAX_AGE_SECONDS, create_session_token
router = APIRouter(prefix="/auth", tags=["auth"])
oauth = OAuth()
oauth.register(
name="google",
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_id=settings.google_client_id,
client_secret=settings.google_client_secret,
client_kwargs={"scope": "openid email profile"},
)
@router.get("/google/login")
async def google_login(request: Request):
return await oauth.google.authorize_redirect(request, settings.google_redirect_uri)
@router.get("/google/callback")
async def google_callback(request: Request, db: Session = Depends(get_db)):
token = await oauth.google.authorize_access_token(request)
userinfo = token["userinfo"]
google_sub = userinfo["sub"]
email = userinfo["email"]
name = userinfo.get("name")
picture_url = userinfo.get("picture")
user = db.scalar(select(User).where(User.google_sub == google_sub))
if user is None:
user = User(google_sub=google_sub, email=email, name=name, picture_url=picture_url)
db.add(user)
else:
user.email = email
user.name = name
user.picture_url = picture_url
db.commit()
db.refresh(user)
session_token = create_session_token(user.id)
response = RedirectResponse(url="/today", status_code=303)
response.set_cookie(
settings.session_cookie_name,
session_token,
httponly=True,
samesite="lax",
max_age=SESSION_MAX_AGE_SECONDS,
)
return response
@router.post("/logout")
def logout():
response = RedirectResponse(url="/login", status_code=303)
response.delete_cookie(settings.session_cookie_name)
return response