Skip to content

Conversation

@PinJinx
Copy link
Contributor

@PinJinx PinJinx commented Jan 16, 2026

Closes #175

📝 Description

This PR implements a comprehensive User Profile management system and robust database synchronization triggers.

It introduces a full-featured frontend Profile Page allowing users to view and edit their details (bio, skills, avatars) and see their social connection status. On the backend, it adds logic to handle profile updates and intelligently merge duplicate user accounts if a Discord ID links two different rows.

Additionally, this PR includes essential Database Triggers that automatically sync Supabase Auth users to our public users table upon registration and email verification, ensuring our user data is always consistent.

🔧 Changes Made

Frontend

  • New Profile Page (ProfilePage.tsx):
    • View/Edit mode toggle: Seamless switching between viewing and editing profile details.
    • Avatar Management: Added functionality to upload, replace, or remove profile pictures via Supabase Storage.
    • Skill Tags: Created a robust UI for adding "Languages" and "Frameworks" as dismissible tags. (This was implemented just because I saw how skills were stored as a dictionary in the backend and thought something like this might be what you were going for; I can change it if this is the wrong idea).
    • Social Status: Visual indicators showing if GitHub, Discord, or Slack are "Connected" or allowing manual entry in edit mode. (For now, it just asks to directly give the ID for each platform; this can be changed later where platform APIs are used to login).
  • New Component: SkillSection.tsx for reusable tag management.

Backend

  • Added get_user_details: Implemented logic to detect "Ghost" accounts (users with the same Discord ID but different User UUIDs) and merge their data dynamically so users don't lose context.
  • Added all the needed services for get_user_details and the edit endpoint to work.
  • New /edit Endpoint: Added an endpoint allowing updates to the user profile.
  • Pydantic Models: Added UserProfileEdit.

Database

  • Auth Sync Triggers:
    • add_user_to_table: Automatically inserts a row into public.users when a user signs up via Supabase Auth.
    • verify_user_on_registration: Updates is_verified status in public.users when the email is confirmed.

📷 Screenshots or Visual Changes (if applicable)

image image

🤝 Collaboration

nil

✅ Checklist

  • I have read the contributing guidelines.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added necessary documentation (if applicable).
  • Any dependent changes have been merged and published in downstream modules.

Summary by CodeRabbit

  • New Features
    • Added user profile management system with full editing capabilities.
    • Implemented avatar upload and removal functionality with real-time progress feedback.
    • Added skills management section for languages and frameworks.
    • Integrated social media field management with connection status indicators.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

This PR implements a complete user profile management system, connecting the frontend with backend APIs and replacing mock data. It adds backend endpoints for fetching and editing user profiles with token-based authentication, database triggers for automatic user onboarding, data models for profile validation, and a dynamic frontend UI with file uploads and skill management.

Changes

Cohort / File(s) Summary
Backend API Router
backend/app/api/router.py
Registers new profile router under /v1/profile endpoint with "Profile" tag
Backend Profile Endpoints
backend/app/api/v1/profile.py
Implements two new endpoints: GET / (fetch user profile with token validation) and PATCH /edit (update profile fields), with error handling and structured responses
Backend Database Services
backend/app/database/supabase/services.py
Adds three new public functions: validate_user() for token authentication, get_user_details() to fetch user data with Discord ID merging, and update_user_details() to persist profile changes
Backend Data Models
backend/app/models/database/supabase.py
Introduces SkillsModel and UserProfileEdit Pydantic models for request/response validation with optional fields
Database Schema & Triggers
backend/database/01_create_integration_tables.sql
Adds trigger functions for automatic user synchronization between auth and public tables on registration/verification, and implements Row Level Security policies for organization integrations
Frontend API Utilities
frontend/src/lib/api.ts
Exports API_BASE_URL constant for frontend API consumption
Frontend Profile UI
frontend/src/components/pages/ProfilePage.tsx
Replaces mock data with dynamic profile rendering: fetches real user data via API, implements edit mode with form validation, handles avatar uploads to Supabase storage, and manages loading/saving states
Frontend Skill Component
frontend/src/components/ui/skills.tsx
Adds new reusable SkillSection component for rendering and managing skill lists (languages/frameworks) with add/remove functionality in edit mode

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant API as Backend API
    participant Auth as Supabase Auth
    participant DB as Database

    User->>Frontend: Open Profile Page
    activate Frontend
    Frontend->>Frontend: Extract Supabase session token
    Frontend->>API: GET /v1/profile with Bearer token
    activate API
    API->>Auth: Validate token
    Auth-->>API: User ID & metadata
    API->>DB: SELECT user details by ID
    DB-->>API: User record + Discord data
    API->>API: Merge fields, normalize response
    API-->>Frontend: User profile (id, email, display_name, avatar_url, bio, skills, socials)
    deactivate API
    Frontend->>Frontend: Render profile with data
    deactivate Frontend
    User->>Frontend: View Profile
Loading
sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant API as Backend API
    participant Auth as Supabase Auth
    participant DB as Database
    participant Storage as Supabase Storage

    User->>Frontend: Click Edit Profile
    Frontend->>Frontend: Enable edit mode
    User->>Frontend: Select avatar file
    Frontend->>Storage: Upload avatar to /avatars bucket
    activate Storage
    Storage-->>Frontend: Public avatar URL
    deactivate Storage
    Frontend->>Frontend: Update avatar_url in form state
    User->>Frontend: Modify profile fields & click Save
    activate Frontend
    Frontend->>Frontend: Validate session & build update payload
    Frontend->>API: PATCH /v1/profile/edit with UserProfileEdit payload
    activate API
    API->>Auth: Validate bearer token
    Auth-->>API: User ID
    API->>DB: UPDATE public.users with mapped fields
    DB-->>API: Updated user record
    API-->>Frontend: Updated profile response
    deactivate API
    Frontend->>Frontend: Exit edit mode, show success toast
    deactivate Frontend
    User->>Frontend: View updated profile
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • chandansgowda
  • smokeyScraper

Poem

🐰 A profile journey, now complete,
Backend and frontend now sweetly meet!
With tokens and triggers, the data flows free,
User details saved—just watch and see! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: profile editing, avatar upload, and user merging logic are all implemented across the PR.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #175 objectives: replaces mock data with backend API calls, implements proper authentication flow, adds loading/error states, maintains TypeScript type safety, and consolidates Supabase interactions to the backend.
Out of Scope Changes check ✅ Passed All changes are directly aligned with PR objectives. Database triggers for user sync, profile service functions, API endpoints, frontend components, and avatar upload are all in-scope implementations addressing the linked issue requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@PinJinx PinJinx changed the title feat: Implement profile edit, avatar upload, and user merging logic feat: Implement of profile page and necessary frontend and backend services Jan 16, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@backend/app/api/v1/profile.py`:
- Around line 15-25: In get_user, remove the clear-text token logging and stop
concatenating the token into the HTTPException detail: delete the
print(f"Extracted token: {token}") and change the error detail to a generic
message (e.g., "Invalid or expired token") when validate_user(token) returns
falsy; if you need traceability, log only a truncated or hashed identifier
derived from token (never the raw token) or include context without the token
value, and keep all changes within the get_user function that uses security,
token, and validate_user.
- Around line 37-47: The returned "skills" field currently defaults to an empty
list which breaks the expected SkillsModel shape; update the response
construction in the profile endpoint(s) that build the user dict to ensure
"skills" is an object with the keys languages and frameworks (e.g.,
{"languages": [], "frameworks": []}) and, if user_details.get("skills") is
present but in a different shape, map/normalize it into that structure before
returning; apply this same normalization logic to both places that build the
user response (the two return blocks in profile.py that populate "skills").
- Around line 29-57: The fetch_details handler currently masks HTTPException and
will blow up if get_user_details returns None; after calling
get_user_details(id) in fetch_details, check if the result is None and raise an
HTTPException (e.g., 404 "User not found"); wrap the try/except so that you
catch HTTPException first and re-raise it unchanged (except HTTPException as he:
raise he), and keep a separate broad except Exception as e to log via
logger.error(...) and convert other errors into the 503 response as before;
reference fetch_details, get_user_details, HTTPException, and logger when making
these changes.

In `@backend/app/database/supabase/services.py`:
- Around line 205-207: Remove the sensitive debug print that can leak PII:
delete the line print(f"Auth response: {response}") in the logic that calls
supabase.auth.get_user(jwt=token) (the variable response and the call to
supabase.auth.get_user(jwt=token) identify the spot), and if logging is required
replace it with a non-PII message (e.g., log that auth lookup succeeded/failed)
or use a privacy-safe logger that omits user metadata before checking
response.user.
- Around line 136-140: The merge loop treats any falsy merged_user value as
"missing" (using if not merged_user.get(key)), which will overwrite valid falsy
values like False or 0; update the check in the block iterating
secondary_user.items() so you only consider a field missing when it is actually
None or an empty container/string (e.g., merged_user.get(key) is None or
merged_user.get(key) == '' or (isinstance(merged_user.get(key), list) and
len(merged_user.get(key)) == 0)), and only then assign merged_user[key] = value
if the secondary value is not None/empty; adjust the condition around
merged_user, secondary_user, and key accordingly to preserve legitimate falsy
values such as False and 0.

In `@backend/app/models/database/supabase.py`:
- Around line 243-250: The example in Config.json_schema_extra does not match
the SkillsModel shape; update the "skills" entry to be an object with
"languages" and "frameworks" arrays (e.g. "skills": {"languages": ["python"],
"frameworks": ["fastapi", "react"]}) so it matches SkillsModel; keep the
existing top-level keys (display_name, bio, github) and adjust the "skills"
structure accordingly in the Config class to reflect the nested schema used by
SkillsModel.

In `@backend/database/01_create_integration_tables.sql`:
- Around line 46-63: The INSERT into public.users sets is_verified based on
NEW.email_confirmed_at but leaves verified_at NULL; update the VALUES clause in
the INSERT (the public.users insertion) to populate verified_at as
NEW.email_confirmed_at when present (otherwise NULL) so that verified_at and
is_verified remain consistent; locate the INSERT statement referencing NEW.id,
NEW.email, NEW.raw_user_meta_data, NEW.created_at and NEW.email_confirmed_at and
add the verified_at value accordingly.
- Around line 40-44: The SECURITY DEFINER functions create_users_at_db and
verify_users_at_db need a fixed search_path to prevent privilege escalation;
update each CREATE OR REPLACE FUNCTION declaration to include a SET search_path
clause (e.g., SET search_path = pg_catalog, public or a specific schema list
appropriate for this DB) so the function runs with a locked search_path rather
than inheriting the caller's. Locate the CREATE OR REPLACE FUNCTION lines for
create_users_at_db and verify_users_at_db and add the SET search_path = ...
clause immediately in the function declaration (keeping LANGUAGE plpgsql and
SECURITY DEFINER).
- Around line 85-90: The UPDATE in the trigger uses the wrong NEW field: change
the assignment setting verified_at = NEW.confirmed_at to use the correct column
verified_at = NEW.email_confirmed_at so the trigger writes the actual email
confirmation timestamp; update the UPDATE statement that targets public.users
(the block that sets is_verified, verified_at, updated_at for WHERE id = NEW.id)
to reference NEW.email_confirmed_at and keep the rest of the logic unchanged.

In `@frontend/src/components/pages/ProfilePage.tsx`:
- Around line 112-142: handleAvatarUpload currently uploads a new avatar but
never deletes the previous file, leaving orphaned files; update
handleAvatarUpload to detect the previous avatar path from profile.avatar_url
(parse the storage object path or filename from the publicUrl), then after a
successful new upload call
supabase.storage.from('avatars').remove([oldFilePath]) (handle and log errors
but do not block the user flow), and finally update profile.avatar_url to the
new publicUrl; ensure you reference the existing symbols: handleAvatarUpload,
profile.avatar_url, supabase.storage.from('avatars').upload and
supabase.storage.from('avatars').remove, and keep setUploadingAvatar and toast
behavior unchanged.
- Around line 156-163: The early return inside handleSave after checking session
leaves isSaving true; to fix, either perform the session validation before
calling setIsSaving(true) or ensure you reset isSaving before returning (e.g.,
call setIsSaving(false) right before the toast/error return) so the finally
cleanup is not relied upon for that path; locate the handleSave function and
update the session check or add a setIsSaving(false) prior to the return so the
save button is never left disabled.
- Around line 62-63: The code returns early when supabase.auth.getSession()
yields sessionError or no session (checking sessionError and session) without
user feedback; update the ProfilePage logic to surface an error to the user
instead of silently returning by setting an error state or invoking the existing
UI alert/toast mechanism (e.g., setError/setAlert or showToast) with a message
derived from sessionError?.message or a fallback like "No active session; please
sign in", and then return; ensure the finally block still clears loading so the
UI removes the spinner while the error message is visible.
- Around line 145-148: The current handleRemoveAvatar only clears local state;
update it to also delete the file from Supabase Storage: inside
handleRemoveAvatar (or a helper it calls) derive the storage file path from
profile.avatar_url, call your Supabase client (e.g., supabase or
supabaseClient).storage.from(<your-bucket-name>).remove([filePath]), handle the
returned error/success, and then update setProfile(prev => ({...prev,
avatar_url: ''})) only after a successful delete (or still clear UI but surface
errors). Show a toast for success and for failure (include error.message) and
ensure you guard for empty/undefined avatar_url before attempting deletion.
🧹 Nitpick comments (9)
frontend/src/lib/api.ts (1)

8-9: Consider extending ApiClient instead of exporting API_BASE_URL for direct fetch calls.

While exporting API_BASE_URL works, ProfilePage.tsx now makes raw fetch() calls duplicating the auth header logic already handled by the ApiClient interceptor. Consider adding profile methods to ApiClient to maintain a single, consistent approach to API communication with built-in error handling and auth.

Example extension
// In ApiClient class
async getProfile(): Promise<UserProfile> {
    const response = await this.client.get('/v1/profile/');
    return response.data;
}

async updateProfile(data: ProfileUpdateRequest): Promise<void> {
    await this.client.patch('/v1/profile/edit', data);
}
frontend/src/components/ui/skills.tsx (3)

36-58: Use skill name as key instead of index for stable animations.

Using index as part of the key (${type}-${index}) can cause animation glitches when items are removed from the middle of the list. Since skills should be unique within each type, use the item value directly.

Suggested fix
-                <motion.span 
-                  key={`${type}-${index}`}
+                <motion.span 
+                  key={`${type}-${item}`}

49-56: Add accessible label to the remove button.

Screen readers won't understand the purpose of this button. Add an aria-label for accessibility.

Suggested fix
                   {isEditing && (
                     <button 
                       onClick={() => onRemove(type, item)}
                       className="ml-1.5 hover:text-red-400 transition-colors"
+                      aria-label={`Remove ${item}`}
                     >
                       <X size={12} />
                     </button>
                   )}

64-78: Consider making the Plus icon clickable or adding an aria-label to the input.

The Plus icon suggests interactivity but does nothing. Either make it trigger onAdd on click, or add an aria-label to the input explaining that Enter submits the value.

frontend/src/components/pages/ProfilePage.tsx (3)

118-121: Handle files without extensions gracefully.

If a file has no extension, split('.').pop() returns the filename itself (not undefined as one might expect), but files with names like "photo" will produce odd paths. Consider validating the file type or providing a fallback.

Suggested fix
      const file = event.target.files[0];
-     const fileExt = file.name.split('.').pop();
-     const fileName = `${Math.random()}.${fileExt}`;
+     const fileExt = file.name.includes('.') ? file.name.split('.').pop() : 'jpg';
+     const fileName = `${crypto.randomUUID()}.${fileExt}`;

289-293: Avoid hardcoded external fallback image URL.

Using an external Unsplash URL introduces external dependency, potential CORS issues, and privacy implications (user's browser makes requests to third-party). Consider using a local placeholder or a generic avatar component.

Suggested approach
- src={profile.avatar_url || "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200"}
+ src={profile.avatar_url || "/default-avatar.svg"}

Add a simple SVG placeholder to your public assets.


59-109: Consider preserving original profile state for cancel functionality.

The current implementation doesn't store the original profile data, so clicking "Cancel" exits edit mode but retains any unsaved changes in the UI. Users might expect Cancel to revert their changes.

backend/app/models/database/supabase.py (1)

230-233: Avoid mutable list defaults in SkillsModel.

Class-level list defaults can be shared and will trip Ruff RUF012; use Field(default_factory=list) instead.

♻️ Proposed fix
 class SkillsModel(BaseModel):
-    languages: List[str] = []
-    frameworks: List[str] = []
+    languages: List[str] = Field(default_factory=list)
+    frameworks: List[str] = Field(default_factory=list)
backend/app/database/supabase/services.py (1)

197-199: Preserve traceback when re-raising update errors.

Use logger.exception and bare raise to keep the original stack trace.

♻️ Proposed fix
-    except Exception as e:
-        logger.error(f"Error updating user details for {user_id}: {e}")
-        raise e
+    except Exception:
+        logger.exception(f"Error updating user details for {user_id}")
+        raise
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa66519 and 133f5a3.

📒 Files selected for processing (8)
  • backend/app/api/router.py
  • backend/app/api/v1/profile.py
  • backend/app/database/supabase/services.py
  • backend/app/models/database/supabase.py
  • backend/database/01_create_integration_tables.sql
  • frontend/src/components/pages/ProfilePage.tsx
  • frontend/src/components/ui/skills.tsx
  • frontend/src/lib/api.ts
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/api/v1/profile.py (2)
backend/app/database/supabase/services.py (3)
  • get_user_details (83-154)
  • validate_user (202-226)
  • update_user_details (156-199)
backend/app/models/database/supabase.py (1)
  • UserProfileEdit (234-251)
frontend/src/components/pages/ProfilePage.tsx (3)
frontend/src/lib/supabaseClient.ts (1)
  • supabase (12-12)
frontend/src/lib/api.ts (1)
  • API_BASE_URL (8-9)
backend/app/models/database/supabase.py (1)
  • User (7-69)
🪛 Ruff (0.14.11)
backend/app/api/v1/profile.py

16-16: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


30-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


33-33: Unpacked variable name is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


35-35: Abstract raise to an inner function

(TRY301)


49-49: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


63-63: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


72-72: Abstract raise to an inner function

(TRY301)


76-79: Abstract raise to an inner function

(TRY301)


84-84: Abstract raise to an inner function

(TRY301)


100-100: Use raise without specifying exception name

Remove exception name

(TRY201)


102-102: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backend/app/database/supabase/services.py

150-150: Consider moving this statement to an else block

(TRY300)


195-195: Consider moving this statement to an else block

(TRY300)


198-198: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


199-199: Use raise without specifying exception name

Remove exception name

(TRY201)

backend/app/models/database/supabase.py

244-251: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🔇 Additional comments (3)
backend/database/01_create_integration_tables.sql (2)

71-77: Trigger wiring for user insert looks good.


96-104: Email-confirmation transition trigger is appropriately scoped.

backend/app/api/router.py (1)

5-5: Profile router registration looks good.

Nice, registering the new router under /v1/profile keeps the API surface organized.

Also applies to: 27-31

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +15 to +25
async def get_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
token = credentials.credentials
print(f"Extracted token: {token}")
result = await validate_user(token)
if not result:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"+token,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not log or echo access tokens.

Printing tokens and including them in error details leaks credentials.

🔒 Proposed fix
-    print(f"Extracted token: {token}")
     result = await validate_user(token)
     if not result:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid or expired token"+token,
+            detail="Invalid or expired token",
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
token = credentials.credentials
print(f"Extracted token: {token}")
result = await validate_user(token)
if not result:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"+token,
)
async def get_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
token = credentials.credentials
result = await validate_user(token)
if not result:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
)
🧰 Tools
🪛 Ruff (0.14.11)

16-16: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In `@backend/app/api/v1/profile.py` around lines 15 - 25, In get_user, remove the
clear-text token logging and stop concatenating the token into the HTTPException
detail: delete the print(f"Extracted token: {token}") and change the error
detail to a generic message (e.g., "Invalid or expired token") when
validate_user(token) returns falsy; if you need traceability, log only a
truncated or hashed identifier derived from token (never the raw token) or
include context without the token value, and keep all changes within the
get_user function that uses security, token, and validate_user.

Comment on lines +29 to +57
@router.get("/")
async def fetch_details(user=Depends(get_user),):
"""Fetch user profile details."""
try:
(id, name) = user
if not id:
raise HTTPException(status_code=401, detail="Unauthorized")
user_details = await get_user_details(id)
return {"user": {
"id": user_details.get("id"),
"email": user_details.get("email"),
"display_name": user_details.get("display_name"),
"avatar_url": user_details.get("avatar_url"),
"bio": user_details.get("bio"),
"discord":user_details.get("discord_username"),
"github":user_details.get("github_username"),
"slack":user_details.get("slack_username"),
"skills": user_details.get("skills") or [],
}}
except Exception as e:
logger.error(f"Fetching profile details failed: {e}")
raise HTTPException(
status_code=503,
detail={
"service": "frontend_services",
"status": "http exception",
"error": str(e)
}
) from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "profile.py" | head -20

Repository: AOSSIE-Org/Devr.AI

Length of output: 180


Handle missing user records and prevent HTTPExceptions from being masked as 503 errors.

HTTPException raised on line 32 (401 Unauthorized) will be caught by the outer except Exception handler and converted to a 503 error instead of being returned directly. Additionally, if get_user_details returns None, accessing .get() on it will raise AttributeError, also masked as 503. Add a None check after calling get_user_details and catch HTTPException separately to re-raise it unchanged.

🐛 Proposed fix
     try:
         (id, name) = user
         if not id:
             raise HTTPException(status_code=401, detail="Unauthorized")
         user_details = await get_user_details(id)
+        if not user_details:
+            raise HTTPException(status_code=404, detail="User not found")
         return {"user": {
             "id": user_details.get("id"),
             ...
         }}
+    except HTTPException as he:
+        raise he
     except Exception as e:
         logger.error(f"Fetching profile details failed: {e}")
         raise HTTPException(
             status_code=503,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get("/")
async def fetch_details(user=Depends(get_user),):
"""Fetch user profile details."""
try:
(id, name) = user
if not id:
raise HTTPException(status_code=401, detail="Unauthorized")
user_details = await get_user_details(id)
return {"user": {
"id": user_details.get("id"),
"email": user_details.get("email"),
"display_name": user_details.get("display_name"),
"avatar_url": user_details.get("avatar_url"),
"bio": user_details.get("bio"),
"discord":user_details.get("discord_username"),
"github":user_details.get("github_username"),
"slack":user_details.get("slack_username"),
"skills": user_details.get("skills") or [],
}}
except Exception as e:
logger.error(f"Fetching profile details failed: {e}")
raise HTTPException(
status_code=503,
detail={
"service": "frontend_services",
"status": "http exception",
"error": str(e)
}
) from e
`@router.get`("/")
async def fetch_details(user=Depends(get_user),):
"""Fetch user profile details."""
try:
(id, name) = user
if not id:
raise HTTPException(status_code=401, detail="Unauthorized")
user_details = await get_user_details(id)
if not user_details:
raise HTTPException(status_code=404, detail="User not found")
return {"user": {
"id": user_details.get("id"),
"email": user_details.get("email"),
"display_name": user_details.get("display_name"),
"avatar_url": user_details.get("avatar_url"),
"bio": user_details.get("bio"),
"discord":user_details.get("discord_username"),
"github":user_details.get("github_username"),
"slack":user_details.get("slack_username"),
"skills": user_details.get("skills") or [],
}}
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Fetching profile details failed: {e}")
raise HTTPException(
status_code=503,
detail={
"service": "frontend_services",
"status": "http exception",
"error": str(e)
}
) from e
🧰 Tools
🪛 Ruff (0.14.11)

30-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


33-33: Unpacked variable name is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


35-35: Abstract raise to an inner function

(TRY301)


49-49: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In `@backend/app/api/v1/profile.py` around lines 29 - 57, The fetch_details
handler currently masks HTTPException and will blow up if get_user_details
returns None; after calling get_user_details(id) in fetch_details, check if the
result is None and raise an HTTPException (e.g., 404 "User not found"); wrap the
try/except so that you catch HTTPException first and re-raise it unchanged
(except HTTPException as he: raise he), and keep a separate broad except
Exception as e to log via logger.error(...) and convert other errors into the
503 response as before; reference fetch_details, get_user_details,
HTTPException, and logger when making these changes.

Comment on lines +37 to +47
return {"user": {
"id": user_details.get("id"),
"email": user_details.get("email"),
"display_name": user_details.get("display_name"),
"avatar_url": user_details.get("avatar_url"),
"bio": user_details.get("bio"),
"discord":user_details.get("discord_username"),
"github":user_details.get("github_username"),
"slack":user_details.get("slack_username"),
"skills": user_details.get("skills") or [],
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n backend/app/api/v1/profile.py | head -100

Repository: AOSSIE-Org/Devr.AI

Length of output: 4031


🏁 Script executed:

find . -type f -name "*.py" | xargs grep -l "SkillsModel\|skills" | grep -E "models|schema" | head -20

Repository: AOSSIE-Org/Devr.AI

Length of output: 103


🏁 Script executed:

cat -n backend/app/models/database/supabase.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 11781


🏁 Script executed:

rg "skills.*or.*\[\]" backend/

Repository: AOSSIE-Org/Devr.AI

Length of output: 233


Return skills in the same shape as SkillsModel.

Defaulting to [] conflicts with the {languages, frameworks} structure defined in SkillsModel and will break typed clients.

🧩 Proposed fix
-            "skills": user_details.get("skills") or [],
+            "skills": user_details.get("skills") or {"languages": [], "frameworks": []},
-            "skills": updated_user.get("skills") or [],
+            "skills": updated_user.get("skills") or {"languages": [], "frameworks": []},

Also applies to: 87-97

🤖 Prompt for AI Agents
In `@backend/app/api/v1/profile.py` around lines 37 - 47, The returned "skills"
field currently defaults to an empty list which breaks the expected SkillsModel
shape; update the response construction in the profile endpoint(s) that build
the user dict to ensure "skills" is an object with the keys languages and
frameworks (e.g., {"languages": [], "frameworks": []}) and, if
user_details.get("skills") is present but in a different shape, map/normalize it
into that structure before returning; apply this same normalization logic to
both places that build the user response (the two return blocks in profile.py
that populate "skills").

Comment on lines +136 to +140
for key, value in secondary_user.items():
# If primary user doesn't have a value for this field (None or empty string/list)
# but the secondary user does, we adopt the secondary user's value.
if not merged_user.get(key) and value:
merged_user[key] = value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t treat falsy values as “missing” when merging duplicates.

if not merged_user.get(key) will overwrite valid falsy values (e.g., False, 0). This can corrupt merged profiles (e.g., is_verified=False becomes True).

🐛 Safer merge check
-                if not merged_user.get(key) and value:
-                    merged_user[key] = value
+                primary_value = merged_user.get(key)
+                if primary_value in (None, "", [], {}) and value not in (None, "", [], {}):
+                    merged_user[key] = value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for key, value in secondary_user.items():
# If primary user doesn't have a value for this field (None or empty string/list)
# but the secondary user does, we adopt the secondary user's value.
if not merged_user.get(key) and value:
merged_user[key] = value
for key, value in secondary_user.items():
# If primary user doesn't have a value for this field (None or empty string/list)
# but the secondary user does, we adopt the secondary user's value.
primary_value = merged_user.get(key)
if primary_value in (None, "", [], {}) and value not in (None, "", [], {}):
merged_user[key] = value
🤖 Prompt for AI Agents
In `@backend/app/database/supabase/services.py` around lines 136 - 140, The merge
loop treats any falsy merged_user value as "missing" (using if not
merged_user.get(key)), which will overwrite valid falsy values like False or 0;
update the check in the block iterating secondary_user.items() so you only
consider a field missing when it is actually None or an empty container/string
(e.g., merged_user.get(key) is None or merged_user.get(key) == '' or
(isinstance(merged_user.get(key), list) and len(merged_user.get(key)) == 0)),
and only then assign merged_user[key] = value if the secondary value is not
None/empty; adjust the condition around merged_user, secondary_user, and key
accordingly to preserve legitimate falsy values such as False and 0.

Comment on lines +205 to +207
response = await supabase.auth.get_user(jwt=token)
print(f"Auth response: {response}")
if not response or not response.user:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove auth response printing to avoid leaking PII.

print(f"Auth response: {response}") can dump user metadata into logs/console.

🔒 Proposed fix
-    print(f"Auth response: {response}")
+    # Avoid logging full auth payloads (PII/credentials).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response = await supabase.auth.get_user(jwt=token)
print(f"Auth response: {response}")
if not response or not response.user:
response = await supabase.auth.get_user(jwt=token)
# Avoid logging full auth payloads (PII/credentials).
if not response or not response.user:
🤖 Prompt for AI Agents
In `@backend/app/database/supabase/services.py` around lines 205 - 207, Remove the
sensitive debug print that can leak PII: delete the line print(f"Auth response:
{response}") in the logic that calls supabase.auth.get_user(jwt=token) (the
variable response and the call to supabase.auth.get_user(jwt=token) identify the
spot), and if logging is required replace it with a non-PII message (e.g., log
that auth lookup succeeded/failed) or use a privacy-safe logger that omits user
metadata before checking response.user.

Comment on lines +85 to +90
UPDATE public.users
SET
is_verified = TRUE,
verified_at = NEW.confirmed_at,
updated_at = NOW()
WHERE id = NEW.id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "01_create_integration_tables.sql"

Repository: AOSSIE-Org/Devr.AI

Length of output: 111


🏁 Script executed:

cat -n backend/database/01_create_integration_tables.sql

Repository: AOSSIE-Org/Devr.AI

Length of output: 4969


🏁 Script executed:

# Search for the public.users table definition to verify columns
rg -A 20 "CREATE TABLE.*public\.users|CREATE TABLE.*users" backend/database/

Repository: AOSSIE-Org/Devr.AI

Length of output: 44


🏁 Script executed:

# Search for users table definition in the entire codebase
find . -name "*.sql" | head -20

Repository: AOSSIE-Org/Devr.AI

Length of output: 223


🏁 Script executed:

cat -n backend/app/database/supabase/scripts/create_db.sql | head -150

Repository: AOSSIE-Org/Devr.AI

Length of output: 5610


Change NEW.confirmed_at to NEW.email_confirmed_at in the UPDATE statement.

The trigger condition correctly checks email_confirmed_at (lines 100-102), but the UPDATE statement uses the non-existent column NEW.confirmed_at, which will be NULL. This prevents verified_at from being set to the actual email confirmation timestamp.

Fix
     UPDATE public.users
     SET
         is_verified = TRUE,
-        verified_at = NEW.confirmed_at,
+        verified_at = NEW.email_confirmed_at,
         updated_at = NOW()
     WHERE id = NEW.id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UPDATE public.users
SET
is_verified = TRUE,
verified_at = NEW.confirmed_at,
updated_at = NOW()
WHERE id = NEW.id;
UPDATE public.users
SET
is_verified = TRUE,
verified_at = NEW.email_confirmed_at,
updated_at = NOW()
WHERE id = NEW.id;
🤖 Prompt for AI Agents
In `@backend/database/01_create_integration_tables.sql` around lines 85 - 90, The
UPDATE in the trigger uses the wrong NEW field: change the assignment setting
verified_at = NEW.confirmed_at to use the correct column verified_at =
NEW.email_confirmed_at so the trigger writes the actual email confirmation
timestamp; update the UPDATE statement that targets public.users (the block that
sets is_verified, verified_at, updated_at for WHERE id = NEW.id) to reference
NEW.email_confirmed_at and keep the rest of the logic unchanged.

Comment on lines +62 to +63
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Provide user feedback when session is missing.

Silently returning without feedback leaves users confused. The finally block will clear loading, but users won't understand why no profile data appeared.

Suggested fix
        const { data: { session }, error: sessionError } = await supabase.auth.getSession();
-       if (sessionError || !session) return;
+       if (sessionError || !session) {
+         toast.error('Please log in to view your profile');
+         return;
+       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session) return;
import { toast } from 'react-toastify'; // or appropriate toast library
// ... existing code ...
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session) {
toast.error('Please log in to view your profile');
return;
}
🤖 Prompt for AI Agents
In `@frontend/src/components/pages/ProfilePage.tsx` around lines 62 - 63, The code
returns early when supabase.auth.getSession() yields sessionError or no session
(checking sessionError and session) without user feedback; update the
ProfilePage logic to surface an error to the user instead of silently returning
by setting an error state or invoking the existing UI alert/toast mechanism
(e.g., setError/setAlert or showToast) with a message derived from
sessionError?.message or a fallback like "No active session; please sign in",
and then return; ensure the finally block still clears loading so the UI removes
the spinner while the error message is visible.

Comment on lines +112 to +142
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
if (!event.target.files || event.target.files.length === 0) {
return;
}

const file = event.target.files[0];
const fileExt = file.name.split('.').pop();
const fileName = `${Math.random()}.${fileExt}`;
const filePath = `${fileName}`;

setUploadingAvatar(true);
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file);

if (uploadError) {
throw uploadError;
}
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(filePath);
setProfile(prev => ({ ...prev, avatar_url: publicUrl }));
toast.success("Image uploaded! Click 'Save Changes' to apply.");

} catch (error: any) {
console.error('Error uploading avatar:', error);
toast.error('Error uploading image');
} finally {
setUploadingAvatar(false);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

New avatar uploads don't clean up old files.

When uploading a new avatar, the previous file (if any) remains in storage. Combined with the removal issue, this could accumulate orphaned files.

🤖 Prompt for AI Agents
In `@frontend/src/components/pages/ProfilePage.tsx` around lines 112 - 142,
handleAvatarUpload currently uploads a new avatar but never deletes the previous
file, leaving orphaned files; update handleAvatarUpload to detect the previous
avatar path from profile.avatar_url (parse the storage object path or filename
from the publicUrl), then after a successful new upload call
supabase.storage.from('avatars').remove([oldFilePath]) (handle and log errors
but do not block the user flow), and finally update profile.avatar_url to the
new publicUrl; ensure you reference the existing symbols: handleAvatarUpload,
profile.avatar_url, supabase.storage.from('avatars').upload and
supabase.storage.from('avatars').remove, and keep setUploadingAvatar and toast
behavior unchanged.

Comment on lines +145 to +148
const handleRemoveAvatar = () => {
setProfile(prev => ({ ...prev, avatar_url: '' }));
toast.success("Avatar removed. Click 'Save Changes' to apply.");
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avatar removal doesn't delete the file from Supabase Storage.

This only clears the local state. The old avatar file remains in storage, causing orphaned files and potential storage costs over time. Consider deleting the file from storage when removing.

Suggested fix
- const handleRemoveAvatar = () => {
+ const handleRemoveAvatar = async () => {
+   if (profile.avatar_url) {
+     try {
+       // Extract filename from URL
+       const urlParts = profile.avatar_url.split('/');
+       const fileName = urlParts[urlParts.length - 1];
+       await supabase.storage.from('avatars').remove([fileName]);
+     } catch (error) {
+       console.error('Failed to delete old avatar:', error);
+     }
+   }
    setProfile(prev => ({ ...prev, avatar_url: '' }));
    toast.success("Avatar removed. Click 'Save Changes' to apply.");
  };
🤖 Prompt for AI Agents
In `@frontend/src/components/pages/ProfilePage.tsx` around lines 145 - 148, The
current handleRemoveAvatar only clears local state; update it to also delete the
file from Supabase Storage: inside handleRemoveAvatar (or a helper it calls)
derive the storage file path from profile.avatar_url, call your Supabase client
(e.g., supabase or
supabaseClient).storage.from(<your-bucket-name>).remove([filePath]), handle the
returned error/success, and then update setProfile(prev => ({...prev,
avatar_url: ''})) only after a successful delete (or still clear UI but surface
errors). Show a toast for success and for failure (include error.message) and
ensure you guard for empty/undefined avatar_url before attempting deletion.

Comment on lines +156 to +163
const handleSave = async () => {
setIsSaving(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
toast.error("You must be logged in to save changes.");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Early return leaves isSaving stuck as true.

If session validation fails, the function returns before the finally block can reset isSaving, leaving the save button permanently disabled.

Suggested fix
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) {
        toast.error("You must be logged in to save changes.");
+       setIsSaving(false);
        return;
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSave = async () => {
setIsSaving(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
toast.error("You must be logged in to save changes.");
return;
}
const handleSave = async () => {
setIsSaving(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
toast.error("You must be logged in to save changes.");
setIsSaving(false);
return;
}
🤖 Prompt for AI Agents
In `@frontend/src/components/pages/ProfilePage.tsx` around lines 156 - 163, The
early return inside handleSave after checking session leaves isSaving true; to
fix, either perform the session validation before calling setIsSaving(true) or
ensure you reset isSaving before returning (e.g., call setIsSaving(false) right
before the toast/error return) so the finally cleanup is not relied upon for
that path; locate the handleSave function and update the session check or add a
setIsSaving(false) prior to the return so the save button is never left
disabled.

@PinJinx
Copy link
Contributor Author

PinJinx commented Jan 16, 2026

Hi @smokeyScraper,

This is a fairly large PR, but I’ve done my best to keep the code clean and well-commented throughout. I have a few implementation details and setup questions I'd like your feedback on:

  1. Supabase Storage Setup For the avatar upload feature to work, we need a public bucket named avatars.

Documentation: Should I add the setup instructions to the README.md, or is there a specific documentation file for project setup where this belongs?

SQL Policies: I have the RLS policies ready (for authenticated uploads/public reads). Should I append them to database/01_create_integration_tables.sql in this PR, or would you prefer a separate SQL migration file?

  1. Skills Structure I implemented the frontend UI to map to the backend's dictionary structure (separating "Languages" and "Frameworks"). Let me know if this structure aligns with your vision or if you prefer something else.

  2. Profile Fields I aligned the frontend fields with the current database schema. If you feel any fields are missing or unnecessary, just let me know, and I can adjust the schema and UI in a follow-up commit.

Everything has been thoroughly tested locally. I will keep an eye on the CodeRabbit review and bundle any syntax/style fixes into a single cleanup commit to keep the history tidy.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEATURE REQUEST: Connect Frontend With Backend User System

1 participant