Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .v1.auth import router as auth_router
from .v1.health import router as health_router
from .v1.integrations import router as integrations_router
from .v1.users import router as users_router

api_router = APIRouter()

Expand All @@ -23,4 +24,10 @@
tags=["Integrations"]
)

api_router.include_router(
users_router,
prefix="/v1/users",
tags=["Users"]
)

__all__ = ["api_router"]
139 changes: 139 additions & 0 deletions backend/app/api/v1/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""User profile API endpoints."""
from fastapi import APIRouter, HTTPException, Depends, status
from uuid import UUID
from pydantic import BaseModel
from typing import Optional, List
from app.core.dependencies import get_current_user
from app.services.auth.management import get_user_by_id, update_user_profile
import logging

logger = logging.getLogger(__name__)
router = APIRouter()


class UserProfileResponse(BaseModel):
"""Response model for user profile."""
id: str
email: Optional[str] = None
display_name: str
avatar_url: Optional[str] = None
bio: Optional[str] = None
location: Optional[str] = None
github_username: Optional[str] = None
discord_username: Optional[str] = None
slack_username: Optional[str] = None
preferred_languages: Optional[List[str]] = None


class UserProfileUpdateRequest(BaseModel):
"""Request model for updating user profile."""
display_name: Optional[str] = None
bio: Optional[str] = None
location: Optional[str] = None
avatar_url: Optional[str] = None
preferred_languages: Optional[List[str]] = None


@router.get("/me", response_model=UserProfileResponse)
async def get_current_user_profile(user_id: UUID = Depends(get_current_user)):
"""Get the current user's profile information."""
try:
user = await get_user_by_id(str(user_id))

if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User profile not found"
)

return UserProfileResponse(
id=str(user.id),
email=user.email,
display_name=user.display_name,
avatar_url=user.avatar_url,
bio=user.bio,
location=user.location,
github_username=user.github_username,
discord_username=user.discord_username,
slack_username=user.slack_username,
preferred_languages=user.preferred_languages
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching user profile: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to fetch user profile"
) from e


@router.put("/me", response_model=UserProfileResponse)
async def update_current_user_profile(
request: UserProfileUpdateRequest,
user_id: UUID = Depends(get_current_user)
):
"""Update the current user's profile information."""
try:
# Build update dict from request, excluding None values
updates = {}
if request.display_name is not None:
updates["display_name"] = request.display_name
if request.bio is not None:
updates["bio"] = request.bio
if request.location is not None:
updates["location"] = request.location
if request.avatar_url is not None:
updates["avatar_url"] = request.avatar_url
if request.preferred_languages is not None:
updates["preferred_languages"] = request.preferred_languages

if not updates:
# No updates provided, just return current profile
user = await get_user_by_id(str(user_id))
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User profile not found"
)
return UserProfileResponse(
id=str(user.id),
email=user.email,
display_name=user.display_name,
avatar_url=user.avatar_url,
bio=user.bio,
location=user.location,
github_username=user.github_username,
discord_username=user.discord_username,
slack_username=user.slack_username,
preferred_languages=user.preferred_languages
)

updated_user = await update_user_profile(str(user_id), **updates)

if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Failed to update user profile"
)

return UserProfileResponse(
id=str(updated_user.id),
email=updated_user.email,
display_name=updated_user.display_name,
avatar_url=updated_user.avatar_url,
bio=updated_user.bio,
location=updated_user.location,
github_username=updated_user.github_username,
discord_username=updated_user.discord_username,
slack_username=updated_user.slack_username,
preferred_languages=updated_user.preferred_languages
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating user profile: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user profile"
) from e
97 changes: 97 additions & 0 deletions backend/database/02_create_users_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
-- Create users table to store extended user profile information
CREATE TABLE IF NOT EXISTS public.users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Contact & Identity
email TEXT,

-- Platform Integrations
discord_id TEXT UNIQUE,
discord_username TEXT,
github_id TEXT UNIQUE,
github_username TEXT,
slack_id TEXT UNIQUE,
slack_username TEXT,

-- Profile Information
display_name TEXT NOT NULL DEFAULT 'User',
avatar_url TEXT,
bio TEXT,
location TEXT,

-- Verification
is_verified BOOLEAN NOT NULL DEFAULT false,
verification_token TEXT,
verification_token_expires_at TIMESTAMPTZ,
verified_at TIMESTAMPTZ,

-- Stats & Metadata
skills JSONB DEFAULT '{}',
github_stats JSONB DEFAULT '{}',
last_active_discord TIMESTAMPTZ,
last_active_github TIMESTAMPTZ,
last_active_slack TIMESTAMPTZ,
total_interactions_count INTEGER NOT NULL DEFAULT 0,
preferred_languages TEXT[] DEFAULT '{}'
);

-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_users_email ON public.users(email);
CREATE INDEX IF NOT EXISTS idx_users_discord_id ON public.users(discord_id);
CREATE INDEX IF NOT EXISTS idx_users_github_id ON public.users(github_id);
CREATE INDEX IF NOT EXISTS idx_users_slack_id ON public.users(slack_id);
CREATE INDEX IF NOT EXISTS idx_users_github_username ON public.users(github_username);

-- Create trigger to automatically update updated_at
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Enable Row Level Security (RLS)
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;

-- Create RLS policies for users table
-- Users can view their own profile
CREATE POLICY "Users can view their own profile"
ON public.users
FOR SELECT
USING (auth.uid() = id);

-- Users can update their own profile
CREATE POLICY "Users can update their own profile"
ON public.users
FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);

-- Function to handle new user creation from auth.users
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (id, email, display_name, avatar_url)
VALUES (
NEW.id,
NEW.email,
COALESCE(NEW.raw_user_meta_data->>'display_name', split_part(NEW.email, '@', 1), 'User'),
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Trigger to create user profile when auth user is created
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();

-- Add helpful comments
COMMENT ON TABLE public.users IS 'Extended user profile information synced with auth.users';
COMMENT ON COLUMN public.users.display_name IS 'User display name from signup or profile update';
COMMENT ON COLUMN public.users.skills IS 'User skills and expertise areas';
COMMENT ON COLUMN public.users.github_stats IS 'Cached GitHub statistics';
COMMENT ON COLUMN public.users.preferred_languages IS 'Array of preferred programming languages';
15 changes: 14 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading