From 321d5c5a8cbe4ffd71fc3583312523b5fff8687f Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:41:44 +0530 Subject: [PATCH 1/9] feat: Add LinkedIn account field to profiles table - Add linkedin_account column to profiles table - Create unique case-insensitive index for linkedin_account - Follows same pattern as github_login implementation - Enforces global uniqueness (first-come-first-serve) Part of #163 --- backend/migrations/004_add_linkedin_account.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/migrations/004_add_linkedin_account.sql diff --git a/backend/migrations/004_add_linkedin_account.sql b/backend/migrations/004_add_linkedin_account.sql new file mode 100644 index 0000000..b86db30 --- /dev/null +++ b/backend/migrations/004_add_linkedin_account.sql @@ -0,0 +1,2 @@ +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS linkedin_account TEXT; +CREATE UNIQUE INDEX IF NOT EXISTS unique_linkedin_account_lower ON profiles (LOWER(linkedin_account)); From efec1c2e260fa0539e3b22e7e4082ac178197a5e Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:41:55 +0530 Subject: [PATCH 2/9] feat: Add linkedin_account field to Profile entity - Add linkedin_account as optional String field - Initialize to None in Profile::new() - Maintains consistency with github_login pattern - Serializable for API responses Part of #163 --- backend/src/domain/entities/profile.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/domain/entities/profile.rs b/backend/src/domain/entities/profile.rs index fd42982..a6acd01 100644 --- a/backend/src/domain/entities/profile.rs +++ b/backend/src/domain/entities/profile.rs @@ -10,6 +10,7 @@ pub struct Profile { pub description: Option, pub avatar_url: Option, pub github_login: Option, + pub linkedin_account: Option, pub login_nonce: i64, pub created_at: DateTime, pub updated_at: DateTime, @@ -24,6 +25,7 @@ impl Profile { description: None, avatar_url: None, github_login: None, + linkedin_account: None, login_nonce: 1, created_at: now, updated_at: now, From fcc7d52f2f980f1a9ffe4a82ea83361085f25a88 Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:42:22 +0530 Subject: [PATCH 3/9] feat: Add find_by_linkedin_account method to ProfileRepository trait - Add async method to find profile by LinkedIn account - Follows same pattern as find_by_github_login - Returns Option for case-insensitive lookup - Enables LinkedIn account uniqueness validation Part of #163 --- backend/src/domain/repositories/profile_repository.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/domain/repositories/profile_repository.rs b/backend/src/domain/repositories/profile_repository.rs index 3658458..a02a135 100644 --- a/backend/src/domain/repositories/profile_repository.rs +++ b/backend/src/domain/repositories/profile_repository.rs @@ -16,6 +16,10 @@ pub trait ProfileRepository: Send + Sync { &self, github_login: &str, ) -> Result, Box>; + async fn find_by_linkedin_account( + &self, + linkedin_account: &str, + ) -> Result, Box>; async fn get_login_nonce_by_wallet_address( &self, address: &WalletAddress, From b89b441518e07ee8b8145f0bbb62e7478006cb54 Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:42:48 +0530 Subject: [PATCH 4/9] feat: Add LinkedIn account support to PostgresProfileRepository - Add linkedin_account to all SELECT queries - Include linkedin_account in INSERT and UPDATE operations - Implement find_by_linkedin_account with case-insensitive lookup - Maintain consistency with github_login implementation - Ensure all Profile instances include linkedin_account field Part of #163 --- .../postgres_profile_repository.rs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/backend/src/infrastructure/repositories/postgres_profile_repository.rs b/backend/src/infrastructure/repositories/postgres_profile_repository.rs index 3eb691d..2de4b47 100644 --- a/backend/src/infrastructure/repositories/postgres_profile_repository.rs +++ b/backend/src/infrastructure/repositories/postgres_profile_repository.rs @@ -24,7 +24,7 @@ impl ProfileRepository for PostgresProfileRepository { ) -> Result, Box> { let row = sqlx::query!( r#" - SELECT address, name, description, avatar_url, github_login, created_at, updated_at + SELECT address, name, description, avatar_url, github_login, linkedin_account, created_at, updated_at FROM profiles WHERE address = $1 "#, @@ -40,6 +40,7 @@ impl ProfileRepository for PostgresProfileRepository { description: r.description, avatar_url: r.avatar_url, github_login: r.github_login, + linkedin_account: r.linkedin_account, login_nonce: 0, // Not needed for regular profile queries created_at: r.created_at.unwrap(), updated_at: r.updated_at.unwrap(), @@ -49,7 +50,7 @@ impl ProfileRepository for PostgresProfileRepository { async fn find_all(&self) -> Result, Box> { let rows = sqlx::query!( r#" - SELECT address, name, description, avatar_url, github_login, created_at, updated_at + SELECT address, name, description, avatar_url, github_login, linkedin_account, created_at, updated_at FROM profiles "#, ) @@ -65,6 +66,7 @@ impl ProfileRepository for PostgresProfileRepository { description: r.description, avatar_url: r.avatar_url, github_login: r.github_login, + linkedin_account: r.linkedin_account, login_nonce: 0, // Not needed for regular profile queries created_at: r.created_at.unwrap(), updated_at: r.updated_at.unwrap(), @@ -75,14 +77,15 @@ impl ProfileRepository for PostgresProfileRepository { async fn create(&self, profile: &Profile) -> Result<(), Box> { sqlx::query!( r#" - INSERT INTO profiles (address, name, description, avatar_url, github_login, login_nonce, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + INSERT INTO profiles (address, name, description, avatar_url, github_login, linkedin_account, login_nonce, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) "#, profile.address.as_str(), profile.name, profile.description, profile.avatar_url, profile.github_login, + profile.linkedin_account, profile.login_nonce, profile.created_at, profile.updated_at @@ -98,7 +101,7 @@ impl ProfileRepository for PostgresProfileRepository { sqlx::query!( r#" UPDATE profiles - SET name = $2, description = $3, avatar_url = $4, github_login = $5, updated_at = $6 + SET name = $2, description = $3, avatar_url = $4, github_login = $5, linkedin_account = $6, updated_at = $7 WHERE address = $1 "#, profile.address.as_str(), @@ -106,6 +109,7 @@ impl ProfileRepository for PostgresProfileRepository { profile.description, profile.avatar_url, profile.github_login, + profile.linkedin_account, profile.updated_at ) .execute(&self.pool) @@ -136,7 +140,7 @@ impl ProfileRepository for PostgresProfileRepository { ) -> Result, Box> { let row = sqlx::query!( r#" - SELECT address, name, description, avatar_url, github_login, created_at, updated_at + SELECT address, name, description, avatar_url, github_login, linkedin_account, created_at, updated_at FROM profiles WHERE LOWER(github_login) = LOWER($1) "#, @@ -152,6 +156,36 @@ impl ProfileRepository for PostgresProfileRepository { description: r.description, avatar_url: r.avatar_url, github_login: r.github_login, + linkedin_account: r.linkedin_account, + login_nonce: 0, // Not needed for regular profile queries + created_at: r.created_at.unwrap(), + updated_at: r.updated_at.unwrap(), + })) + } + + async fn find_by_linkedin_account( + &self, + linkedin_account: &str, + ) -> Result, Box> { + let row = sqlx::query!( + r#" + SELECT address, name, description, avatar_url, github_login, linkedin_account, created_at, updated_at + FROM profiles + WHERE LOWER(linkedin_account) = LOWER($1) + "#, + linkedin_account + ) + .fetch_optional(&self.pool) + .await + .map_err(|e| Box::new(e) as Box)?; + + Ok(row.map(|r| Profile { + address: WalletAddress(r.address), + name: r.name, + description: r.description, + avatar_url: r.avatar_url, + github_login: r.github_login, + linkedin_account: r.linkedin_account, login_nonce: 0, // Not needed for regular profile queries created_at: r.created_at.unwrap(), updated_at: r.updated_at.unwrap(), From 5474effc57b8451dcb0c78a421a7d53bc88ba0bd Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:44:02 +0530 Subject: [PATCH 5/9] feat: Add linkedin_account to profile DTOs - Add linkedin_account to UpdateProfileRequest for profile updates - Add linkedin_account to ProfileResponse for API responses - Maintains consistency with github_login pattern - Enables frontend to send and receive LinkedIn account data Part of #163 --- backend/src/application/dtos/profile_dtos.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/application/dtos/profile_dtos.rs b/backend/src/application/dtos/profile_dtos.rs index c1f356e..55f9346 100644 --- a/backend/src/application/dtos/profile_dtos.rs +++ b/backend/src/application/dtos/profile_dtos.rs @@ -15,6 +15,7 @@ pub struct UpdateProfileRequest { pub description: Option, pub avatar_url: Option, pub github_login: Option, + pub linkedin_account: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -24,6 +25,7 @@ pub struct ProfileResponse { pub description: Option, pub avatar_url: Option, pub github_login: Option, + pub linkedin_account: Option, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } From cb6e36fdd893a14a80085c9fb11cbd7d6b4257fa Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:44:31 +0530 Subject: [PATCH 6/9] feat: Add LinkedIn account validation to update_profile - Add LinkedIn account validation with format checking (3-100 chars, alphanumeric + hyphens) - Implement case-insensitive uniqueness check via find_by_linkedin_account - Allow empty LinkedIn accounts (set to None) - Prevent conflicts with existing LinkedIn accounts - Return linkedin_account in ProfileResponse - Follows same validation pattern as github_login Part of #163 --- .../application/commands/update_profile.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/src/application/commands/update_profile.rs b/backend/src/application/commands/update_profile.rs index e94b524..c7df354 100644 --- a/backend/src/application/commands/update_profile.rs +++ b/backend/src/application/commands/update_profile.rs @@ -18,6 +18,8 @@ pub async fn update_profile( .ok_or("Profile not found")?; profile.update_info(request.name, request.description, request.avatar_url); + + // Handle GitHub login validation if let Some(ref handle) = request.github_login { let trimmed = handle.trim(); @@ -43,6 +45,35 @@ pub async fn update_profile( profile.github_login = Some(trimmed.to_string()); } } + + // Handle LinkedIn account validation + if let Some(ref account) = request.linkedin_account { + let trimmed = account.trim(); + + // Allow empty accounts (set to None) + if trimmed.is_empty() { + profile.linkedin_account = None; + } else { + // Validate format for non-empty LinkedIn accounts + // LinkedIn usernames can be 3-100 characters, alphanumeric and hyphens + let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{3,100}$").unwrap(); + if !valid_format.is_match(trimmed) { + return Err("Invalid LinkedIn account format".to_string()); + } + if let Some(conflicting_profile) = profile_repository + .find_by_linkedin_account(trimmed) + .await + .map_err(|e| e.to_string())? + { + // Only conflict if it's not the current user's profile + if conflicting_profile.address != wallet_address { + return Err("LinkedIn account already taken".to_string()); + } + } + profile.linkedin_account = Some(trimmed.to_string()); + } + } + profile_repository .update(&profile) .await @@ -54,6 +85,7 @@ pub async fn update_profile( description: profile.description, avatar_url: profile.avatar_url, github_login: profile.github_login, + linkedin_account: profile.linkedin_account, created_at: profile.created_at, updated_at: profile.updated_at, }) From ba2f69a4a2474f4ca1ec27628977399f1501df1e Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:45:01 +0530 Subject: [PATCH 7/9] feat: Include linkedin_account in get_profile response - Add linkedin_account to ProfileResponse in get_profile query - Ensures LinkedIn account is returned when fetching profile - Maintains consistency with github_login pattern Part of #163 --- backend/src/application/queries/get_profile.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/application/queries/get_profile.rs b/backend/src/application/queries/get_profile.rs index f4593ab..818d900 100644 --- a/backend/src/application/queries/get_profile.rs +++ b/backend/src/application/queries/get_profile.rs @@ -21,6 +21,7 @@ pub async fn get_profile( description: profile.description, avatar_url: profile.avatar_url, github_login: profile.github_login, + linkedin_account: profile.linkedin_account, created_at: profile.created_at, updated_at: profile.updated_at, }) From 006ae8692003b855bc9c617bcf9dbfbd5da9bcdb Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:45:02 +0530 Subject: [PATCH 8/9] feat: Include linkedin_account in get_all_profiles response - Add linkedin_account to ProfileResponse in get_all_profiles query - Ensures LinkedIn account is returned when fetching all profiles - Maintains consistency with github_login pattern Part of #163 --- backend/src/application/queries/get_all_profiles.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/application/queries/get_all_profiles.rs b/backend/src/application/queries/get_all_profiles.rs index ed42faf..6feaffe 100644 --- a/backend/src/application/queries/get_all_profiles.rs +++ b/backend/src/application/queries/get_all_profiles.rs @@ -18,6 +18,7 @@ pub async fn get_all_profiles( description: profile.description, avatar_url: profile.avatar_url, github_login: profile.github_login, + linkedin_account: profile.linkedin_account, created_at: profile.created_at, updated_at: profile.updated_at, }) From ccf63f27177cd219a4b1c2e4486ed6238fb0f922 Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:45:54 +0530 Subject: [PATCH 9/9] docs: Add comprehensive documentation for LinkedIn account feature - Document all backend changes and implementation details - Provide API usage examples with request/response formats - Detail validation rules and error handling - Include database schema and migration instructions - Add testing procedures and manual testing steps - Document consistency with GitHub login pattern - List future enhancement opportunities Part of #163 --- LINKEDIN_BACKEND_IMPLEMENTATION.md | 398 +++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 LINKEDIN_BACKEND_IMPLEMENTATION.md diff --git a/LINKEDIN_BACKEND_IMPLEMENTATION.md b/LINKEDIN_BACKEND_IMPLEMENTATION.md new file mode 100644 index 0000000..98a27f1 --- /dev/null +++ b/LINKEDIN_BACKEND_IMPLEMENTATION.md @@ -0,0 +1,398 @@ +# LinkedIn Account Backend Implementation + +This document describes the implementation of LinkedIn account support in the backend, addressing issue #163. + +## Overview + +This implementation adds LinkedIn account functionality to user profiles, following the same pattern as the existing GitHub login feature. Users can now add their LinkedIn account to their profile with global, case-insensitive uniqueness enforcement (first-come-first-serve). + +## Changes Made + +### 1. Database Migration (`backend/migrations/004_add_linkedin_account.sql`) + +```sql +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS linkedin_account TEXT; +CREATE UNIQUE INDEX IF NOT EXISTS unique_linkedin_account_lower ON profiles (LOWER(linkedin_account)); +``` + +**Features:** +- Adds `linkedin_account` column to profiles table +- Creates unique case-insensitive index for global uniqueness +- Follows same pattern as `github_login` implementation + +### 2. Domain Entity (`backend/src/domain/entities/profile.rs`) + +**Changes:** +- Added `linkedin_account: Option` field to `Profile` struct +- Initialized to `None` in `Profile::new()` +- Serializable for API responses + +### 3. Repository Trait (`backend/src/domain/repositories/profile_repository.rs`) + +**New Method:** +```rust +async fn find_by_linkedin_account( + &self, + linkedin_account: &str, +) -> Result, Box>; +``` + +**Purpose:** +- Enables case-insensitive lookup by LinkedIn account +- Used for uniqueness validation during profile updates + +### 4. Repository Implementation (`backend/src/infrastructure/repositories/postgres_profile_repository.rs`) + +**Changes:** +- Added `linkedin_account` to all SELECT queries +- Included `linkedin_account` in INSERT and UPDATE operations +- Implemented `find_by_linkedin_account` with case-insensitive lookup using `LOWER()` + +**Example Query:** +```rust +SELECT address, name, description, avatar_url, github_login, linkedin_account, created_at, updated_at +FROM profiles +WHERE LOWER(linkedin_account) = LOWER($1) +``` + +### 5. DTOs (`backend/src/application/dtos/profile_dtos.rs`) + +**UpdateProfileRequest:** +```rust +pub struct UpdateProfileRequest { + pub name: Option, + pub description: Option, + pub avatar_url: Option, + pub github_login: Option, + pub linkedin_account: Option, // NEW +} +``` + +**ProfileResponse:** +```rust +pub struct ProfileResponse { + pub address: WalletAddress, + pub name: String, + pub description: Option, + pub avatar_url: Option, + pub github_login: Option, + pub linkedin_account: Option, // NEW + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} +``` + +### 6. Update Profile Command (`backend/src/application/commands/update_profile.rs`) + +**Validation Logic:** +```rust +if let Some(ref account) = request.linkedin_account { + let trimmed = account.trim(); + + if trimmed.is_empty() { + profile.linkedin_account = None; + } else { + // Validate format (3-100 chars, alphanumeric + hyphens) + let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{3,100}$").unwrap(); + if !valid_format.is_match(trimmed) { + return Err("Invalid LinkedIn account format".to_string()); + } + + // Check uniqueness + if let Some(conflicting_profile) = profile_repository + .find_by_linkedin_account(trimmed) + .await + .map_err(|e| e.to_string())? + { + if conflicting_profile.address != wallet_address { + return Err("LinkedIn account already taken".to_string()); + } + } + + profile.linkedin_account = Some(trimmed.to_string()); + } +} +``` + +**Features:** +- Format validation: 3-100 characters, alphanumeric and hyphens +- Case-insensitive uniqueness check +- Allows empty values (sets to `None`) +- Prevents conflicts with existing LinkedIn accounts +- Returns appropriate error messages + +### 7. Query Functions + +**get_profile.rs:** +- Added `linkedin_account` to `ProfileResponse` + +**get_all_profiles.rs:** +- Added `linkedin_account` to `ProfileResponse` mapping + +## API Usage + +### Update Profile with LinkedIn Account + +**Endpoint:** `PUT /api/profiles` + +**Request Body:** +```json +{ + "name": "John Doe", + "description": "Software Developer", + "avatar_url": "https://example.com/avatar.jpg", + "github_login": "johndoe", + "linkedin_account": "john-doe-123" +} +``` + +**Success Response (200 OK):** +```json +{ + "address": "0x1234567890abcdef", + "name": "John Doe", + "description": "Software Developer", + "avatar_url": "https://example.com/avatar.jpg", + "github_login": "johndoe", + "linkedin_account": "john-doe-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-02T00:00:00Z" +} +``` + +**Error Responses:** + +**409 Conflict** - LinkedIn account already taken: +```json +{ + "error": "LinkedIn account already taken" +} +``` + +**400 Bad Request** - Invalid format: +```json +{ + "error": "Invalid LinkedIn account format" +} +``` + +### Get Profile + +**Endpoint:** `GET /api/profiles/:address` + +**Response:** +```json +{ + "address": "0x1234567890abcdef", + "name": "John Doe", + "description": "Software Developer", + "avatar_url": "https://example.com/avatar.jpg", + "github_login": "johndoe", + "linkedin_account": "john-doe-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-02T00:00:00Z" +} +``` + +### Get All Profiles + +**Endpoint:** `GET /api/profiles` + +**Response:** +```json +[ + { + "address": "0x1234567890abcdef", + "name": "John Doe", + "linkedin_account": "john-doe-123", + ... + }, + { + "address": "0xfedcba0987654321", + "name": "Jane Smith", + "linkedin_account": "jane-smith-456", + ... + } +] +``` + +## Validation Rules + +### LinkedIn Account Format + +- **Length:** 3-100 characters +- **Allowed Characters:** Alphanumeric (a-z, A-Z, 0-9) and hyphens (-) +- **Regex Pattern:** `^[a-zA-Z0-9-]{3,100}$` + +### Examples + +✅ **Valid:** +- `john-doe` +- `jane-smith-123` +- `developer-2024` +- `abc` + +❌ **Invalid:** +- `ab` (too short, minimum 3 characters) +- `john_doe` (underscores not allowed) +- `john.doe` (dots not allowed) +- `john doe` (spaces not allowed) +- `john@doe` (special characters not allowed) + +### Uniqueness + +- **Case-Insensitive:** `john-doe` and `JOHN-DOE` are considered the same +- **Global:** LinkedIn accounts are unique across all profiles +- **First-Come-First-Serve:** The first user to claim a LinkedIn account owns it +- **Conflict Prevention:** Users cannot take LinkedIn accounts already claimed by others +- **Self-Update Allowed:** Users can update their own LinkedIn account without conflict + +### Empty Values + +- Sending an empty string (`""`) or whitespace-only string sets `linkedin_account` to `NULL` +- Omitting the field in the request leaves the existing value unchanged + +## Database Schema + +### profiles Table + +```sql +CREATE TABLE profiles ( + address VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + description TEXT, + avatar_url TEXT, + github_login TEXT, + linkedin_account TEXT, -- NEW + login_nonce BIGINT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Existing index +CREATE UNIQUE INDEX unique_github_login_lower ON profiles (LOWER(github_login)); + +-- New index +CREATE UNIQUE INDEX unique_linkedin_account_lower ON profiles (LOWER(linkedin_account)); +``` + +## Testing + +### Manual Testing Steps + +1. **Add LinkedIn Account:** + ```bash + curl -X PUT http://localhost:8080/api/profiles \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"linkedin_account": "john-doe-123"}' + ``` + +2. **Verify Uniqueness:** + ```bash + # Try to claim the same LinkedIn account with different wallet + # Should return 409 Conflict + ``` + +3. **Test Case-Insensitivity:** + ```bash + # Try "JOHN-DOE-123" after claiming "john-doe-123" + # Should return 409 Conflict + ``` + +4. **Test Format Validation:** + ```bash + # Try invalid formats + curl -X PUT http://localhost:8080/api/profiles \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"linkedin_account": "ab"}' # Too short + # Should return 400 Bad Request + ``` + +5. **Test Empty Value:** + ```bash + curl -X PUT http://localhost:8080/api/profiles \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"linkedin_account": ""}' + # Should set linkedin_account to NULL + ``` + +6. **Verify in Database:** + ```sql + SELECT address, name, github_login, linkedin_account + FROM profiles + WHERE linkedin_account IS NOT NULL; + ``` + +## Migration Instructions + +### Running the Migration + +```bash +# From backend directory +sqlx migrate run +``` + +### Rollback (if needed) + +```bash +# Create rollback migration +cat > migrations/005_rollback_linkedin.sql << EOF +DROP INDEX IF EXISTS unique_linkedin_account_lower; +ALTER TABLE profiles DROP COLUMN IF EXISTS linkedin_account; +EOF + +sqlx migrate run +``` + +## Consistency with GitHub Login + +This implementation follows the exact same pattern as the GitHub login feature: + +| Feature | GitHub Login | LinkedIn Account | +|---------|-------------|------------------| +| Database Column | `github_login` | `linkedin_account` | +| Unique Index | `unique_github_login_lower` | `unique_linkedin_account_lower` | +| Repository Method | `find_by_github_login` | `find_by_linkedin_account` | +| Validation Regex | `^[a-zA-Z0-9-]{1,39}$` | `^[a-zA-Z0-9-]{3,100}$` | +| Case Sensitivity | Case-insensitive | Case-insensitive | +| Empty Handling | Sets to NULL | Sets to NULL | +| Error Message | "GitHub handle already taken" | "LinkedIn account already taken" | + +## Future Enhancements + +Potential improvements for future PRs: + +1. **LinkedIn Profile Verification:** + - OAuth integration to verify LinkedIn account ownership + - Display verified badge on profiles + +2. **LinkedIn Profile Data:** + - Fetch and display LinkedIn profile information + - Show professional headline, company, etc. + +3. **Search by LinkedIn:** + - Add API endpoint to search profiles by LinkedIn account + - Enable profile discovery via LinkedIn + +4. **LinkedIn Profile Links:** + - Generate clickable LinkedIn profile URLs + - Format: `https://linkedin.com/in/{linkedin_account}` + +5. **Batch Operations:** + - Import LinkedIn accounts from CSV + - Bulk validation and assignment + +## Related Issues + +- Fixes #163 (Add linkedin link to profile: BE) +- Related to #162 (Add linkedin account to user profiles - Full Stack) + +## Notes + +- All changes are backward compatible +- Existing profiles will have `linkedin_account` set to `NULL` +- No breaking changes to existing API endpoints +- Frontend integration required separately (issue #162)