diff --git a/devplacepy/models.py b/devplacepy/models.py index dc79cce..9915630 100644 --- a/devplacepy/models.py +++ b/devplacepy/models.py @@ -34,6 +34,7 @@ class LoginForm(BaseModel): email: str = Field(min_length=1, max_length=255) password: str = Field(min_length=1, max_length=128) remember_me: str = "" + next: str = "" class ForgotPasswordForm(BaseModel): diff --git a/devplacepy/routers/auth.py b/devplacepy/routers/auth.py index df08772..c2a0b83 100644 --- a/devplacepy/routers/auth.py +++ b/devplacepy/routers/auth.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Request, Form from fastapi.responses import RedirectResponse, HTMLResponse from devplacepy.database import get_table from devplacepy.templating import templates -from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge, clear_session_cache +from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge, clear_session_cache, safe_next from devplacepy.seo import base_seo_context from devplacepy.models import SignupForm, LoginForm, ForgotPasswordForm, ResetPasswordForm @@ -30,17 +30,17 @@ async def signup_page(request: Request): @router.get("/login", response_class=HTMLResponse) -async def login_page(request: Request): +async def login_page(request: Request, next: str | None = None): user = get_current_user(request) if user: - return RedirectResponse(url="/feed", status_code=302) + return RedirectResponse(url=safe_next(next), status_code=302) seo_ctx = base_seo_context( request, title="Sign In", description="Sign in to DevPlace to connect with developers.", robots="noindex,nofollow", ) - return templates.TemplateResponse(request, "login.html", {**seo_ctx, "request": request}) + return templates.TemplateResponse(request, "login.html", {**seo_ctx, "request": request, "next_url": safe_next(next, "")}) @router.post("/signup") @@ -108,12 +108,12 @@ async def login(request: Request, data: Annotated[LoginForm, Form()]): seo_ctx = base_seo_context(request, title="Sign In", robots="noindex,nofollow") return templates.TemplateResponse( request, "login.html", - {**seo_ctx, "request": request, "errors": errors, "email": email}, + {**seo_ctx, "request": request, "errors": errors, "email": email, "next_url": safe_next(data.next, "")}, ) token = create_session(user["uid"]) max_age = 86400 * 30 if remember_me else 86400 * 7 - response = RedirectResponse(url="/feed", status_code=302) + response = RedirectResponse(url=safe_next(data.next), status_code=302) response.set_cookie(key="session", value=token, max_age=max_age, httponly=True, samesite="lax") logger.info(f"User {user['username']} logged in") return response diff --git a/devplacepy/routers/comments.py b/devplacepy/routers/comments.py index 3fddda3..3b24f1a 100644 --- a/devplacepy/routers/comments.py +++ b/devplacepy/routers/comments.py @@ -59,7 +59,7 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()]) create_mention_notifications(content, user["uid"], comment_url) logger.info(f"Comment by {user['username']} on {target_type} {target_uid}") - return RedirectResponse(url=redirect_url, status_code=302) + return RedirectResponse(url=comment_url, status_code=302) @router.post("/delete/{comment_uid}") diff --git a/devplacepy/static/js/CommentManager.js b/devplacepy/static/js/CommentManager.js index 4bb68d7..ec7f98f 100644 --- a/devplacepy/static/js/CommentManager.js +++ b/devplacepy/static/js/CommentManager.js @@ -1,3 +1,5 @@ +import { Http } from "./Http.js"; + export class CommentManager { constructor() { this.initCommentReply(); @@ -25,7 +27,10 @@ export class CommentManager { } const template = document.getElementById("comment-reply-template"); - if (!template) return; + if (!template) { + Http.toLogin(); + return; + } const container = comment.closest(".post-card, .comments-section"); const source = container && container.querySelector(".comment-form:not(.comment-reply-form)"); diff --git a/devplacepy/static/js/Http.js b/devplacepy/static/js/Http.js index d0d005f..92ce966 100644 --- a/devplacepy/static/js/Http.js +++ b/devplacepy/static/js/Http.js @@ -1,4 +1,9 @@ export class Http { + static toLogin() { + const next = encodeURIComponent(location.pathname + location.search + location.hash); + window.location.href = `/auth/login?next=${next}`; + } + static async getJson(url) { const response = await fetch(url); return response.json(); @@ -13,6 +18,10 @@ export class Http { }, body: new URLSearchParams(params), }); + if (response.redirected && response.url.includes("/auth/login")) { + Http.toLogin(); + return new Promise(() => {}); + } if (!response.ok) { throw new Error(`request failed with status ${response.status}`); } diff --git a/devplacepy/templates/login.html b/devplacepy/templates/login.html index 0a56d17..d6f816b 100644 --- a/devplacepy/templates/login.html +++ b/devplacepy/templates/login.html @@ -20,6 +20,7 @@ {% endif %}
+
diff --git a/devplacepy/utils.py b/devplacepy/utils.py index cbea56b..0d4e1f9 100644 --- a/devplacepy/utils.py +++ b/devplacepy/utils.py @@ -77,10 +77,16 @@ def get_current_user(request: Request): return user +def safe_next(value, default="/feed"): + if value and value.startswith("/") and not value.startswith("//"): + return value + return default + + def require_user(request: Request): user = get_current_user(request) if not user: - raise HTTPException(status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/"}) + raise HTTPException(status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/auth/login"}) return user