Pre locust

This commit is contained in:
2026-05-11 00:41:41 +02:00
parent e340a8dc42
commit 120eb740aa
31 changed files with 598 additions and 473 deletions
+94 -1
View File
@@ -1,5 +1,6 @@
import secrets
import logging
from datetime import datetime
from datetime import datetime, timedelta
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from devplacepy.database import get_table
@@ -79,6 +80,14 @@ async def signup(request: Request):
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
badges.insert({
"uid": generate_uid(),
"user_uid": uid,
"badge_name": "Member",
"created_at": datetime.utcnow().isoformat(),
})
token = create_session(uid)
response = RedirectResponse(url="/feed", status_code=302)
response.set_cookie(key="session", value=token, max_age=86400 * 7, httponly=True)
@@ -117,6 +126,90 @@ async def login(request: Request):
return response
@router.get("/forgot-password", response_class=HTMLResponse)
async def forgot_password_page(request: Request):
return templates.TemplateResponse("forgot_password.html", {"request": request})
@router.post("/forgot-password")
async def forgot_password(request: Request):
form = await request.form()
email = form.get("email", "").strip().lower()
errors = []
if not email or "@" not in email:
errors.append("Valid email is required")
if errors:
return templates.TemplateResponse("forgot_password.html", {
"request": request, "errors": errors,
})
users = get_table("users")
user = users.find_one(email=email)
if user:
token = secrets.token_hex(32)
resets = get_table("password_resets")
resets.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"token": hash_password(token),
"expires_at": (datetime.utcnow() + timedelta(hours=1)).isoformat(),
"used": False,
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"Password reset requested for {email}: /auth/reset-password/{token}")
return templates.TemplateResponse("forgot_password.html", {
"request": request, "sent": True,
})
@router.get("/reset-password/{token}", response_class=HTMLResponse)
async def reset_password_page(request: Request, token: str):
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token,
})
@router.post("/reset-password/{token}")
async def reset_password(request: Request, token: str):
form = await request.form()
password = form.get("password", "")
confirm = form.get("confirm_password", "")
errors = []
if len(password) < 6:
errors.append("Password must be at least 6 characters")
if password != confirm:
errors.append("Passwords do not match")
if errors:
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token, "errors": errors,
})
resets = get_table("password_resets")
users = get_table("users")
all_resets = list(resets.find(used=False))
matched = None
for r in all_resets:
expires = datetime.fromisoformat(r["expires_at"])
if expires > datetime.utcnow() and verify_password(token, r["token"]):
matched = r
break
if not matched:
errors.append("Invalid or expired reset token")
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token, "errors": errors,
})
users.update({"uid": matched["user_uid"], "password_hash": hash_password(password)}, ["uid"])
resets.update({"id": matched["id"], "used": True}, ["id"])
logger.info(f"Password reset completed for user {matched['user_uid']}")
return RedirectResponse(url="/auth/login", status_code=302)
@router.get("/logout")
async def logout(request: Request):
token = request.cookies.get("session")
+10
View File
@@ -32,6 +32,16 @@ async def create_comment(request: Request):
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
existing = badges.find_one(user_uid=user["uid"], badge_name="First Comment")
if not existing:
badges.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"badge_name": "First Comment",
"created_at": datetime.utcnow().isoformat(),
})
posts = get_table("posts")
post = posts.find_one(uid=post_uid)
if post and post["user_uid"] != user["uid"]:
+7 -2
View File
@@ -1,7 +1,8 @@
import logging
from datetime import datetime, timedelta
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from devplacepy.database import get_table
from devplacepy.database import get_table, get_daily_topic
from devplacepy.templating import templates
from devplacepy.utils import get_current_user, require_user, time_ago
@@ -59,9 +60,12 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None):
posts = get_feed_posts(user, tab, topic)
users_table = get_table("users")
total_members = len(list(users_table.all()))
posts_today = len(list(get_table("posts").find()))
posts_table = get_table("posts")
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
posts_today = len(list(posts_table.find(created_at={">=": today_start})))
total_projects = len(list(get_table("projects").all()))
top_authors = list(users_table.find(order_by=["-stars"], _limit=5))
daily_topic = get_daily_topic()
return templates.TemplateResponse("feed.html", {
"request": request,
@@ -73,4 +77,5 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None):
"posts_today": posts_today,
"total_projects": total_projects,
"top_authors": top_authors,
"daily_topic": daily_topic,
})
+61
View File
@@ -0,0 +1,61 @@
import logging
from datetime import datetime
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse
from devplacepy.database import get_table
from devplacepy.utils import generate_uid, require_user
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/{username}")
async def follow_user(request: Request, username: str):
user = require_user(request)
users = get_table("users")
target = users.find_one(username=username)
if not target or target["uid"] == user["uid"]:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows = get_table("follows")
existing = follows.find_one(follower_uid=user["uid"], following_uid=target["uid"])
if existing:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows.insert({
"uid": generate_uid(),
"follower_uid": user["uid"],
"following_uid": target["uid"],
"created_at": datetime.utcnow().isoformat(),
})
notifications = get_table("notifications")
notifications.insert({
"uid": generate_uid(),
"user_uid": target["uid"],
"type": "follow",
"message": f"{user['username']} started following you",
"related_uid": user["uid"],
"read": False,
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"{user['username']} followed {username}")
return RedirectResponse(url=f"/profile/{username}", status_code=302)
@router.post("/unfollow/{username}")
async def unfollow_user(request: Request, username: str):
user = require_user(request)
users = get_table("users")
target = users.find_one(username=username)
if not target:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows = get_table("follows")
existing = follows.find_one(follower_uid=user["uid"], following_uid=target["uid"])
if existing:
follows.delete(id=existing["id"])
logger.info(f"{user['username']} unfollowed {username}")
return RedirectResponse(url=f"/profile/{username}", status_code=302)
+32 -2
View File
@@ -1,9 +1,13 @@
import os
import logging
import aiofiles
from datetime import datetime
from fastapi import APIRouter, Request, HTTPException, status
from pathlib import Path
from fastapi import APIRouter, Request, HTTPException, status, UploadFile, Form
from fastapi.responses import RedirectResponse, HTMLResponse
from devplacepy.database import get_table
from devplacepy.templating import templates
from devplacepy.config import STATIC_DIR
from devplacepy.utils import generate_uid, get_current_user, require_user, time_ago
logger = logging.getLogger(__name__)
@@ -30,6 +34,22 @@ async def create_post(request: Request):
if errors:
return RedirectResponse(url="/feed", status_code=302)
image_filename = None
image_file = form.get("image")
if image_file and hasattr(image_file, "filename") and image_file.filename:
try:
upload_dir = STATIC_DIR / "uploads"
upload_dir.mkdir(parents=True, exist_ok=True)
ext = Path(image_file.filename).suffix or ".png"
image_filename = f"{generate_uid()}{ext}"
file_path = upload_dir / image_filename
content_bytes = await image_file.read()
async with aiofiles.open(str(file_path), "wb") as f:
await f.write(content_bytes)
logger.info(f"Image saved: {image_filename}")
except Exception as e:
logger.warning(f"Image upload failed: {e}")
posts = get_table("posts")
uid = generate_uid()
posts.insert({
@@ -39,11 +59,21 @@ async def create_post(request: Request):
"content": content,
"topic": topic,
"project_uid": project_uid or None,
"image": None,
"image": image_filename,
"stars": 0,
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
existing = badges.find_one(user_uid=user["uid"], badge_name="First Post")
if not existing:
badges.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"badge_name": "First Post",
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"Post {uid} created by {user['username']}")
return RedirectResponse(url=f"/posts/{uid}", status_code=302)
+19
View File
@@ -32,6 +32,22 @@ async def profile_page(request: Request, username: str, tab: str = "posts"):
badges = list(get_table("badges").find(user_uid=profile_user["uid"]))
projects = list(get_table("projects").find(user_uid=profile_user["uid"]))
posts_count = len(list(get_table("posts").find(user_uid=profile_user["uid"])))
activities = []
if tab == "activity":
posts_table = get_table("posts")
for p in posts_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
activities.append({"type": "post", "content": p.get("title") or p["content"][:80], "time_ago": time_ago(p["created_at"]), "uid": p["uid"]})
comments_table = get_table("comments")
for c in comments_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
activities.append({"type": "comment", "content": c["content"][:80], "time_ago": time_ago(c["created_at"]), "uid": c["post_uid"]})
activities.sort(key=lambda a: a["time_ago"], reverse=True)
is_following = False
if current_user:
follows = get_table("follows")
is_following = bool(follows.find_one(follower_uid=current_user["uid"], following_uid=profile_user["uid"]))
return templates.TemplateResponse("profile.html", {
"request": request,
@@ -41,6 +57,9 @@ async def profile_page(request: Request, username: str, tab: str = "posts"):
"badges": badges,
"projects": projects,
"current_tab": tab,
"posts_count": posts_count,
"is_following": is_following,
"activities": activities,
})
+6 -2
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None):
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None):
projects = get_table("projects")
users = get_table("users")
all_projects = list(projects.all())
@@ -18,6 +18,9 @@ def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = Non
if user_uid:
all_projects = [p for p in all_projects if p["user_uid"] == user_uid]
if project_type:
all_projects = [p for p in all_projects if p.get("project_type") == project_type]
if search:
search_lower = search.lower()
all_projects = [
@@ -48,9 +51,10 @@ async def projects_page(
tab: str = "recent",
search: str = "",
user_uid: str = None,
project_type: str = None,
):
user = get_current_user(request)
projects = get_projects_list(tab, search, user_uid)
projects = get_projects_list(tab, search, user_uid, project_type)
users = get_table("users")
total_members = len(list(users.all()))
+3
View File
@@ -41,6 +41,9 @@ async def vote(request: Request, target_type: str, target_uid: str):
if target_type == "post":
posts = get_table("posts")
posts.update({"uid": target_uid, "stars": up - down}, ["uid"])
elif target_type == "project":
projects = get_table("projects")
projects.update({"uid": target_uid, "stars": up - down}, ["uid"])
referer = request.headers.get("Referer", "/feed")
return RedirectResponse(url=referer, status_code=302)