feat: implement comments and activity panel in workspace

- Added CommentsPanel component for managing comments on documents.
- Integrated comment creation, deletion, and resolution functionalities.
- Implemented activity tracking for document interactions.
- Enhanced SketchPage with editing and saving capabilities.
- Introduced template management API endpoints for creating, sharing, and deleting templates.
- Added runtime validation report for frontend and backend.
- Created GettingStartedPanel for user onboarding with template options.
- Implemented Playwright E2E tests for accessibility and export functionality.
This commit is contained in:
Anurag Jangid 2026-10-22 21:18:10 +05:30
parent 6e354ba041
commit 809235e962
41 changed files with 2840 additions and 285 deletions

View file

@ -1,4 +1,9 @@
from datetime import timedelta
from urllib.parse import urlencode
import secrets
import httpx
from fastapi.responses import RedirectResponse
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
@ -20,6 +25,7 @@ from app.schemas.user import (
UserCreate,
UserResponse,
UserUpdate,
ChangePasswordRequest,
)
from app.services.user_service import (
authenticate_user,
@ -30,7 +36,14 @@ from app.services.user_service import (
get_valid_reset_token,
update_user,
)
from app.utils.security import create_access_token, create_refresh_token, hash_password
from app.utils.security import (
create_access_token,
create_oauth_state,
create_refresh_token,
hash_password,
verify_oauth_state,
verify_password,
)
router = APIRouter(prefix="/auth", tags=["auth"])
@ -114,3 +127,90 @@ def reset_password(data: ResetPasswordRequest, db: Session = Depends(get_db)) ->
db.commit()
return MessageResponse(message="Password reset successfully. You can now log in.")
@router.post("/change-password", response_model=MessageResponse)
def change_password(
data: ChangePasswordRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> MessageResponse:
if not verify_password(data.current_password, current_user.hashed_password):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Current password is incorrect")
current_user.hashed_password = hash_password(data.new_password)
db.commit()
return MessageResponse(message="Password updated successfully.")
@router.get("/google/login")
def google_login() -> RedirectResponse:
if not settings.google_client_id or not settings.google_client_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Google sign-in is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
)
state = create_oauth_state()
query = urlencode({
"client_id": settings.google_client_id,
"redirect_uri": settings.google_redirect_uri,
"response_type": "code",
"scope": "openid email profile",
"state": state,
"access_type": "offline",
"prompt": "select_account",
})
return RedirectResponse(f"https://accounts.google.com/o/oauth2/v2/auth?{query}")
@router.get("/google/callback")
async def google_callback(code: str, state: str | None = None) -> RedirectResponse:
if not settings.google_client_id or not settings.google_client_secret:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Google sign-in is not configured")
if not code or not state or not verify_oauth_state(state):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Google authorization was incomplete")
async with httpx.AsyncClient(timeout=10) as client:
token_response = await client.post(
"https://oauth2.googleapis.com/token",
data={
"code": code,
"client_id": settings.google_client_id,
"client_secret": settings.google_client_secret,
"redirect_uri": settings.google_redirect_uri,
"grant_type": "authorization_code",
},
)
if token_response.status_code != 200:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Google authorization failed")
token_data = token_response.json()
profile_response = await client.get(
"https://openidconnect.googleapis.com/v1/userinfo",
headers={"Authorization": f"Bearer {token_data['access_token']}"},
)
profile = profile_response.json()
email = str(profile.get("email", "")).lower()
if not email or not profile.get("email_verified"):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Google account email is not verified")
from app.services.user_service import get_user_by_email
db = next(get_db())
try:
user = get_user_by_email(db, email)
if user is None:
base_username = email.split("@")[0][:90]
username = base_username
suffix = 1
while get_user_by_username(db, username):
username = f"{base_username}{suffix}"
suffix += 1
user = create_user(db, UserCreate(
email=email,
username=username,
password=secrets.token_urlsafe(32),
full_name=str(profile.get("name") or username),
))
access_token = create_access_token(user.email)
refresh_token = create_refresh_token(user.email)
finally:
db.close()
query = urlencode({"access_token": access_token, "refresh_token": refresh_token})
return RedirectResponse(f"{settings.frontend_url}/auth/google/callback?{query}")