This commit is contained in:
retoor 2026-06-05 19:02:30 +02:00
parent adb07bb40e
commit c3d37bd103
7 changed files with 31 additions and 9 deletions

View File

@ -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):

View File

@ -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

View File

@ -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}")

View File

@ -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)");

View File

@ -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}`);
}

View File

@ -20,6 +20,7 @@
{% endif %}
<form class="auth-form" method="POST" action="/auth/login">
<input type="hidden" name="next" value="{{ next_url or '' }}">
<div class="auth-field">
<label for="email">Email address</label>
<input type="email" id="email" name="email" value="{{ email or '' }}" required maxlength="255" placeholder="you@example.com">

View File

@ -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