diff --git a/.gitignore b/.gitignore
index 7985c8f..c83044d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,9 @@ pnpm-debug.log*
lerna-debug.log*
/backend/node_modules
-/frontend/dist
+# /frontend/dist
/backend/.env
+/frontend/.env
dist-ssr
*.local
diff --git a/BACKEND_INTEGRATION.md b/BACKEND_INTEGRATION.md
new file mode 100644
index 0000000..363df1b
--- /dev/null
+++ b/BACKEND_INTEGRATION.md
@@ -0,0 +1,242 @@
+# Backend Integration Summary
+
+This document summarizes all the backend endpoints and integrations that have been implemented to support the frontend features.
+
+## ✅ Completed Integrations
+
+### 1. Settings Page (`/settings`)
+
+**Backend Endpoints:**
+- `POST /settings/get` - Get user settings
+- `POST /settings/update` - Update user settings
+
+**Controller:** `SettingsController.js`
+- `getSettings()` - Retrieves or creates default settings for user
+- `updateSettings()` - Updates user settings with validation
+
+**Route:** `SettingsRoute.js`
+- Protected routes (requires authentication via `verifyToken`)
+
+**Frontend Integration:**
+- `Settings.jsx` already uses these endpoints
+- Loads settings on mount
+- Saves settings on button click
+
+---
+
+### 2. Search Functionality (Navbar)
+
+**Backend Endpoint:**
+- `POST /search/users` - Search for users
+
+**Controller:** `SearchController.js`
+- `searchUsers()` - Searches users by username or name
+- Filters results based on user settings (isPublic + allowSearch)
+- Returns only searchable profiles
+- Includes profile images
+
+**Route:** `SearchRoute.js`
+- Public route (no authentication required)
+
+**Frontend Integration:**
+- `Nav.jsx` already uses `/search/users` endpoint
+- Debounced search (300ms)
+- Shows results in dropdown
+- Click to visit profile
+
+---
+
+### 3. Profile Preview (`/profile/:username`)
+
+**Backend Endpoint:**
+- `POST /profile/getpublicprofile` - Get public profile data
+
+**Controller:** `ProfileController.js`
+- `getPublicProfile()` - Returns public profile with:
+ - Profile information (filtered by settings)
+ - Only public links
+ - Settings for display control
+ - Stats (if enabled)
+
+**Route:** `ProfileRoute.js`
+- Public route (no authentication required)
+
+**Frontend Integration:**
+- `ProfilePreview.jsx` already uses `/profile/getpublicprofile`
+- Respects all visibility settings
+- Shows only public links
+- Displays stats based on settings
+
+---
+
+### 4. Link Visibility Controls
+
+**Backend Endpoint:**
+- `POST /source/updatevisibility` - Update link visibility
+
+**Controller:** `LinkController.js`
+- `updateVisibility()` - Updates link visibility (public/unlisted/private)
+- Handles password hashing for unlisted links
+- Validates user ownership
+- Clears password for public/private links
+
+**Route:** `LinkRoute.js`
+- Protected route (requires authentication)
+
+**Frontend Integration:**
+- `Linkcard.jsx` already uses `/source/updatevisibility`
+- Dropdown menu for visibility selection
+- Updates Redux state on success
+- Visual indicators for visibility status
+
+---
+
+### 5. Link Model Enhancements
+
+**Changes:**
+- Added `linkId` field (unique, required) - Generated automatically
+- Added `visibility` field (enum: public/unlisted/private)
+- Added `password` field (for unlisted links)
+- Added `deletedAt` field (soft delete)
+- Pre-save hook to generate `linkId` if missing
+
+**Controller Updates:**
+- `addNewSource()` - Generates `linkId` when creating links
+- `getAllSource()` - Returns `visibility` and `linkId` fields
+- Filters out deleted links (`deletedAt: null`)
+
+---
+
+## 📋 API Endpoints Reference
+
+### Settings Endpoints
+
+```
+POST /settings/get
+Body: { username?: string }
+Response: { success: boolean, settings: UserSettings }
+Auth: Required
+
+POST /settings/update
+Body: { username, profile?, links?, search?, privacy?, notifications? }
+Response: { success: boolean, settings: UserSettings }
+Auth: Required
+```
+
+### Search Endpoints
+
+```
+POST /search/users
+Body: { query: string }
+Response: { success: boolean, results: User[] }
+Auth: Not required (public)
+```
+
+### Profile Endpoints
+
+```
+POST /profile/getpublicprofile
+Body: { username: string }
+Response: { success: boolean, profile, links, settings, stats }
+Auth: Not required (public)
+```
+
+### Link Endpoints
+
+```
+POST /source/updatevisibility
+Body: { id: string, visibility: 'public'|'unlisted'|'private', password?: string }
+Response: { success: boolean, link: Link }
+Auth: Required
+```
+
+---
+
+## 🔒 Security Features
+
+1. **Authentication**: All user-specific endpoints require JWT token
+2. **Authorization**: Users can only modify their own data
+3. **Password Hashing**: Unlisted link passwords are hashed with bcrypt
+4. **Input Validation**: All endpoints validate required fields
+5. **Privacy Controls**: Profile visibility respects user settings
+
+---
+
+## 🗄️ Database Models Used
+
+1. **UserSettings** - Privacy and visibility settings
+2. **User** - User authentication and basic info
+3. **UserProfile** - Extended profile information
+4. **Link** - Link data with visibility controls
+
+---
+
+## 🔄 Data Flow
+
+### Settings Flow
+1. User opens Settings page
+2. Frontend calls `GET /settings/get`
+3. Backend returns or creates default settings
+4. User modifies settings
+5. Frontend calls `POST /settings/update`
+6. Backend updates and saves settings
+
+### Search Flow
+1. User types in search box
+2. Frontend debounces and calls `POST /search/users`
+3. Backend searches users and filters by settings
+4. Backend returns searchable profiles
+5. Frontend displays results in dropdown
+
+### Profile Preview Flow
+1. Visitor navigates to `/profile/:username`
+2. Frontend calls `POST /profile/getpublicprofile`
+3. Backend checks if profile is public
+4. Backend returns profile data with only public links
+5. Frontend displays based on visibility settings
+
+### Link Visibility Flow
+1. User clicks visibility button on link card
+2. User selects new visibility (public/unlisted/private)
+3. Frontend calls `POST /source/updatevisibility`
+4. Backend validates ownership and updates
+5. Frontend updates Redux state
+
+---
+
+## 🐛 Error Handling
+
+All endpoints include proper error handling:
+- 400: Bad Request (missing/invalid data)
+- 401: Unauthorized (authentication required)
+- 403: Forbidden (insufficient permissions)
+- 404: Not Found (resource doesn't exist)
+- 500: Internal Server Error
+
+---
+
+## 📝 Notes
+
+1. **linkId Generation**: Automatically generated using crypto.randomBytes()
+2. **Default Settings**: Created automatically on first access
+3. **Search Privacy**: Only shows profiles with `isPublic=true` AND `allowSearch=true`
+4. **Link Visibility**: Default is 'public' for new links
+5. **Soft Deletes**: All models support soft deletes via `deletedAt` field
+
+---
+
+## ✅ Testing Checklist
+
+- [x] Settings page loads and saves correctly
+- [x] Search functionality works with privacy settings
+- [x] Profile preview respects all visibility settings
+- [x] Link visibility can be changed
+- [x] Only public links show in profile preview
+- [x] Authentication required for protected endpoints
+- [x] Error handling works correctly
+- [x] linkId is generated for new links
+
+---
+
+**Last Updated**: 2024-12-XX
+**Status**: ✅ All endpoints implemented and wired up
diff --git a/DEPLOYMENT_FIXES.md b/DEPLOYMENT_FIXES.md
deleted file mode 100644
index 1208983..0000000
--- a/DEPLOYMENT_FIXES.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# EC2 Deployment Issues - Fixed
-
-## Issues Found:
-
-### 1. **Nginx Location Block Order** ❌
-
- - **Problem**: The `location /` block was catching all requests, including `/app`, before the more specific `/app` location could be matched.
-
- - **Fix**: Reordered location blocks so `/app` comes first with `^~` prefix, and backend routes are more specific.
-
-### 2. **API Base URL Configuration** ❌
- - **Problem**: `VITE_API_URL` was set to `http://backend:8080` (internal Docker network), but browser requests need the public URL.
- - **Fix**: Changed to `https://clickly.cv` in docker-compose.yml and updated api.js to use environment variable with fallback.
-
-### 3. **Vite Dev Server Proxy Configuration** ⚠️
- - **Problem**: Vite dev server needs proper proxy headers for HMR (Hot Module Replacement).
- - **Fix**: Added proper proxy headers including Upgrade and Connection for WebSocket support.
-
-## Changes Made:
-
-### nginx/nginx.conf
-- Reordered location blocks: `/app` first, then specific backend routes
-- Added Vite HMR support with proper timeout settings
-- Added specific regex patterns for username routes
-- Root path now redirects to `/app/`
-
-### frontend/src/utils/api.js
-- Now uses `import.meta.env.VITE_API_URL` with fallback
-- Added `withCredentials: true` for cookie support
-
-### docker-compose.yml
-- Changed `VITE_API_URL` from `http://backend:8080` to `https://clickly.cv`
-
-## Deployment Steps on EC2:
-
-1. **Pull the latest changes:**
- ```bash
- git pull
- ```
-
-2. **Restart Docker containers:**
- ```bash
- docker-compose down
- docker-compose up -d --build
- ```
-
-3. **Check nginx logs if issues persist:**
- ```bash
- docker logs nginx
- docker logs frontend
- docker logs backend
- ```
-
-4. **Verify SSL certificates exist:**
- ```bash
- sudo ls -la /etc/letsencrypt/live/clickly.cv/
- ```
-
-5. **If certificates don't exist, generate them:**
- ```bash
- ./generate-cert.sh
- ```
-
-## Testing:
-
-1. **Frontend should be accessible at:**
- - `https://clickly.cv/app/`
- - `https://www.clickly.cv/app/`
-
-2. **Backend API should work at:**
- - `https://clickly.cv/auth/...`
- - `https://clickly.cv/source/...`
-
-3. **User profiles should work at:**
- - `https://clickly.cv/username`
- - `https://clickly.cv/username/source`
-
-## Troubleshooting:
-
-If frontend still not accessible:
-
-1. **Check if frontend container is running:**
- ```bash
- docker ps | grep frontend
- ```
-
-2. **Check frontend logs:**
- ```bash
- docker logs frontend
- ```
-
-3. **Test nginx configuration:**
- ```bash
- docker exec nginx nginx -t
- ```
-
-4. **Check if port 443 is open in EC2 security group**
-
-5. **Verify DNS is pointing to EC2 IP:**
- ```bash
- dig clickly.cv
- ```
-
diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
new file mode 100644
index 0000000..fd70dec
--- /dev/null
+++ b/DOCUMENTATION.md
@@ -0,0 +1,199 @@
+# All in1 url Documentation Index
+
+This document provides an overview of all documentation available in the All in1 url project.
+
+## 📚 Documentation Structure
+
+### Frontend Documentation
+
+**Location**: `/frontend/docs/`
+
+- **[PAGES.md](./frontend/docs/PAGES.md)** - Comprehensive documentation about all page components
+ - Page descriptions and features
+ - Routing information
+ - Design patterns
+ - Light & dark mode guidelines
+ - Component dependencies
+ - Responsive design notes
+
+- **[README.md](./frontend/docs/README.md)** - Quick reference guide for frontend
+ - Page routes table
+ - Key features overview
+ - Design system
+ - Development guidelines
+
+- **[CHANGELOG.md](./frontend/CHANGELOG.md)** - Frontend change history
+ - All feature additions
+ - Bug fixes
+ - UI/UX improvements
+ - Breaking changes
+
+### Backend Documentation
+
+**Location**: `/backend/doc/`
+
+- **[README.md](./backend/doc/README.md)** - Database models overview
+ - Model relationships
+ - Common patterns
+ - Best practices
+ - Query examples
+
+- **[linkModel.md](./backend/doc/linkModel.md)** - Link model documentation
+- **[linkAnalyticsModel.md](./backend/doc/linkAnalyticsModel.md)** - Analytics model documentation
+- **[userModel.md](./backend/doc/userModel.md)** - User model documentation
+- **[userProfile.md](./backend/doc/userProfile.md)** - User Profile model documentation
+- **[userSettingsModel.md](./backend/doc/userSettingsModel.md)** - User Settings model documentation
+- **[otpModel.md](./backend/doc/otpModel.md)** - OTP model documentation
+
+- **[CHANGELOG.md](./backend/CHANGELOG.md)** - Backend change history
+ - Model changes
+ - API endpoint additions
+ - Database schema updates
+
+## 🎯 Quick Links
+
+### For Developers
+
+- **Getting Started**: See [README.md](./README.md#-getting-started)
+- **Frontend Pages**: See [frontend/docs/PAGES.md](./frontend/docs/PAGES.md)
+- **Database Models**: See [backend/doc/README.md](./backend/doc/README.md)
+- **API Endpoints**: See backend controllers (to be documented)
+
+### For Users
+
+- **User Guide**: See [Documentation Component](./frontend/src/components/Documentation.jsx)
+- **Features**: See [README.md](./README.md#-features)
+
+## 📖 Documentation Standards
+
+All documentation follows these standards:
+
+1. **Markdown Format**: All docs use Markdown for easy reading
+2. **Code Examples**: Include practical usage examples
+3. **Field Descriptions**: Explain purpose and usage of each field
+4. **Enum Values**: Document all possible enum values
+5. **Required/Optional**: Explain why fields are required or optional
+6. **Light/Dark Mode**: Document both mode behaviors
+7. **Responsive Design**: Note mobile/tablet/desktop differences
+
+## 🔄 Keeping Documentation Updated
+
+When making changes:
+
+1. **Update CHANGELOG**: Add entry to appropriate CHANGELOG.md
+2. **Update Model Docs**: If model changes, update corresponding .md file
+3. **Update Page Docs**: If page changes, update PAGES.md
+4. **Update README**: If major features added, update main README.md
+
+## 📝 Documentation Checklist
+
+When adding new features:
+
+- [ ] Update relevant CHANGELOG.md
+- [ ] Update model documentation if database changes
+- [ ] Update PAGES.md if new page added
+- [ ] Add code comments for complex logic
+- [ ] Update README.md if major feature
+- [ ] Add usage examples
+- [ ] Document light/dark mode behavior
+- [ ] Note responsive design considerations
+
+## 🎨 Design Documentation
+
+### Color Scheme
+
+**Light Mode**:
+- Primary: Purple (#9333EA), Pink (#EC4899), Blue (#3B82F6)
+- Background: White, Gray-50
+- Text: Gray-900, Gray-700
+- Cards: White/80, White/90
+
+**Dark Mode**:
+- Primary: Purple (#A855F7), Pink (#F472B6), Blue (#60A5FA)
+- Background: Gray-900, Gray-800
+- Text: White, Gray-300
+- Cards: Gray-900/50, Gray-800/50
+
+### Typography
+
+- Headings: Bold, gradient text
+- Body: Regular weight
+- Code: Monospace font
+- Sizes: Responsive (text-sm to text-5xl)
+
+### Spacing
+
+- Consistent padding: p-4, p-6, p-8
+- Gap spacing: gap-3, gap-4, gap-6
+- Margin: mb-4, mb-6, mb-8
+
+## 🚀 API Documentation
+
+API endpoints are documented in:
+- Backend controllers (inline comments)
+- Frontend API calls (usage examples)
+- Model documentation (data structures)
+
+**Note**: Comprehensive API documentation file coming soon.
+
+## 📱 Responsive Breakpoints
+
+- **Mobile**: < 640px (sm)
+- **Tablet**: 640px - 1024px (md)
+- **Desktop**: > 1024px (lg)
+
+## 🔍 Search Functionality
+
+**Desktop**: Full search input always visible
+**Mobile**: Search icon button, expands to input when clicked
+
+## 🎯 Key Features Documentation
+
+- **Link Visibility**: See [linkModel.md](./backend/doc/linkModel.md#visibility)
+- **User Settings**: See [userSettingsModel.md](./backend/doc/userSettingsModel.md)
+- **Analytics**: See [linkAnalyticsModel.md](./backend/doc/linkAnalyticsModel.md)
+- **Profile Preview**: See [PAGES.md](./frontend/docs/PAGES.md#6-profilepreview)
+
+## 💡 Tips for Contributors
+
+1. **Read First**: Check existing documentation before adding new features
+2. **Follow Patterns**: Use existing documentation as templates
+3. **Be Detailed**: Include examples and use cases
+4. **Update Both**: Update both frontend and backend docs if needed
+5. **Test Examples**: Ensure code examples work
+6. **Check Formatting**: Use consistent Markdown formatting
+
+## 📞 Questions?
+
+- Check existing documentation first
+- Review code comments
+- See examples in model documentation
+- Check CHANGELOG for recent changes
+
+---
+
+## 📊 Analytics Features
+
+### Comprehensive Analytics Dashboard
+- **Multiple Metrics**: Profile visits, click counts, location, OS, browser, device, referrer, hourly, day of week, platform, and link-based analytics
+- **Visualization Types**: Line charts, bar charts, area charts, and pie charts
+- **Time Ranges**: 7 days, 30 days, 90 days, 1 year, or all time
+- **Summary Cards**: Total clicks, profile visits, unique countries, top referrer
+- **Detailed Breakdowns**: Referrer categories, device types, browsers, operating systems, geographic distribution, hourly patterns, day of week patterns
+- **Theme Support**: Full dark and light theme support with proper text contrast
+
+### Analytics Data Points
+- **Click Tracking**: Real-time click counts with date-based trends
+- **Geographic Data**: Country-level location distribution
+- **Device Types**: Desktop, Mobile, Tablet breakdown
+- **Browsers**: Chrome, Safari, Firefox, Edge, and more
+- **Operating Systems**: Windows, macOS, Linux, iOS, Android
+- **Referrers**: Categorized as Direct, Search, Social, Internal, or External
+- **Temporal Patterns**: Hourly distribution and day-of-week analysis
+- **Platform Performance**: Individual platform click metrics
+- **Link Performance**: Per-link analytics and statistics
+
+---
+
+**Last Updated**: 2024-12-XX
+**Maintained By**: All in1 url Development Team
diff --git a/LINKEDIN_POST.md b/LINKEDIN_POST.md
new file mode 100644
index 0000000..03dacf9
--- /dev/null
+++ b/LINKEDIN_POST.md
@@ -0,0 +1,32 @@
+# LinkedIn Post Caption
+
+**Transform your digital presence with All in1 url - the smart way to manage all your social profiles in one place.**
+
+Stop juggling multiple long, forgettable links across your resume, business cards, and social media. All in1 url creates personalized, memorable links that work forever and update everywhere automatically.
+
+**Why professionals choose All in1 url:**
+✅ **Get your own FREE domain** - Your personalized subdomain (yourname.allin1url.in) that reflects your brand
+✅ **Memorable link format** - Clean URLs like yourname.allin1url.in/platform (not random codes!)
+✅ **Centralized management** - Update once, changes reflect everywhere instantly
+✅ **Advanced analytics** - Track clicks with detailed insights (location, device, browser, referrer)
+✅ **Click details dashboard** - See exactly who clicked, when, and from where
+✅ **Smart notifications** - Get email alerts for link clicks, profile views, and weekly reports
+✅ **Link privacy controls** - Public, unlisted, or password-protected private links
+✅ **Password protection** - Secure sensitive links with password authentication
+✅ **Never expires** - Your links work forever, no subscription required
+✅ **Completely free** - No hidden fees, no premium plans, open source forever
+
+Perfect for professionals, content creators, and anyone managing multiple online profiles. Create your personalized link hub today and simplify your digital identity.
+
+Try it free: allin1url.in
+
+---
+
+**I'd love to hear from you!** 💬
+
+Have you tried All in1 url? What do you think? Whether it's positive feedback, constructive criticism, feature suggestions, or ideas for improvement - I welcome all input. Your feedback helps make All in1 url better for everyone.
+
+Drop a comment below or reach out directly. Let's build something amazing together! 🚀
+
+#DigitalPresence #LinkManagement #ProfessionalBranding #SocialMedia #OpenSource #Analytics #Privacy
+
diff --git a/LinkBridger.postman_collection.json b/LinkBridger.postman_collection.json
new file mode 100644
index 0000000..7f433c0
--- /dev/null
+++ b/LinkBridger.postman_collection.json
@@ -0,0 +1,804 @@
+{
+ "info": {
+ "_postman_id": "All in1 url-api-collection",
+ "name": "All in1 url API Collection",
+ "description": "Complete API collection for All in1 url application. Includes all authentication, links, profile, settings, and search endpoints.",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Authentication",
+ "item": [
+ {
+ "name": "Send OTP (Signup)",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "if (pm.response.code === 201) {",
+ " pm.environment.set(\"email\", pm.request.body.raw ? JSON.parse(pm.request.body.raw).email : \"\");",
+ "}"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"user@example.com\",\n \"username\": \"testuser\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/signup",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "signup"
+ ]
+ },
+ "description": "Send OTP to email for account verification during signup. Username is optional."
+ },
+ "response": []
+ },
+ {
+ "name": "Verify Account (Complete Signup)",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"testuser\",\n \"password\": \"password123\",\n \"otp\": \"1234\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/verifyacc",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "verifyacc"
+ ]
+ },
+ "description": "Complete signup by verifying OTP. Requires email, username (min 5 chars), password, and OTP."
+ },
+ "response": []
+ },
+ {
+ "name": "Sign In",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "if (pm.response.code === 200) {",
+ " const response = pm.response.json();",
+ " if (response.user) {",
+ " pm.environment.set(\"username\", response.user.username);",
+ " pm.environment.set(\"userId\", response.user._id);",
+ " }",
+ "}"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"password123\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/signin",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "signin"
+ ]
+ },
+ "description": "Sign in with email and password. Token is stored in cookies automatically."
+ },
+ "response": []
+ },
+ {
+ "name": "Verify Token (Get User Info)",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}",
+ "description": "Token from signin cookie"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/auth/verify",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "verify"
+ ]
+ },
+ "description": "Verify authentication token and get user information. Requires valid token in cookies."
+ },
+ "response": []
+ },
+ {
+ "name": "Check Username Availability",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"testuser\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/checkavailablity",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "checkavailablity"
+ ]
+ },
+ "description": "Check if username is available. Returns status 209 if exists, 200 if available."
+ },
+ "response": []
+ },
+ {
+ "name": "Sign Out",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/auth/signout",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "signout"
+ ]
+ },
+ "description": "Sign out and clear authentication cookie."
+ },
+ "response": []
+ },
+ {
+ "name": "Password Reset - Send OTP",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"user@example.com\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/password_reset",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "password_reset"
+ ]
+ },
+ "description": "Send OTP to email for password reset."
+ },
+ "response": []
+ },
+ {
+ "name": "Password Reset - Validate OTP & Change Password",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"{{email}}\",\n \"otp\": \"1234\",\n \"password\": \"newpassword123\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/auth/validate_otp",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "auth",
+ "validate_otp"
+ ]
+ },
+ "description": "Validate OTP and change password. OTP is deleted after successful validation."
+ },
+ "response": []
+ }
+ ],
+ "description": "Authentication endpoints for user signup, signin, verification, and password reset."
+ },
+ {
+ "name": "Links",
+ "item": [
+ {
+ "name": "Add New Source",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\",\n \"source\": \"linkedin\",\n \"destination\": \"https://linkedin.com/in/username\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/source/addnewsource",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "addnewsource"
+ ]
+ },
+ "description": "Add a new link/source. Source is normalized to lowercase. Requires authentication."
+ },
+ "response": []
+ },
+ {
+ "name": "Get All Sources",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/source/getallsource",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "getallsource"
+ ]
+ },
+ "description": "Get all links/sources for a user. Returns source, destination, clicked, notSeen, visibility, and linkId. Requires authentication."
+ },
+ "response": []
+ },
+ {
+ "name": "Edit Link",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"id\": \"link_id_here\",\n \"source\": \"linkedin\",\n \"destination\": \"https://linkedin.com/in/newusername\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/source/editlink",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "editlink"
+ ]
+ },
+ "description": "Edit an existing link. Source and destination are optional. Requires authentication and ownership."
+ },
+ "response": []
+ },
+ {
+ "name": "Delete Link",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"id\": \"link_id_here\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/source/deletelink",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "deletelink"
+ ]
+ },
+ "description": "Delete a link by ID. Requires authentication."
+ },
+ "response": []
+ },
+ {
+ "name": "Set Notifications to Zero",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/source/notifications",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "notifications"
+ ]
+ },
+ "description": "Reset all notification counts (notSeen) to zero for all user's links. Requires authentication."
+ },
+ "response": []
+ },
+ {
+ "name": "Update Link Visibility",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"id\": \"link_id_here\",\n \"visibility\": \"public\",\n \"password\": \"optional_password_for_unlisted\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/source/updatevisibility",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "source",
+ "updatevisibility"
+ ]
+ },
+ "description": "Update link visibility. Options: 'public', 'unlisted', 'private'. Password required for 'unlisted'. Requires authentication."
+ },
+ "response": []
+ }
+ ],
+ "description": "Link management endpoints for adding, editing, deleting, and managing link visibility."
+ },
+ {
+ "name": "Profile",
+ "item": [
+ {
+ "name": "Update Profile",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\",\n \"name\": \"John Doe\",\n \"location\": \"New York, USA\",\n \"bio\": \"Software Developer\",\n \"passion\": \"Coding\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/profile/update",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "profile",
+ "update"
+ ]
+ },
+ "description": "Update user profile information. All fields are optional and will be set to empty strings if not provided."
+ },
+ "response": []
+ },
+ {
+ "name": "Get Profile Info",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/profile/getprofileinfo",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "profile",
+ "getprofileinfo"
+ ]
+ },
+ "description": "Get profile information for a user by username."
+ },
+ "response": []
+ },
+ {
+ "name": "Update Profile Picture",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\",\n \"image\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/profile/updatepic",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "profile",
+ "updatepic"
+ ]
+ },
+ "description": "Update profile picture. Image should be base64 encoded data URL. Image is uploaded to Cloudinary."
+ },
+ "response": []
+ },
+ {
+ "name": "Get Public Profile",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"testuser\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/profile/getpublicprofile",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "profile",
+ "getpublicprofile"
+ ]
+ },
+ "description": "Get public profile information. Returns profile data, public links, settings, and stats based on user's privacy settings. No authentication required."
+ },
+ "response": []
+ }
+ ],
+ "description": "Profile management endpoints for updating and retrieving user profile information."
+ },
+ {
+ "name": "Settings",
+ "item": [
+ {
+ "name": "Get Settings",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/settings/get",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "settings",
+ "get"
+ ]
+ },
+ "description": "Get user settings. Username is optional - if not provided, uses authenticated user's ID. Requires authentication."
+ },
+ "response": []
+ },
+ {
+ "name": "Update Settings",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"{{username}}\",\n \"profile\": {\n \"isPublic\": true,\n \"allowProfileView\": true,\n \"showProfileImage\": true,\n \"showBio\": true,\n \"showLocation\": true,\n \"showPassion\": true\n },\n \"links\": {\n \"showLinkCount\": true,\n \"showClickStats\": true\n },\n \"search\": {\n \"allowSearch\": true,\n \"searchKeywords\": [\"developer\", \"coder\"]\n },\n \"privacy\": {\n \"hideEmail\": true,\n \"hideUsername\": false\n },\n \"notifications\": {\n \"emailNotifications\": true,\n \"visitNotifications\": true\n }\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/settings/update",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "settings",
+ "update"
+ ]
+ },
+ "description": "Update user settings. You can update any section (profile, links, search, privacy, notifications) independently. Requires authentication."
+ },
+ "response": []
+ }
+ ],
+ "description": "User settings endpoints for managing privacy, visibility, and notification preferences."
+ },
+ {
+ "name": "Search",
+ "item": [
+ {
+ "name": "Search Users",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"query\": \"test\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/search/users",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "search",
+ "users"
+ ]
+ },
+ "description": "Search for users by username or name. Only returns users who have isPublic=true and allowSearch=true in their settings. No authentication required. Returns up to 20 results."
+ },
+ "response": []
+ }
+ ],
+ "description": "Search endpoints for finding users."
+ },
+ {
+ "name": "Public Routes",
+ "item": [
+ {
+ "name": "Get User Link Tree",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/:username",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ ":username"
+ ],
+ "variable": [
+ {
+ "key": "username",
+ "value": "testuser",
+ "description": "Username to get link tree for"
+ }
+ ]
+ },
+ "description": "Get user's link tree page. Renders EJS template with all user links. Sends visit notification email. No authentication required."
+ },
+ "response": []
+ },
+ {
+ "name": "Redirect to Source",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{base_url}}/:username/:source",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ ":username",
+ ":source"
+ ],
+ "variable": [
+ {
+ "key": "username",
+ "value": "testuser",
+ "description": "Username"
+ },
+ {
+ "key": "source",
+ "value": "linkedin",
+ "description": "Source/platform name"
+ }
+ ]
+ },
+ "description": "Redirect to destination URL for a specific source. Increments click and notSeen counters. Sends visit notification email. No authentication required."
+ },
+ "response": []
+ }
+ ],
+ "description": "Public routes that don't require authentication. Used for sharing and accessing user link trees."
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ],
+ "variable": [
+ {
+ "key": "base_url",
+ "value": "http://localhost:8080",
+ "type": "string"
+ },
+ {
+ "key": "token",
+ "value": "",
+ "type": "string",
+ "description": "Authentication token from signin (stored in cookies)"
+ },
+ {
+ "key": "username",
+ "value": "",
+ "type": "string"
+ },
+ {
+ "key": "email",
+ "value": "",
+ "type": "string"
+ },
+ {
+ "key": "userId",
+ "value": "",
+ "type": "string"
+ }
+ ]
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinkBridger_API_Environment.postman_environment.json b/LinkBridger_API_Environment.postman_environment.json
new file mode 100644
index 0000000..05bf5a8
--- /dev/null
+++ b/LinkBridger_API_Environment.postman_environment.json
@@ -0,0 +1,48 @@
+{
+ "id": "All in1 url-env",
+ "name": "All in1 url API Environment",
+ "values": [
+ {
+ "key": "base_url",
+ "value": "http://localhost:8080",
+ "type": "default",
+ "enabled": true
+ },
+ {
+ "key": "token",
+ "value": "",
+ "type": "secret",
+ "enabled": true
+ },
+ {
+ "key": "username",
+ "value": "",
+ "type": "default",
+ "enabled": true
+ },
+ {
+ "key": "email",
+ "value": "",
+ "type": "default",
+ "enabled": true
+ },
+ {
+ "key": "userId",
+ "value": "",
+ "type": "default",
+ "enabled": true
+ }
+ ],
+ "_postman_variable_scope": "environment"
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NAMECHEAP_DNS_SETUP.md b/NAMECHEAP_DNS_SETUP.md
new file mode 100644
index 0000000..7c9a811
--- /dev/null
+++ b/NAMECHEAP_DNS_SETUP.md
@@ -0,0 +1,156 @@
+# Namecheap DNS TXT Record Setup - Step by Step
+
+## Exact Steps for Namecheap
+
+### Step 1: Log into Namecheap
+1. Go to https://www.namecheap.com
+2. Log in to your account
+3. Click on **Domain List** from the left sidebar
+
+### Step 2: Select Your Domain
+1. Find `allin1url.in` in your domain list
+2. Click the **Manage** button next to it
+
+### Step 3: Go to Advanced DNS
+1. You'll see several tabs at the top
+2. Click on the **Advanced DNS** tab
+3. Scroll down to the **Host Records** section
+
+### Step 4: Add TXT Record
+1. Click the **Add New Record** button
+2. A new row will appear
+
+### Step 5: Fill in the Record Details
+
+**In the new record row, fill in exactly:**
+
+| Field | Value |
+|-------|-------|
+| **Type** | Select **TXT Record** from dropdown |
+| **Host** | `_acme-challenge` |
+| **Value** | `xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ` |
+| **TTL** | **Automatic** (or select 300 if Automatic is not available) |
+
+**Important Notes:**
+- ✅ **Host field**: Enter exactly `_acme-challenge` (without quotes, no spaces)
+- ✅ **Value field**: Enter exactly `xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ` (no quotes, no spaces)
+- ✅ Namecheap will automatically add `.allin1url.in` to the host name
+- ✅ So `_acme-challenge` becomes `_acme-challenge.allin1url.in` automatically
+
+### Step 6: Save the Record
+1. Click the **Save All Changes** button (green checkmark icon) at the top right
+2. You should see a confirmation message
+
+### Step 7: Verify the Record
+1. After saving, scroll down to see your records
+2. Look for a TXT record that shows:
+ - **Type**: TXT Record
+ - **Host**: `_acme-challenge.allin1url.in` (Namecheap shows the full name)
+ - **Value**: `xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ`
+
+**If you see this record, it's added correctly!**
+
+## Visual Guide (What You Should See)
+
+### Before Adding:
+```
+Host Records:
+[Type] [Host] [Value] [TTL]
+A @ 123.45.67.89 Automatic
+A www 123.45.67.89 Automatic
+```
+
+### After Adding:
+```
+Host Records:
+[Type] [Host] [Value] [TTL]
+A @ 123.45.67.89 Automatic
+A www 123.45.67.89 Automatic
+TXT _acme-challenge.allin1url.in xgPUGNyXbaKrC1ZSQR57... Automatic
+```
+
+## Common Mistakes to Avoid
+
+❌ **DON'T enter**: `_acme-challenge.allin1url.in` in the Host field
+- Namecheap will create: `_acme-challenge.allin1url.in.allin1url.in` (WRONG!)
+
+✅ **DO enter**: `_acme-challenge` in the Host field
+- Namecheap creates: `_acme-challenge.allin1url.in` (CORRECT!)
+
+❌ **DON'T add quotes** around the value
+- Wrong: `"xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ"`
+
+✅ **DO enter** the value without quotes
+- Correct: `xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ`
+
+## Wait for DNS Propagation
+
+After saving:
+1. **Wait 2-5 minutes** for DNS to propagate
+2. Don't press Enter in certbot yet!
+
+## Verify DNS is Ready
+
+**On your EC2 instance, run:**
+
+```bash
+dig _acme-challenge.allin1url.in TXT +short
+```
+
+**Expected output:**
+```
+"xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ"
+```
+
+**Or use the verification script:**
+```bash
+./verify-dns.sh
+```
+
+**If you see the value above, DNS is ready!** ✅
+
+## Continue with Certbot
+
+Once DNS is verified:
+1. Go back to your EC2 terminal where certbot is waiting
+2. Press **Enter** to continue
+3. Certbot should now successfully verify the DNS record
+
+## Troubleshooting
+
+### Issue: Record not showing in Namecheap
+- Make sure you clicked **Save All Changes**
+- Refresh the page and check again
+- Look in the TXT Records section
+
+### Issue: DNS not propagating
+- Wait 5-10 minutes (can take longer)
+- Try different DNS servers: `dig @8.8.8.8 _acme-challenge.allin1url.in TXT`
+- Check if the record shows correctly in Namecheap dashboard
+
+### Issue: Wrong host name created
+- If you see `_acme-challenge.allin1url.in.allin1url.in`, you entered the full name
+- Delete the record and add it again with just `_acme-challenge`
+
+## Quick Checklist
+
+- [ ] Logged into Namecheap
+- [ ] Went to Domain List → allin1url.in → Manage → Advanced DNS
+- [ ] Added new TXT record
+- [ ] Host: `_acme-challenge` (just this, no domain)
+- [ ] Value: `xgPUGNyXbaKrC1ZSQR57af9lVwZz0Jj4UgoWTFTgLVQ` (exact value)
+- [ ] TTL: Automatic
+- [ ] Clicked Save All Changes
+- [ ] Verified record shows as `_acme-challenge.allin1url.in` in dashboard
+- [ ] Waited 2-5 minutes
+- [ ] Verified with `dig` command
+- [ ] Pressed Enter in certbot
+
+## Still Having Issues?
+
+1. Double-check the record in Namecheap dashboard
+2. Make sure the Host field shows `_acme-challenge.allin1url.in` (not double domain)
+3. Verify the value matches exactly (no extra spaces)
+4. Wait longer (up to 10 minutes)
+5. Try the verification script: `./verify-dns.sh`
+
diff --git a/POSTMAN_COLLECTION_README.md b/POSTMAN_COLLECTION_README.md
new file mode 100644
index 0000000..0703576
--- /dev/null
+++ b/POSTMAN_COLLECTION_README.md
@@ -0,0 +1,249 @@
+# All in1 url API Postman Collection
+
+This directory contains the Postman collection and environment files for testing all All in1 url APIs.
+
+## Files
+
+- `LinkBridger_API_Collection.postman_collection.json` - Complete API collection with all endpoints
+- `LinkBridger_API_Environment.postman_environment.json` - Environment variables for easy testing
+
+## Setup Instructions
+
+### 1. Import Collection and Environment
+
+1. Open Postman
+2. Click **Import** button (top left)
+3. Select both JSON files:
+ - `LinkBridger_API_Collection.postman_collection.json`
+ - `LinkBridger_API_Environment.postman_environment.json`
+4. Click **Import**
+
+### 2. Configure Environment
+
+1. Select **"All in1 url API Environment"** from the environment dropdown (top right)
+2. Update `base_url` if your backend is running on a different port:
+ - Default: `http://localhost:8080`
+ - Production: `https://your-production-url.com`
+
+### 3. Testing Authentication Flow
+
+1. **Sign Up Flow:**
+ - Run `Send OTP (Signup)` with your email and desired username
+ - Check your email for OTP
+ - Run `Verify Account (Complete Signup)` with email, username, password, and OTP
+
+2. **Sign In:**
+ - Run `Sign In` with your email and password
+ - The token will be automatically stored in cookies (Postman handles this)
+ - For manual token usage, copy the token from cookies and set it in the `token` environment variable
+
+3. **Using Authenticated Endpoints:**
+ - After signing in, all authenticated endpoints will use the cookie automatically
+ - If cookies don't work, manually set the `token` variable and use it in the Cookie header
+
+## API Endpoints Overview
+
+### Authentication (`/auth`)
+- `POST /signup` - Send OTP for signup
+- `POST /verifyacc` - Complete signup with OTP verification
+- `POST /signin` - Sign in with email/password
+- `POST /verify` - Verify token and get user info
+- `POST /checkavailablity` - Check username availability
+- `GET /signout` - Sign out
+- `POST /password_reset` - Send OTP for password reset
+- `POST /validate_otp` - Validate OTP and change password
+
+### Links (`/source`)
+- `POST /addnewsource` - Add new link/source (requires auth)
+- `POST /getallsource` - Get all user links (requires auth)
+- `POST /editlink` - Edit existing link (requires auth)
+- `POST /deletelink` - Delete link (requires auth)
+- `POST /notifications` - Reset notification counts (requires auth)
+- `POST /updatevisibility` - Update link visibility (requires auth)
+
+### Profile (`/profile`)
+- `POST /update` - Update profile information
+- `POST /getprofileinfo` - Get profile by username
+- `POST /updatepic` - Update profile picture (base64 image)
+- `POST /getpublicprofile` - Get public profile (respects privacy settings)
+
+### Settings (`/settings`)
+- `POST /get` - Get user settings (requires auth)
+- `POST /update` - Update user settings (requires auth)
+
+### Search (`/search`)
+- `POST /users` - Search for users (public, no auth required)
+
+### Public Routes
+- `GET /:username` - Get user's link tree page
+- `GET /:username/:source` - Redirect to source destination
+
+## Adding New APIs
+
+When you add new API endpoints, follow these steps to update the collection:
+
+### Step 1: Add to Collection JSON
+
+1. Open `LinkBridger_API_Collection.postman_collection.json`
+2. Find the appropriate folder (or create a new one)
+3. Add a new request object following this structure:
+
+```json
+{
+ "name": "Your API Name",
+ "request": {
+ "method": "POST", // or GET, PUT, DELETE, etc.
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Cookie",
+ "value": "token={{token}}", // if auth required
+ "description": "Token from signin cookie"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"field1\": \"value1\",\n \"field2\": \"value2\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/your/route/path",
+ "host": ["{{base_url}}"],
+ "path": ["your", "route", "path"]
+ },
+ "description": "Description of what this API does"
+ },
+ "response": []
+}
+```
+
+### Step 2: Place in Correct Folder
+
+- **Authentication** → Add to `"Authentication"` folder
+- **Links** → Add to `"Links"` folder
+- **Profile** → Add to `"Profile"` folder
+- **Settings** → Add to `"Settings"` folder
+- **Search** → Add to `"Search"` folder
+- **New Category** → Create new folder in the `"item"` array
+
+### Step 3: Add Environment Variables (if needed)
+
+If your new API needs new environment variables:
+
+1. Open `LinkBridger_API_Environment.postman_environment.json`
+2. Add to the `"values"` array:
+
+```json
+{
+ "key": "your_variable_name",
+ "value": "default_value",
+ "type": "default", // or "secret" for sensitive data
+ "enabled": true
+}
+```
+
+3. Also add to the collection's `"variable"` array in the collection file
+
+### Step 4: Test and Update
+
+1. Import the updated collection into Postman
+2. Test the new endpoint
+3. Update the description if needed
+4. Add example responses if helpful
+
+## Tips
+
+1. **Cookie Handling**: Postman automatically handles cookies from responses. If you need to manually set a token, use the `token` environment variable in the Cookie header.
+
+2. **Base64 Images**: For profile picture updates, convert your image to base64 and use the format: `data:image/png;base64,YOUR_BASE64_STRING`
+
+3. **Environment Variables**: Use `{{variable_name}}` syntax in URLs and request bodies to reference environment variables.
+
+4. **Testing Flow**:
+ - Always start with authentication endpoints
+ - Store credentials in environment variables
+ - Use the stored variables in subsequent requests
+
+5. **Error Handling**: Check response status codes:
+ - `200` - Success
+ - `201` - Created
+ - `400` - Bad Request
+ - `401` - Unauthorized
+ - `404` - Not Found
+ - `409` - Conflict
+ - `500` - Server Error
+
+## Collection Structure
+
+```
+All in1 url API Collection
+├── Authentication
+│ ├── Send OTP (Signup)
+│ ├── Verify Account (Complete Signup)
+│ ├── Sign In
+│ ├── Verify Token (Get User Info)
+│ ├── Check Username Availability
+│ ├── Sign Out
+│ ├── Password Reset - Send OTP
+│ └── Password Reset - Validate OTP & Change Password
+├── Links
+│ ├── Add New Source
+│ ├── Get All Sources
+│ ├── Edit Link
+│ ├── Delete Link
+│ ├── Set Notifications to Zero
+│ └── Update Link Visibility
+├── Profile
+│ ├── Update Profile
+│ ├── Get Profile Info
+│ ├── Update Profile Picture
+│ └── Get Public Profile
+├── Settings
+│ ├── Get Settings
+│ └── Update Settings
+├── Search
+│ └── Search Users
+└── Public Routes
+ ├── Get User Link Tree
+ └── Redirect to Source
+```
+
+## Notes
+
+- All authenticated endpoints require a valid token in cookies
+- The token is automatically set when you sign in (Postman handles cookies)
+- For production testing, update the `base_url` environment variable
+- Some endpoints may have different behaviors based on user settings (privacy, visibility, etc.)
+
+## Troubleshooting
+
+**Token not working:**
+- Make sure you've signed in first
+- Check that cookies are enabled in Postman settings
+- Manually copy token from cookies and set in environment variable
+
+**CORS errors:**
+- Make sure your backend CORS settings allow your Postman origin
+- Check the allowed origins in `backend/index.js`
+
+**404 errors:**
+- Verify the `base_url` is correct
+- Check that the backend server is running
+- Ensure the route path matches exactly
+
+---
+
+**Last Updated**: This collection includes all APIs as of the current codebase. Update this file when adding new endpoints.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 77fe97d..86e9254 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# 🌉 LinkBridger
+# 🌉 All in1 url
### **Personalized Social Profile Link Manager**
@@ -10,13 +10,13 @@
[](https://www.mongodb.com/)
[](https://www.typescriptlang.org/)
[](CONTRIBUTING.md)
-[](https://github.com/DpkRn/LinkBridger)
+[](https://github.com/DpkRn/All in1 url)
**Transform your social media presence with memorable, personalized links that never expire**
-[🚀 Live Demo](https://linkb-one.vercel.app) • [📖 Documentation](./frontend/src/components/Documentation.jsx) • [🐛 Report Bug](https://github.com/DpkRn/LinkBridger/issues) • [💡 Request Feature](https://github.com/DpkRn/LinkBridger/issues) • [💬 Discuss](https://github.com/DpkRn/LinkBridger/discussions)
+[🚀 Live Demo](https://allin1url.in) • [📖 Documentation](./frontend/src/components/Documentation.jsx) • [🐛 Report Bug](https://github.com/DpkRn/All in1 url/issues) • [💡 Request Feature](https://github.com/DpkRn/All in1 url/issues) • [💬 Discuss](https://github.com/DpkRn/All in1 url/discussions)
-
+
@@ -25,9 +25,9 @@
## 📋 Table of Contents
- [About The Project](#-about-the-project)
-- [Why LinkBridger?](#-why-linkbridger)
+- [Why All in1 url?](#-why-All in1 url)
- [Key Benefits](#-key-benefits)
-- [LinkBridger vs. Competitors](#-linkbridger-vs-competitors)
+- [All in1 url vs. Competitors](#-All in1 url-vs-competitors)
- [Live Examples](#-live-examples)
- [Features](#-features)
- [Tech Stack](#-tech-stack)
@@ -41,11 +41,11 @@
## 🎯 About The Project
-**LinkBridger** is a revolutionary, open-source social profile link management platform that empowers users to create personalized, memorable URLs for all their social media profiles. Unlike traditional link shorteners that generate random, forgettable codes, LinkBridger uses your username and platform name to create links that are both human-readable and professional.
+**All in1 url** is a revolutionary, open-source social profile link management platform that empowers users to create personalized, memorable URLs for all their social media profiles. Unlike traditional link shorteners that generate random, forgettable codes, All in1 url uses your username and platform name to create links that are both human-readable and professional.
### 🎨 The Vision
-In today's digital-first world, professionals, creators, and developers manage multiple social media profiles across various platforms. LinkBridger was born from the need to simplify this complexity and provide a unified solution that combines **memorability**, **professionalism**, and **functionality** in one powerful platform.
+In today's digital-first world, professionals, creators, and developers manage multiple social media profiles across various platforms. All in1 url was born from the need to simplify this complexity and provide a unified solution that combines **memorability**, **professionalism**, and **functionality** in one powerful platform.
### 🔍 The Problem It Solves
@@ -61,29 +61,60 @@ In today's digital-first world, professionals, creators, and developers manage m
### ✨ The Solution
-LinkBridger bridges this gap by providing:
+All in1 url bridges this gap by providing:
-- 🎯 **Personalized URLs** using your username and platform name (e.g., `linkb-one.vercel.app/yourname/linkedin`)
+- 🎯 **Personalized URLs** using your username and platform name (e.g., `yourname.allin1url.in/linkedin`)
- 🌐 **Single Hub Link** that acts as a landing page for all your profiles
- 📊 **Built-in Analytics** to track clicks and understand your audience
- 🔄 **Centralized Updates** that reflect instantly across all platforms
- 🚀 **Zero Expiration** - your links work forever
- 🎨 **Brand Identity** - links that reflect your personal or professional brand
- 🔒 **Privacy First** - no tracking scripts, no third-party analytics
+- 🔐 **Link Privacy Controls** - three-tier visibility system:
+ - **Public**: Visible everywhere (link hub, profile preview, search)
+ - **Unlisted**: Visible in profile preview only, NOT in link hub (perfect for 100+ links without cluttering)
+ - **Private**: Hidden everywhere, password-protected for direct access
+- 👥 **User Discovery** - real-time search in navigation bar to discover other users' public profiles
+- ⚙️ **Granular Privacy Settings** - fully customizable visibility controls:
+ - Profile visibility (public/private)
+ - Search visibility and keywords
+ - Content visibility (email, location, bio, passion, image)
+ - Link display settings
+ - Privacy and notification preferences
- 💰 **Completely Free** - open source and free forever
### 🏗️ How It Works
1. **Sign Up**: Create an account with your email and choose a username
2. **Add Platforms**: Add any social media platform with its destination URL
-3. **Get Your Links**: Receive personalized links like `linkb-one.vercel.app/username/platform`
-4. **Share Everywhere**: Use your links on resumes, business cards, email signatures, and social media
-5. **Update Anytime**: Change destination URLs anytime - all your shared links update automatically
-6. **Track Performance**: Monitor clicks and engagement through the dashboard
+3. **Get Your Links**: Receive personalized links like `username.allin1url.in/platform`
+4. **Set Link Privacy**: Choose visibility for each link:
+ - **Public**: Visible everywhere (link hub, profile preview, search)
+ - **Unlisted**: Visible in profile preview only, NOT in link hub (perfect for 100+ links without cluttering hub)
+ - **Private**: Hidden everywhere, password-protected for direct access
+5. **Configure Privacy Settings**: Customize what information is visible in your public profile:
+ - Toggle profile visibility (public/private)
+ - Enable/disable search visibility
+ - Control content visibility (email, location, bio, passion, image)
+ - Configure link display settings
+ - Set up customizable email notification preferences
+ - Link click notifications (toggle on/off)
+ - Profile view notifications (toggle on/off)
+ - Weekly report emails (toggle on/off)
+6. **Share Everywhere**: Use your links on resumes, business cards, email signatures, and social media
+7. **Update Anytime**: Change destination URLs anytime - all your shared links update automatically
+8. **Track Performance**: Monitor clicks and engagement through the dashboard
+9. **Discover Users**: Search for other users in the navigation bar and view their public profiles
+10. **Customize Notifications**: Set up email notification preferences in Settings
+ - Enable/disable link click notifications
+ - Enable/disable profile view notifications
+ - Enable/disable weekly report emails
+ - Each notification type can be controlled independently
+11. **Get Notified**: Receive email notifications based on your preferences when someone clicks your links or views your profile
---
-## 💡 Why LinkBridger?
+## 💡 Why All in1 url?
### 👔 For Professionals
@@ -127,13 +158,26 @@ LinkBridger bridges this gap by providing:
### 1. **Memorable & Professional Links** 🎯
**Before**: `https://www.linkedin.com/in/john-doe-software-engineer-123456789/`
-**After**: `https://linkb-one.vercel.app/johndoe/linkedin`
+**After**: `https://johndoe.allin1url.in/linkedin`
-Your audience will remember it! The pattern is simple: `domain/username/platform`. Once someone knows your username, they can easily guess your other platform links.
+Your audience will remember it! The pattern is simple: `username.domain/platform`. Once someone knows your username, they can easily guess your other platform links.
+
+**🌟 New: Custom Subdomain Format**
+- Each user gets their own custom subdomain (e.g., `username.allin1url.in`)
+- Hub link: `https://username.allin1url.in` (shows all your profiles)
+- Platform links: `https://username.allin1url.in/platform` (direct redirects)
+- More professional and memorable than path-based URLs
+- Works seamlessly in both development and production environments
### 2. **Single Hub for All Profiles** 🌐
-Share one link (`https://linkb-one.vercel.app/yourname`) that acts as a beautiful landing page for all your social profiles. Visitors can browse and choose which platform to visit, creating a professional digital business card.
+Share one link (`https://yourname.allin1url.in`) that acts as a beautiful landing page for all your social profiles. Visitors can browse and choose which platform to visit, creating a professional digital business card.
+
+**🌟 Custom Subdomain Hub**
+- Your personal subdomain: `https://yourname.allin1url.in`
+- Beautiful landing page showcasing all your profiles
+- Easy to remember and share
+- Professional digital business card
**Benefits:**
- One link to remember instead of dozens
@@ -156,17 +200,131 @@ Imagine you've shared your LinkedIn profile link in:
**The Problem**: Your LinkedIn account gets banned or you change your username. Now you need to update links in all these places - but you don't even remember where you shared them!
-**The LinkBridger Solution**: Update the destination URL once in your LinkBridger dashboard, and **all your shared links automatically redirect to the new URL**. No more hunting down old links!
-
-### 4. **Click Analytics & Insights** 📊
+**The All in1 url Solution**: Update the destination URL once in your All in1 url dashboard, and **all your shared links automatically redirect to the new URL**. No more hunting down old links!
+
+### 4. **Advanced Analytics & Insights** 📊
+
+Comprehensive analytics dashboard to understand your audience and optimize your strategy:
+
+- **Multiple Metrics**: Track profile visits, clicks, location, devices, browsers, OS, referrers, and more
+- **Visual Analytics**: Multiple chart types (Line, Bar, Area, Pie) for different data views
+- **Time-Based Analysis**: Analyze trends over 7 days, 30 days, 90 days, 1 year, or all time
+- **Geographic Insights**: Country-level location distribution with visual breakdowns
+- **Device Analytics**: Complete breakdown of Desktop, Mobile, and Tablet usage
+- **Browser Analytics**: Track which browsers your audience uses (Chrome, Safari, Firefox, Edge, etc.)
+- **Operating System Analytics**: Understand OS distribution (Windows, macOS, Linux, iOS, Android)
+- **Referrer Analytics**: Categorized referrer tracking (Direct, Search, Social, Internal, External)
+- **Top Referrer Sources**: Domain-level referrer tracking with detailed insights
+- **Temporal Patterns**: Hourly distribution and day-of-week analysis
+- **Platform Performance**: Individual platform click metrics
+- **Link Performance**: Per-link analytics and statistics
+- **Summary Statistics**: Quick overview cards with key metrics
+- **Real-time Updates**: See clicks and analytics as they happen
+- **Customizable Email Notifications**: Get notified based on your preferences
+ - Link click notifications (customizable - toggle on/off)
+ - Profile view notifications (customizable - toggle on/off)
+ - Weekly report emails (customizable - toggle on/off)
+ - Each notification type includes detailed information (platform, device, location, timestamp)
+
+### 4.5. **Link Privacy & Protection** 🔒
+
+Control who can access your links with three visibility levels:
+
+- **Public Links**: Fully visible and accessible
+ - ✅ Visible in your profile preview
+ - ✅ Visible in link hub (`/username`)
+ - ✅ Visible in user search results
+ - ✅ Accessible via direct URL (`/username/platform`)
+ - Anyone can see and access these links
+ - Perfect for professional profiles and public content
+ - No password required
+
+- **Unlisted Links**: Hidden from hub but visible in profile preview
+ - ❌ **NOT** visible in link hub (`/username`)
+ - ✅ **Visible** in profile preview (`/profile/username`)
+ - ❌ **NOT** visible in user search results
+ - ✅ Accessible via direct URL (`/username/platform`) - **NO password required for direct access**
+ - **Purpose**: Allows you to preserve many links (up to 100+) in your profile preview without cluttering the public link hub
+ - Perfect for links that are less important but you still want visible in your profile
+ - Ideal for maintaining a comprehensive profile while keeping the public hub clean and focused
+ - Direct access works without password - anyone with the direct URL can access it
+
+- **Private Links**: Completely hidden - require password to access
+ - ❌ **NOT** visible in profile preview
+ - ❌ **NOT** visible in link hub
+ - ❌ **NOT** visible in user search results
+ - ✅ Only accessible via direct URL (`/username/platform`) with correct password
+ - Perfect for sensitive or personal content
+ - Secure password prompt page with validation
+ - Automatic redirect to destination after successful verification
+
+**How It Works:**
+
+**For Link Visibility:**
+1. **Setting Link Visibility**: Use the lock icon on each link card to change visibility
+2. **Public Links**: Immediately visible in link hub (`/username`), profile preview (`/profile/username`), and search results
+3. **Unlisted Links**:
+ - ✅ Visible in profile preview (`/profile/username`)
+ - ❌ NOT visible in link hub (`/username`) - keeps hub clean and focused
+ - ✅ Accessible via direct URL (`/username/platform`) - NO password required
+ - **Purpose**: Perfect for preserving 100+ links in your profile preview without cluttering the public link hub
+ - Ideal for less important links you still want visible in your profile
+4. **Private Links**: When setting a link to "private", you'll be prompted to set a password
+5. **Accessing Private Links**: When someone visits a private link directly, they see a password prompt page
+6. **Password Verification**: The system securely verifies the password using bcrypt hashing
+7. **Automatic Redirect**: After successful verification, users are automatically redirected to the destination URL
+8. **Click Tracking**: All link clicks (including private links) are tracked and can be reported via email notifications (if enabled in your notification preferences)
+
+**For Privacy & Permissions (Fully Customizable):**
+1. **Access Settings**: Navigate to Settings page from your profile menu
+2. **Profile Visibility**: Toggle whether your profile is public or private
+ - Public: Visible to everyone, can be found in search
+ - Private: Only accessible if you share the direct link
+3. **Search Settings**: Control discoverability
+ - Enable/disable profile searchability
+ - Add search keywords for better discoverability
+ - Featured profile option for enhanced visibility
+4. **Content Visibility**: Individually toggle what information is visible
+ - Show/hide email address
+ - Show/hide location
+ - Show/hide bio
+ - Show/hide passion
+ - Show/hide profile image
+ - Each setting works independently
+5. **Link Display Settings**: Control what statistics are shown
+ - Show/hide total link count
+ - Show/hide click statistics
+6. **Privacy Controls**: Configure advanced privacy options
+ - Show/hide analytics information
+ - Show/hide last updated timestamp
+ - Require authentication for profile view
+7. **Email Notifications**: Fully customizable email notification preferences
+ - **Link Click Notifications**: Toggle email notifications when someone clicks your links
+ - Includes platform, device, location, and timestamp information
+ - Perfect for tracking engagement and recruiter interest
+ - Can be enabled/disabled independently
+ - **Profile View Notifications**: Toggle email notifications when someone views your public profile
+ - Know when potential clients or employers check out your profile
+ - Helps you understand profile visibility and interest
+ - Can be enabled/disabled independently
+ - **Weekly Reports**: Toggle weekly summary emails
+ - Get insights into your most popular links
+ - Track overall engagement trends
+ - Monitor profile performance over time
+ - Can be enabled/disabled independently
+ - All notification types can be controlled independently in Settings
+ - Changes take effect immediately
+ - No premium subscription required - all notification types are free
+8. **Real-time Updates**: All changes take effect immediately and are reflected in profile preview
+9. **Profile Preview**: Use the "Preview" button to see exactly how your profile appears to visitors with current privacy settings
-Track which platforms get the most clicks to understand your audience:
-
-- **See Engagement**: Know which platforms drive the most traffic
-- **Optimize Strategy**: Focus on platforms that get the most engagement
-- **Measure Impact**: Track the effectiveness of your networking efforts
-- **Real-time Updates**: See clicks as they happen
-- **Email Notifications**: Get notified when someone clicks your links (with device and location info)
+**Security Features:**
+- **Password Protection**: Secure password verification with bcrypt hashing
+- **Direct Redirection**: After password verification, users are automatically redirected to the destination
+- **Secure Encoding**: Username and source are encoded (Base64) for security when passing through password prompt
+- **Error Handling**: User-friendly error messages for incorrect passwords
+- **Click Tracking**: Private link clicks are still tracked and can be reported via email notifications (if enabled in your notification preferences)
+- **No URL Tampering**: Encoded parameters prevent unauthorized access attempts
### 5. **Platform Flexibility** 🎨
@@ -187,23 +345,107 @@ Unlike many link shorteners that:
- Delete inactive links
- Limit the number of links
-**LinkBridger links work forever** - as long as you maintain your account, your links remain active. No expiration dates, no premium plans, no limits.
+**All in1 url links work forever** - as long as you maintain your account, your links remain active. No expiration dates, no premium plans, no limits.
-### 7. **Email Notifications** 📧
+### 7. **Customizable Email Notifications** 📧
-Get notified when someone clicks your links with:
+Get notified about important events based on your customizable preferences:
+
+**Notification Types (All Customizable):**
+- **Link Click Notifications**: Get notified when someone clicks your links
+ - Toggle on/off in Settings
+ - Includes platform information, device details, location data, and timestamp
+ - Perfect for tracking recruiter interest and engagement
+- **Profile View Notifications**: Get notified when someone views your public profile
+ - Toggle on/off in Settings
+ - Know when potential clients or employers check out your profile
+- **Weekly Reports**: Receive weekly summaries of your link performance
+ - Toggle on/off in Settings
+ - Get insights into your most popular links and overall engagement
+
+**What's Included in Notifications:**
- **Platform Information**: Which link was clicked
- **Device Details**: Desktop, mobile, or tablet
- **Location Data**: General location information (privacy-respecting)
-- **Timestamp**: When the click occurred
-
-Perfect for:
+- **Timestamp**: When the click or view occurred
+- **User Information**: Basic details about the visitor (when available)
+
+**Customization (Based on Your Permissions):**
+- **Independent Control**: Control each notification type independently
+ - Link click notifications: Enable/disable in Settings
+ - Profile view notifications: Enable/disable in Settings
+ - Weekly report emails: Enable/disable in Settings
+- **Permission-Based**: Email notifications are sent based on your notification preferences
+ - Only enabled notification types will trigger emails
+ - You have full control over what notifications you receive
+ - No emails are sent for disabled notification types
+- **Easy Management**: All settings can be changed anytime in the Settings page
+ - Navigate to Settings from your profile menu
+ - Toggle each notification type on/off as needed
+ - Changes take effect immediately
+ - No premium subscription required - all notification customization is free
+
+**Perfect for:**
- Tracking recruiter interest
- Understanding audience behavior
- Measuring engagement
- Staying informed about your digital presence
-
-### 8. **Dark Mode Support** 🌓
+- Monitoring profile visibility
+- Getting weekly performance insights
+
+### 8. **User Search & Public Profiles** 👥
+
+Discover and connect with other users:
+
+- **User Search**: Search for other users by username or name in the navigation bar
+ - Real-time search with instant results
+ - Search dropdown with user profiles
+ - Click to visit any public profile
+ - Responsive design: search icon on mobile, full input on desktop
+ - Only shows users who have enabled public profile and search visibility
+
+- **Public Profile Viewing**: View other users' public profiles
+ - Access via `/profile/:username` route
+ - Shows only public links (filters out private links)
+ - Respects all privacy settings from the profile owner
+ - Displays profile information based on owner's permissions
+
+- **Profile Preview**: Preview how your own profile appears to visitors
+ - Click "Preview" button on your profile page
+ - See exactly what visitors see
+ - All content respects your privacy settings, even for owners
+ - Helps you fine-tune your public profile appearance
+
+- **Privacy Controls**: Granular settings for what information is visible
+ - Control profile visibility (public/private)
+ - Toggle search visibility (allow/disallow search)
+ - Control profile view permissions
+ - Fine-tune what information is shown (email, location, bio, passion, profile image)
+ - Link display settings (show link count, show click stats)
+ - Search & discovery settings (allow search, featured, search keywords)
+ - Privacy settings (show analytics, show last updated, require auth)
+ - **Customizable Email Notifications**: Fully customizable notification preferences
+ - Link click notifications (toggle on/off)
+ - Profile view notifications (toggle on/off)
+ - Weekly report emails (toggle on/off)
+ - Each notification type can be controlled independently
+ - All settings accessible in Settings page
+
+- **Search Visibility**: Control whether your profile appears in search results
+ - Enable/disable profile searchability
+ - Add search keywords for better discoverability
+ - Featured profile option for enhanced visibility
+ - Only searchable if profile is public AND search is enabled
+
+- **Profile Permissions**: Fine-tune visibility of email, location, bio, and more
+ - Show/hide email address
+ - Show/hide location
+ - Show/hide bio
+ - Show/hide passion
+ - Show/hide profile image
+ - Each setting can be toggled independently
+
+### 9. **Dark Mode Support** 🌓
Modern, eye-friendly interface with full dark mode support:
- **System Preference Detection**: Automatically matches your system theme
@@ -212,7 +454,7 @@ Modern, eye-friendly interface with full dark mode support:
- **Smooth Transitions**: Beautiful animations when switching themes
- **Accessibility**: Better for low-light environments and reducing eye strain
-### 9. **Mobile Responsive Design** 📱
+### 10. **Mobile Responsive Design** 📱
Fully responsive design that works seamlessly on:
- **Desktop**: Full-featured experience with all capabilities
@@ -220,7 +462,7 @@ Fully responsive design that works seamlessly on:
- **Mobile**: Touch-friendly interface for smartphones
- **All Browsers**: Works on Chrome, Firefox, Safari, Edge, and more
-### 10. **Privacy & Security** 🔒
+### 11. **Privacy & Security** 🔒
**Security Features:**
- **JWT Authentication**: Secure token-based authentication
@@ -237,8 +479,12 @@ Fully responsive design that works seamlessly on:
- **No Data Selling**: Your data is yours - we don't sell it
- **Open Source**: Transparent code you can audit
- **User Control**: You control your data and links
+- **Link Privacy Controls**: Three-tier visibility system (public, unlisted, private)
+- **Password Protection**: Secure password protection for private links
+- **Profile Privacy Settings**: Granular control over what information is visible
+- **Search Visibility**: Control whether your profile appears in user searches
-### 11. **Fast Performance** ⚡
+### 12. **Fast Performance** ⚡
- **Optimized Queries**: Efficient database operations
- **Lazy Loading**: Code splitting and lazy imports
@@ -246,7 +492,7 @@ Fully responsive design that works seamlessly on:
- **CDN Ready**: Optimized for content delivery networks
- **Lightweight**: Minimal dependencies and optimized bundle size
-### 12. **User-Friendly Interface** 🎨
+### 13. **User-Friendly Interface** 🎨
- **Intuitive Design**: Easy to use, even for non-technical users
- **Real-time Validation**: Instant feedback on form inputs
@@ -255,7 +501,7 @@ Fully responsive design that works seamlessly on:
- **Success Notifications**: Clear confirmation of actions
- **Smooth Animations**: Polished user experience
-### 13. **Free & Open Source** 💰
+### 14. **Free & Open Source** 💰
- **Completely Free**: No subscription fees, no premium plans
- **Open Source**: Full source code available on GitHub
@@ -264,7 +510,7 @@ Fully responsive design that works seamlessly on:
- **Customizable**: Modify to fit your needs
- **No Vendor Lock-in**: You're not dependent on a single service
-### 14. **Easy Setup** 🚀
+### 15. **Easy Setup** 🚀
Getting started is simple:
1. Choose a short, memorable username
@@ -275,13 +521,13 @@ Getting started is simple:
---
-## 🆚 LinkBridger vs. Competitors
+## 🆚 All in1 url vs. Competitors
### Comparison Table
-| Feature | LinkBridger | Link Shorteners (bit.ly, tinyurl) | Linktree | Bio.link | Custom Domain Services |
+| Feature | All in1 url | Link Shorteners (bit.ly, tinyurl) | Linktree | Bio.link | Custom Domain Services |
|---------|------------|-----------------------------------|----------|----------|------------------------|
-| **Link Format** | `domain/username/platform` | `bit.ly/xyz123` | `linktr.ee/username` | `bio.link/username` | `custom.com/username` |
+| **Link Format** | `username.domain/platform` (🌟 Custom Subdomain) | `bit.ly/xyz123` | `linktr.ee/username` | `bio.link/username` | `custom.com/username` |
| **Memorability** | ✅ Human-readable, memorable | ❌ Random codes | ⚠️ Platform-dependent | ⚠️ Platform-dependent | ✅ Customizable |
| **Professionalism** | ✅ Branded, professional | ❌ Generic, unprofessional | ⚠️ Branded by platform | ⚠️ Branded by platform | ✅ Fully branded |
| **Expiration** | ✅ Never expires | ❌ Often expires | ✅ Usually permanent | ✅ Usually permanent | ✅ Permanent |
@@ -292,15 +538,20 @@ Getting started is simple:
| **Cost** | ✅ Free and open source | ⚠️ Often requires paid plans | ⚠️ Premium features locked | ⚠️ Premium features locked | ❌ Expensive |
| **Transparency** | ✅ Open source, auditable | ❌ Closed source | ❌ Closed source | ❌ Closed source | ⚠️ Varies |
| **No Vendor Lock-in** | ✅ Self-hostable | ❌ Vendor-dependent | ❌ Vendor-dependent | ❌ Vendor-dependent | ✅ Self-hostable |
-| **Email Notifications** | ✅ Built-in | ❌ Not available | ⚠️ Premium feature | ⚠️ Premium feature | ⚠️ Varies |
+| **Email Notifications** | ✅ Fully customizable | ❌ Not available | ⚠️ Premium feature | ⚠️ Premium feature | ⚠️ Varies |
+| **Notification Customization** | ✅ Per-type toggles | ❌ Not available | ⚠️ Limited | ⚠️ Limited | ⚠️ Varies |
| **Platform Flexibility** | ✅ Any platform | ✅ Any URL | ⚠️ Limited platforms | ⚠️ Limited platforms | ✅ Any platform |
| **Dark Mode** | ✅ Full support | ⚠️ Varies | ⚠️ Limited | ⚠️ Limited | ⚠️ Varies |
| **Mobile App** | ⚠️ Web-based (responsive) | ✅ Available | ✅ Available | ✅ Available | ⚠️ Varies |
+| **Link Privacy Controls** | ✅ Public/Unlisted/Private | ❌ Not available | ⚠️ Limited | ⚠️ Limited | ⚠️ Varies |
+| **Password Protection** | ✅ Built-in | ❌ Not available | ⚠️ Premium feature | ⚠️ Premium feature | ⚠️ Varies |
+| **User Search** | ✅ Built-in | ❌ Not available | ⚠️ Limited | ⚠️ Limited | ⚠️ Varies |
+| **Profile Privacy Settings** | ✅ Granular controls | ❌ Not available | ⚠️ Limited | ⚠️ Limited | ⚠️ Varies |
-### Why LinkBridger is Superior
+### Why All in1 url is Superior
#### 1. **Brand Identity** 🎯
-Your links become part of your brand identity, not generic shortened URLs. When someone sees `linkb-one.vercel.app/yourname/linkedin`, they immediately know it's your link and can easily remember the pattern for other platforms.
+Your links become part of your brand identity, not generic shortened URLs. When someone sees `yourname.allin1url.in/linkedin`, they immediately know it's your link and can easily remember the pattern for other platforms. The custom subdomain format (`username.allin1url.in`) makes your links even more professional and memorable.
#### 2. **User Trust** 🤝
Transparent, readable URLs build more trust than mysterious short codes. Users can see where the link will take them before clicking, reducing phishing concerns.
@@ -327,7 +578,7 @@ Built by developers, for developers:
- Transparent development process
#### 6. **Cost Effective** 💰
-- **LinkBridger**: Free forever, open source
+- **All in1 url**: Free forever, open source
- **Linktree Pro**: $6-24/month
- **Bio.link Pro**: $3-9/month
- **Custom Domain Services**: $10-50+/month + setup fees
@@ -349,24 +600,24 @@ Built by developers, for developers:
## 🎬 Live Examples
-See LinkBridger in action with these real-world examples:
+See All in1 url in action with these real-world examples:
### Example User: `dpkrn`
**Single Hub Link** (Access all profiles - acts as a digital business card):
```
-https://linkb-one.vercel.app/dpkrn
+https://dpkrn.allin1url.in
```
Visit this link to see a beautiful landing page with all social profiles!
**Individual Platform Links** (Direct redirects to specific platforms):
-- **LinkedIn**: [`https://linkb-one.vercel.app/dpkrn/linkedin`](https://linkb-one.vercel.app/dpkrn/linkedin)
-- **GitHub**: [`https://linkb-one.vercel.app/dpkrn/github`](https://linkb-one.vercel.app/dpkrn/github)
-- **LeetCode**: [`https://linkb-one.vercel.app/dpkrn/leetcode`](https://linkb-one.vercel.app/dpkrn/leetcode)
-- **Portfolio**: [`https://linkb-one.vercel.app/dpkrn/portfolio`](https://linkb-one.vercel.app/dpkrn/portfolio)
-- **Instagram**: [`https://linkb-one.vercel.app/dpkrn/instagram`](https://linkb-one.vercel.app/dpkrn/instagram)
-- **Facebook**: [`https://linkb-one.vercel.app/dpkrn/facebook`](https://linkb-one.vercel.app/dpkrn/facebook)
-- **Codeforces**: [`https://linkb-one.vercel.app/dpkrn/codeforces`](https://linkb-one.vercel.app/dpkrn/codeforces)
+- **LinkedIn**: [`https://dpkrn.allin1url.in/linkedin`](https://dpkrn.allin1url.in/linkedin)
+- **GitHub**: [`https://dpkrn.allin1url.in/github`](https://dpkrn.allin1url.in/github)
+- **LeetCode**: [`https://dpkrn.allin1url.in/leetcode`](https://dpkrn.allin1url.in/leetcode)
+- **Portfolio**: [`https://dpkrn.allin1url.in/portfolio`](https://dpkrn.allin1url.in/portfolio)
+- **Instagram**: [`https://dpkrn.allin1url.in/instagram`](https://dpkrn.allin1url.in/instagram)
+- **Facebook**: [`https://dpkrn.allin1url.in/facebook`](https://dpkrn.allin1url.in/facebook)
+- **Codeforces**: [`https://dpkrn.allin1url.in/codeforces`](https://dpkrn.allin1url.in/codeforces)
**Notice**: Only the platform name changes; the username remains consistent across all links! This makes it incredibly easy to remember and share.
@@ -386,11 +637,48 @@ Visit this link to see a beautiful landing page with all social profiles!
- 🔐 **Secure Authentication**: JWT-based authentication with email verification
- 👤 **User Profiles**: Customizable profile with bio and profile picture
- 🔗 **Link Management**: Create, edit, and delete social profile links
-- 📊 **Click Tracking**: Real-time analytics for each link with detailed statistics
-- 🔔 **Notifications**: Email notifications for link clicks with device and location information
+- 📊 **Advanced Analytics Dashboard**: Comprehensive analytics with detailed insights
+ - Click tracking with time-based analysis (daily, weekly, monthly, yearly)
+ - Geographic distribution with country-level data
+ - Device analytics (Desktop, Mobile, Tablet breakdown)
+ - Browser analytics (Chrome, Safari, Firefox, Edge, etc.)
+ - Operating system analytics (Windows, macOS, Linux, iOS, Android)
+ - Referrer analytics (Direct, Search, Social, Internal, External categories)
+ - Top referrer sources with domain-level tracking
+ - Hourly distribution patterns
+ - Day of week analysis
+ - Platform performance metrics
+ - Link-based analytics
+ - Multiple chart types (Line, Bar, Area, Pie charts)
+ - Customizable time ranges (7 days, 30 days, 90 days, 1 year, all time)
+ - Summary cards with key metrics (Total Clicks, Profile Visits, Countries, Top Referrer)
+ - Full dark and light theme support
+- 🔔 **Customizable Email Notifications**: Fully customizable email notifications based on your preferences
+ - Link click notifications (toggle on/off)
+ - Profile view notifications (toggle on/off)
+ - Weekly report emails (toggle on/off)
+ - Each notification type can be controlled independently
+- 🔒 **Link Privacy Controls**: Three-tier visibility system (public, unlisted, private) with password protection
+ - **Public**: Visible everywhere (link hub, profile preview, search)
+ - **Unlisted**: Visible in profile preview, NOT in link hub, direct access without password
+ - **Private**: Hidden everywhere, password-protected for direct access only
- 🌓 **Dark Mode**: Full dark mode support with system preference detection and manual toggle
- 📱 **Responsive Design**: Works perfectly on all devices and screen sizes
- 🎨 **Modern UI/UX**: Beautiful, intuitive interface built with React and Tailwind CSS
+ - Responsive text sizing across all pages for optimal mobile experience
+ - Full dark and light theme support with proper color contrast
+ - Mobile-optimized layouts with single-row URL inputs
+ - Smooth animations and transitions throughout
+- 👥 **User Search & Public Profiles**: Search for users and view public profiles
+ - Real-time user search in navigation bar
+ - Public profile viewing with privacy-respecting content
+ - Profile preview to see how your profile appears to visitors
+- ⚙️ **Granular Privacy Settings**: Comprehensive privacy and visibility controls
+ - Profile visibility controls (8+ toggle options)
+ - Link display settings
+ - Search & discovery settings
+ - Privacy and notification preferences
+ - Customizable content visibility (email, location, bio, passion, image)
### Advanced Features
@@ -400,14 +688,68 @@ Visit this link to see a beautiful landing page with all social profiles!
- 🎯 **Error Handling**: Comprehensive error handling with user-friendly messages
- 🧪 **Code Quality**: ESLint, Prettier, and best practices enforced
- 🔄 **Real-time Updates**: Instant updates across all shared links when you change destination URLs
-- 📈 **Analytics Dashboard**: Visual representation of click statistics
+- 📈 **Advanced Analytics Dashboard**: Comprehensive analytics with detailed insights
+ - Multiple visualization types (Line, Bar, Area, Pie charts)
+ - Time-based analysis (daily, weekly, monthly, yearly trends)
+ - Geographic analytics with country-level data
+ - Device, browser, and OS breakdowns
+ - Referrer analytics with category classification
+ - Hourly and day-of-week patterns
+ - Platform and link performance metrics
+ - Customizable time ranges and chart types
+ - Summary statistics and detailed breakdowns
+ - Full dark and light theme support with proper text contrast
- 🎭 **Platform Customization**: Add any platform with custom names
- 🔍 **Search & Filter**: Easy to find and manage your links
-- 📧 **Email Integration**: Seamless email notifications for important events
+- 📧 **Customizable Email Notifications**: Fully customizable email notifications based on your preferences
+ - **Link Click Notifications**: Get notified when someone clicks your links (toggle on/off)
+ - Includes platform, device, location, and timestamp
+ - **Profile View Notifications**: Get notified when someone views your public profile (toggle on/off)
+ - **Weekly Reports**: Receive weekly summaries of your link performance (toggle on/off)
+ - All notification types can be enabled/disabled independently in Settings
+- 🔐 **Private Link Protection**: Password-protected private links with secure verification and direct redirection
+- 👀 **Profile Preview**: Preview how your profile appears to visitors before making it public
+- 🔎 **User Search**: Real-time search for other users in navigation bar with instant results
+ - Search by username or name
+ - Instant dropdown results
+ - Click to visit public profiles
+ - Responsive: search icon on mobile, full input on desktop
+ - Only shows users with public profiles and search enabled
+- 🎛️ **Granular Privacy Controls**: Fully customizable visibility settings
+ - **Profile Visibility**: Control who can view your profile (public/private)
+ - **Search Visibility**: Enable/disable profile searchability and add search keywords
+ - **Content Visibility**: Individually toggle visibility of:
+ - Email address
+ - Location
+ - Bio
+ - Passion
+ - Profile image
+ - **Link Display**: Control whether link count and click stats are shown to visitors
+ - **Search Settings**: Add search keywords, enable featured profile option
+ - **Privacy Settings**: Control analytics visibility, last updated display, authentication requirements
+ - **Email Notifications**: Fully customizable email notification preferences
+ - **Link Click Notifications**: Toggle email notifications when someone clicks your links
+ - Includes detailed information: platform, device type, location, timestamp
+ - Perfect for tracking engagement and understanding audience behavior
+ - **Profile View Notifications**: Toggle email notifications when someone views your public profile
+ - Know when potential clients, employers, or visitors check out your profile
+ - Helps monitor profile visibility and interest
+ - **Weekly Reports**: Toggle weekly summary emails with performance insights
+ - Get insights into your most popular links
+ - Track overall engagement trends
+ - Monitor profile performance over time
+ - Each notification type can be enabled/disabled independently
+ - All settings customizable in Settings page
+ - Changes take effect immediately
+ - **Real-time Preview**: See changes immediately in profile preview
### Developer Features
-- 📚 **Comprehensive Documentation**: Detailed README, API docs, and code comments
+- 📚 **Comprehensive Documentation**:
+ - Detailed README, API docs, and code comments
+ - Model documentation in `/backend/doc/`
+ - Page documentation in `/frontend/docs/`
+ - CHANGELOG files for frontend and backend
- 🧩 **Modular Architecture**: Clean, maintainable code structure
- 🐳 **Docker Support**: Easy deployment with Docker and Docker Compose
- 🔧 **Environment Configuration**: Flexible environment variable setup
@@ -473,8 +815,8 @@ Visit this link to see a beautiful landing page with all social profiles!
#### 1. Clone the Repository
```bash
-git clone https://github.com/DpkRn/LinkBridger.git
-cd LinkBridger
+git clone https://github.com/DpkRn/All in1 url.git
+cd All in1 url
```
#### 2. Install Dependencies
@@ -498,9 +840,9 @@ Create a `.env` file in the `backend/` directory:
JWT_KEY=your_super_secret_jwt_key_here_min_32_chars
# MongoDB Connection String
-DB_URL=mongodb://localhost:27017/linkbridger
+DB_URL=mongodb://localhost:27017/All in1 url
# Or use MongoDB Atlas:
-# DB_URL=mongodb+srv://username:password@cluster.mongodb.net/linkbridger
+# DB_URL=mongodb+srv://username:password@cluster.mongodb.net/All in1 url
# Email Configuration (for notifications)
EMAIL_USER=your_email@gmail.com
@@ -555,15 +897,15 @@ docker-compose down
```bash
# Build backend image
cd backend
-docker build -t linkbridger-backend .
+docker build -t All in1 url-backend .
# Build frontend image
cd frontend
-docker build -t linkbridger-frontend .
+docker build -t All in1 url-frontend .
# Run containers
-docker run -d -p 8080:8080 linkbridger-backend
-docker run -d -p 5173:5173 linkbridger-frontend
+docker run -d -p 8080:8080 All in1 url-backend
+docker run -d -p 5173:5173 All in1 url-frontend
```
### Production Deployment
@@ -681,7 +1023,7 @@ This project adheres to professional development standards and best practices:
## 🤝 Contributing
-We **love** contributions! LinkBridger is an open-source project, and we welcome any contributions from the community. Whether you're fixing bugs, adding features, improving documentation, or suggesting ideas, your input is valuable!
+We **love** contributions! All in1 url is an open-source project, and we welcome any contributions from the community. Whether you're fixing bugs, adding features, improving documentation, or suggesting ideas, your input is valuable!
### 🌟 Why Contribute?
@@ -697,7 +1039,7 @@ We **love** contributions! LinkBridger is an open-source project, and we welcome
```bash
# Click the "Fork" button on GitHub, or use:
-gh repo fork DpkRn/LinkBridger
+gh repo fork DpkRn/All in1 url
```
#### 2. Create a Feature Branch
@@ -746,7 +1088,7 @@ git push origin feature/amazing-feature
#### 7. Open a Pull Request
-- Go to the [GitHub repository](https://github.com/DpkRn/LinkBridger)
+- Go to the [GitHub repository](https://github.com/DpkRn/All in1 url)
- Click "New Pull Request"
- Select your branch
- Describe your changes clearly
@@ -814,7 +1156,7 @@ Contributors will be:
[](https://github.com/DpkRn)
[](mailto:d.wizard.techno@gmail.com)
[](https://www.linkedin.com/in/dpkrn)
-[](https://linkb-one.vercel.app/dpkrn)
+[](https://dpkrn.allin1url.in)
**Passionate about building innovative solutions and contributing to open source**
@@ -822,11 +1164,11 @@ Contributors will be:
### About the Developer
-**Dwizard** is a passionate full-stack developer with expertise in modern web technologies. With a focus on creating user-friendly applications and contributing to the open-source community, Dwizard has built LinkBridger to solve real-world problems faced by professionals, content creators, and developers.
+**Dwizard** is a passionate full-stack developer with expertise in modern web technologies. With a focus on creating user-friendly applications and contributing to the open-source community, Dwizard has built All in1 url to solve real-world problems faced by professionals, content creators, and developers.
### 🎯 Mission
-To create tools that simplify digital life and empower users to build their online presence effectively. LinkBridger represents this mission by providing a free, open-source solution that puts users in control of their digital identity.
+To create tools that simplify digital life and empower users to build their online presence effectively. All in1 url represents this mission by providing a free, open-source solution that puts users in control of their digital identity.
### 💼 Skills & Expertise
@@ -844,14 +1186,14 @@ To create tools that simplify digital life and empower users to build their onli
**Professional Links:**
- 💼 **LinkedIn**: [Connect on LinkedIn](https://www.linkedin.com/in/dpkrn) - Let's network!
- 🐙 **GitHub**: [@DpkRn](https://github.com/DpkRn) - Check out my other projects
-- 🌐 **Portfolio**: [View Portfolio](https://linkb-one.vercel.app/dpkrn) - See my work
+- 🌐 **Portfolio**: [View Portfolio](https://dpkrn.allin1url.in) - See my work
- 📧 **Email**: [d.wizard.techno@gmail.com](mailto:d.wizard.techno@gmail.com) - Let's collaborate!
**Social Media:**
-- 📸 **Instagram**: [Follow on Instagram](https://linkb-one.vercel.app/dpkrn/instagram) (if available)
-- 👨💻 **GitHub Profile**: [View GitHub Profile](https://linkb-one.vercel.app/dpkrn/github)
-- 💻 **LeetCode**: [LeetCode Profile](https://linkb-one.vercel.app/dpkrn/leetcode) (if available)
-- 🎯 **Codeforces**: [Codeforces Profile](https://linkb-one.vercel.app/dpkrn/codeforces) (if available)
+- 📸 **Instagram**: [Follow on Instagram](https://dpkrn.allin1url.in/instagram) (if available)
+- 👨💻 **GitHub Profile**: [View GitHub Profile](https://dpkrn.allin1url.in/github)
+- 💻 **LeetCode**: [LeetCode Profile](https://dpkrn.allin1url.in/leetcode) (if available)
+- 🎯 **Codeforces**: [Codeforces Profile](https://dpkrn.allin1url.in/codeforces) (if available)
### 🎓 Learning & Growth
@@ -888,7 +1230,7 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
### What This Means
-- ✅ **Free to Use**: Use LinkBridger for personal or commercial projects
+- ✅ **Free to Use**: Use All in1 url for personal or commercial projects
- ✅ **Modify**: Change the code to fit your needs
- ✅ **Distribute**: Share the software
- ✅ **Private Use**: Use it privately
@@ -905,7 +1247,7 @@ We're grateful to:
- **Open Source Community** - For the amazing tools and libraries that make this project possible
- **Contributors** - Everyone who has contributed code, documentation, or ideas
-- **Users** - For using LinkBridger and providing valuable feedback
+- **Users** - For using All in1 url and providing valuable feedback
- **MongoDB** - For the excellent database service and documentation
- **Vercel** - For seamless frontend hosting and deployment
- **Tailwind CSS** - For the beautiful utility-first CSS framework
@@ -935,22 +1277,22 @@ To everyone who:
### 🌟 Show Your Support
-- ⭐ **Star the repo** - Help others discover LinkBridger
+- ⭐ **Star the repo** - Help others discover All in1 url
- 🍴 **Fork the repo** - Create your own version
- 🐛 **Report bugs** - Help us improve
- 💡 **Suggest features** - Share your ideas
- 📢 **Share with others** - Spread the word
-- 🤝 **Contribute** - Make LinkBridger even better
+- 🤝 **Contribute** - Make All in1 url even better
**Made with ❤️ by [Dwizard](https://github.com/DpkRn)**
-[⬆ Back to Top](#-linkbridger)
+[⬆ Back to Top](#-All in1 url)
---
### 🚀 Ready to Get Started?
-[**Try LinkBridger Now**](https://linkb-one.vercel.app) • [**View Documentation**](./frontend/src/components/Documentation.jsx) • [**Contribute**](#-contributing)
+[**Try All in1 url Now**](https://allin1url.in) • [**View Documentation**](./frontend/src/components/Documentation.jsx) • [**Contribute**](#-contributing)
**Questions?** [Email us](mailto:d.wizard.techno@gmail.com) - We're here to help! 💬
diff --git a/awsdeploy.md b/awsdeploy.md
new file mode 100644
index 0000000..3b6d4b3
--- /dev/null
+++ b/awsdeploy.md
@@ -0,0 +1,509 @@
+# AWS EC2 + GoDaddy + Docker + Nginx + Wildcard SSL Deployment Guide
+
+## Overview
+
+This guide deploys:
+
+* Frontend → `allin1url.in`
+* Backend APIs → `*.allin1url.in`
+* n8n → `n8n.allin1url.in`
+* Dockerized services
+* Nginx reverse proxy
+* Wildcard SSL using Let's Encrypt
+
+---
+
+# Step 1: Install Required Packages
+
+```bash
+sudo apt update
+sudo apt install git docker.io certbot -y
+```
+
+## Why?
+
+### Git
+
+Used to pull the latest code from GitHub.
+
+### Docker
+
+Runs frontend, backend, nginx and supporting services in containers.
+
+### Certbot
+
+Generates SSL certificates from Let's Encrypt.
+
+---
+
+# Step 2: Enable Docker For Current User
+
+```bash
+sudo usermod -aG docker $USER
+newgrp docker
+```
+
+## Why?
+
+Without this:
+
+```bash
+sudo docker ps
+```
+
+would be required every time.
+
+This adds your user to the Docker group.
+
+---
+
+# Step 3: Configure GitHub SSH
+
+Generate SSH key:
+
+```bash
+ssh-keygen -t ed25519 -C "your_email@example.com"
+```
+
+Show public key:
+
+```bash
+cat ~/.ssh/id_ed25519.pub
+```
+
+Add it to:
+
+GitHub → Settings → SSH Keys
+
+Verify:
+
+```bash
+ssh -T git@github.com
+```
+
+Clone repository:
+
+```bash
+git clone git@github.com:your-org/your-repo.git
+```
+
+## Why?
+
+SSH avoids typing GitHub credentials repeatedly.
+
+---
+
+# Step 4: Setup Project
+
+Go to project root.
+
+Create:
+
+```text
+.env
+frontend/.env
+backend/.env
+```
+
+Copy environment variables from local machine.
+
+## Why?
+
+Application configuration should not be hardcoded.
+
+Examples:
+
+* Database URLs
+* JWT secrets
+* API keys
+* Environment variables
+
+---
+
+# Step 5: Update Domain References
+
+Before deployment:
+
+* Update domain names
+* Update frontend URLs
+* Update backend URLs
+* Update nginx config
+
+Commit and push:
+
+```bash
+git add .
+git commit -m "update domain"
+git push
+```
+
+On server:
+
+```bash
+git pull
+```
+
+## Why?
+
+Server always deploys latest source from GitHub.
+
+---
+
+# Step 6: Build Project
+
+```bash
+./make.sh
+```
+
+## Why?
+
+Builds and prepares project for deployment.
+
+May include:
+
+* frontend build
+* backend build
+* docker build
+
+---
+
+# Step 7: Configure DNS In GoDaddy
+
+## Root Domain
+
+```text
+Type: A
+Host: @
+Value: EC2_PUBLIC_IP
+```
+
+### Why?
+
+Maps:
+
+```text
+allin1url.in
+```
+
+to EC2.
+
+---
+
+## Wildcard Domain
+
+```text
+Type: A
+Host: *
+Value: EC2_PUBLIC_IP
+```
+
+### Why?
+
+Makes all subdomains point to server.
+
+Examples:
+
+```text
+john.allin1url.in
+api.allin1url.in
+n8n.allin1url.in
+anything.allin1url.in
+```
+
+without creating separate records.
+
+---
+
+# Step 8: Generate Wildcard SSL Certificate
+
+```bash
+sudo certbot certonly \
+ --manual \
+ --preferred-challenges dns \
+ -d allin1url.in \
+ -d '*.allin1url.in'
+```
+
+## Why?
+
+Need SSL for:
+
+```text
+allin1url.in
+```
+
+and
+
+```text
+*.allin1url.in
+```
+
+A wildcard certificate alone DOES NOT cover root domain.
+
+Therefore both are requested.
+
+---
+
+# Step 9: Create ACME TXT Records
+
+Certbot generates:
+
+```text
+_acme-challenge.allin1url.in
+value1
+```
+
+and
+
+```text
+_acme-challenge.allin1url.in
+value2
+```
+
+Create BOTH:
+
+```text
+TXT _acme-challenge value1
+TXT _acme-challenge value2
+```
+
+## Why?
+
+Let's Encrypt validates:
+
+```text
+allin1url.in
+```
+
+and
+
+```text
+*.allin1url.in
+```
+
+separately.
+
+Each validation gets its own token.
+
+---
+
+# Step 10: Verify DNS Propagation
+
+```bash
+dig TXT _acme-challenge.allin1url.in +short
+```
+
+Expected:
+
+```text
+"value1"
+"value2"
+```
+
+## Why?
+
+If only one value appears:
+
+Certificate validation fails.
+
+Wait until both appear before continuing.
+
+---
+
+# Step 11: Verify Certificate Creation
+
+```bash
+sudo certbot certificates
+```
+
+Expected:
+
+```text
+Domains: allin1url.in *.allin1url.in
+```
+
+## Why?
+
+Confirms certificate was successfully issued.
+
+---
+
+# Step 12: Certificate Location
+
+Expected files:
+
+```text
+/etc/letsencrypt/live/allin1url.in/
+```
+
+Contains:
+
+```text
+cert.pem
+chain.pem
+fullchain.pem
+privkey.pem
+```
+
+## Meaning
+
+### cert.pem
+
+Server certificate.
+
+### chain.pem
+
+Intermediate CA certificates.
+
+### fullchain.pem
+
+cert.pem + chain.pem combined.
+
+Used by Nginx.
+
+### privkey.pem
+
+Private key.
+
+Must remain secret.
+
+---
+
+# Step 13: Configure Nginx SSL
+
+Use:
+
+```nginx
+ssl_certificate /etc/letsencrypt/live/allin1url.in/fullchain.pem;
+ssl_certificate_key /etc/letsencrypt/live/allin1url.in/privkey.pem;
+ssl_trusted_certificate /etc/letsencrypt/live/allin1url.in/chain.pem;
+```
+
+## Why?
+
+### ssl_certificate
+
+Certificate presented to browser.
+
+### ssl_certificate_key
+
+Private key matching certificate.
+
+### ssl_trusted_certificate
+
+Used for OCSP stapling verification.
+
+---
+
+# Important
+
+Always verify actual certificate path:
+
+```bash
+sudo certbot certificates
+```
+
+Do NOT assume:
+
+```text
+allin1url.in-0001
+```
+
+exists.
+
+Use whatever Certbot created.
+
+---
+
+# Step 14: Deploy Containers
+
+```bash
+git pull
+docker compose up -d --build
+```
+
+## Why?
+
+### git pull
+
+Fetch latest code.
+
+### docker compose up
+
+Creates containers.
+
+### -d
+
+Runs in background.
+
+### --build
+
+Forces fresh image build.
+
+---
+
+# Step 15: Verify Nginx
+
+```bash
+docker exec nginx nginx -t
+```
+
+Expected:
+
+```text
+syntax is ok
+test is successful
+```
+
+## Why?
+
+Prevents broken nginx configuration from being loaded.
+
+---
+
+# Step 16: Verify Public Access
+
+```bash
+curl -I https://allin1url.in
+```
+
+```bash
+curl -I https://n8n.allin1url.in
+```
+
+```bash
+curl -I https://dpkrn.allin1url.in
+```
+
+## Why?
+
+Confirms:
+
+* DNS works
+* SSL works
+* Nginx works
+* Containers are reachable
+
+---
+
+# Biggest Lesson Learned
+
+Before touching nginx SSL paths:
+
+```bash
+sudo certbot certificates
+```
+
+Check where Certbot actually stored certificates.
+
+
+
+when the real certificate lives at:
+
+```text
+/etc/letsencrypt/live/allin1url.in/
+```
+
+go and change the auth 2.0 redirection url
+All in1 url gmail
+https://console.cloud.google.com/auth/clients?project=All in1 url
\ No newline at end of file
diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md
index 7251dc4..5c39a85 100644
--- a/backend/CHANGELOG.md
+++ b/backend/CHANGELOG.md
@@ -1,4 +1,4 @@
-# Changelog
+# Backend Changelog
All notable changes to the backend will be documented in this file.
@@ -7,86 +7,172 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-### Added - 2024-12-19
-- **Edit Link Endpoint**: New API endpoint for editing existing links
- - Added `editLink` function in `LinkController.js`
- - Endpoint: `POST /source/editlink`
- - Validates user ownership before allowing edits
- - Supports updating both `source` (platform) and `destination` fields
- - Added duplicate source validation when changing platform name
- - Prevents creating duplicate platforms for the same user
- - Uses link ID to identify and update links (flexible and safe)
- - Returns updated link object in response
- - Proper error handling for unauthorized access, missing links, and duplicate sources
-
-- **Route Configuration**: Added edit link route
- - Registered `/editlink` route in `LinkRoute.js`
- - Protected with `verifyToken` middleware
- - Follows same authentication pattern as other routes
-
-### Changed - 2024-12-19
-- **Token Verification**: Improved token handling
- - Updated `verifyToken.js` middleware to use optional chaining (`req.cookies?.token`)
- - Added console logging for debugging token issues
- - More robust error handling for missing tokens
-
-- **Error Handling**: Enhanced error responses
- - Improved error messages for better user feedback
- - Better handling of edge cases in user profile routes
- - More descriptive error messages for missing resources
-
-- **Root Route**: Updated root endpoint behavior
- - Changed root route (`/`) to redirect to frontend instead of returning text
- - Redirects to `https://clickly.cv/app/` with 307 status code
- - Added console logging for debugging
-
-- **CORS Configuration**: Updated allowed origins
- - Added `http://localhost:5173` to allowed origins for local development
- - Maintained existing production origins
-
-- **Content Security Policy**: Updated CSP headers
- - Added `https://clickly.cv/*` to `connectSrc` directive
- - Maintained existing security policies
-
-### Fixed - 2024-12-19
-- **User Profile Routes**: Fixed error handling for non-existent users
- - Added null checks for user lookup in `/:username` route
- - Added null checks for user lookup in `/:username/:source` route
- - Returns proper 404 page instead of crashing when user doesn't exist
- - Improved error messages and fallback behavior
-
-- **Link Source Routes**: Enhanced error handling
- - Better handling when link source doesn't exist
- - Returns user-friendly error page with available link information
- - Improved error messages for missing links
-
-- **View Templates**: Updated error page template
- - Enhanced `not_exists.ejs` with better styling
- - Added flexbox layout for better centering
- - Added support for displaying available link information
- - Improved visual presentation of 404 errors
-
-### Security - 2024-12-19
-- **Authentication**: Enhanced token verification
- - More robust token extraction with optional chaining
- - Better error handling for expired or invalid tokens
- - Improved security for protected routes
-
-- **Authorization**: Added ownership validation
- - Edit link endpoint verifies user owns the link before allowing edits
- - Prevents unauthorized access to other users' links
- - Returns 403 Forbidden for unauthorized edit attempts
-
-## [Previous Versions]
-
-### Initial Release
-- User authentication system with JWT tokens
-- Link creation and management
-- User profile management
-- Email notifications for link clicks
-- Device information extraction
-- Cookie-based session management
-- CORS configuration for frontend access
-- Helmet.js security headers
-- MongoDB integration with Mongoose
-
+### Added - 2024-12-XX (Latest)
+
+- **Comprehensive Analytics API Enhancement**: Complete analytics system with real data aggregation
+ - **Referrer Analytics**: Added referrer category aggregation (Direct, Search, Social, Internal, External)
+ - **Top Referrer Sources**: Domain-level referrer tracking with categorization logic
+ - **Device Analytics**: Device type aggregation (Desktop, Mobile, Tablet)
+ - **Browser Analytics**: Browser distribution aggregation (Chrome, Safari, Firefox, Edge, etc.)
+ - **Operating System Analytics**: OS-based aggregation (Windows, macOS, Linux, iOS, Android)
+ - **Hourly Distribution**: Hourly click patterns aggregation (0-23 hours)
+ - **Day of Week Analysis**: Day-of-week click distribution (Sunday through Saturday)
+ - **Platform Performance**: Individual platform click metrics aggregation
+ - **Link-Based Analytics**: Per-link performance tracking with clicks and visits
+ - **MongoDB Aggregation Pipelines**: Replaced mock data with real aggregation queries
+ - **Date Range Filtering**: Proper time range filtering (7d, 30d, 90d, 1y, all)
+ - **Data Structure Consistency**: Ensured consistent data formats across all metrics
+ - **Referrer Categorization**: Intelligent referrer categorization logic
+ - **Domain Extraction**: Extracts domain names from referrer URLs
+ - **Statistics Calculation**: Total clicks, profile visits, unique countries, top referrer
+
+### Added - 2024-12-XX (Previous)
+
+- **Custom Subdomain Routing**: Complete subdomain-based routing system
+ - Subdomain middleware (`resolveUsername`) to extract username from subdomain
+ - Root route handler for subdomain linkhub display (`username.allin1url.in/`)
+ - Subdomain source route handler (`username.allin1url.in/platform`)
+ - Main domain redirects to frontend (`allin1url.in` → `allin1url.in/app/`)
+ - Separate nginx server blocks for main domain and wildcard subdomains
+ - EJS helper function (`getUserLinkUrl`) for subdomain URL generation in templates
+ - Updated all 13 EJS templates to use subdomain format
+ - Environment-aware URL generation (dev: `username.localhost:8080`, prod: `username.allin1url.in`)
+ - CORS configuration updated to allow all subdomains
+ - Password-protected links work correctly with subdomain routing
+ - LinkHub generation updated for subdomain format in error messages
+
+### Added - 2024-12-XX (Previous)
+
+- **Link Model Enhancements**: Added visibility and password fields
+ - Added `visibility` field with enum: 'public', 'unlisted', 'private'
+ - Default value: 'public'
+ - Added `password` field for unlisted link protection (hashed)
+ - Added `linkId` field (unique string) for foreign key relationships
+ - Added `deletedAt` field for soft deletes
+ - Added `createdAt` and `updatedAt` via timestamps
+ - Added indexes: `{ userId: 1, visibility: 1, deletedAt: 1 }`
+ - Added helper methods: `isAccessible()`, `shouldShowInProfile()`, `shouldShowInSearch()`
+
+- **LinkAnalytics Model**: Comprehensive click tracking model
+ - Tracks every click with full timestamp information
+ - Location data (country, city, region, IP address)
+ - Device information (type, brand, model)
+ - Operating system (name, version)
+ - Browser information (name, version)
+ - Referrer and user agent
+ - Full timestamp object with multiple formats
+ - Helper method: `getFullTimestamp()` for formatted timestamps
+ - Compound indexes for efficient queries
+ - Soft delete support
+
+- **UserSettings Model**: Privacy and visibility controls
+ - Profile visibility settings (8 toggle options)
+ - Link display settings (show count, show stats)
+ - Search & discovery settings (allow search, featured, keywords)
+ - Privacy settings (analytics, last updated, require auth)
+ - Notification settings (email on click, profile view, weekly report)
+ - Helper method: `isSearchable()` for profile search
+ - Static method: `getUserSettings()` for easy access
+ - Automatic settings creation on first access
+
+- **Model Timestamps**: Added to all models
+ - `createdAt`: Auto-generated timestamp
+ - `updatedAt`: Auto-updated timestamp
+ - `deletedAt`: Soft delete timestamp (optional)
+
+- **Model Documentation**: Comprehensive documentation
+ - Created `/backend/doc/` folder
+ - One markdown file per model
+ - Field descriptions with examples
+ - Enum values documented
+ - Required/optional explanations
+ - Usage examples
+ - Index information
+ - Relationship documentation
+ - README.md as index
+
+### Changed - 2024-12-XX
+
+- **Link Model**: Enhanced with visibility controls
+ - Changed default visibility from 'private' to 'public'
+ - Added password field for unlisted links
+ - Added linkId for foreign key relationships
+
+- **User Model**: Added soft delete support
+ - Added `deletedAt` field
+ - Maintains timestamps (already had)
+
+- **UserProfile Model**: Added soft delete and public profile support
+ - Added `deletedAt` field
+ - Removed `isPublic` field (moved to UserSettings)
+ - Maintains timestamps (already had)
+
+- **UserSettings Model**: Refactored link visibility
+ - Removed `links.defaultVisibility`
+ - Removed `links.publicLinks` array
+ - Removed `links.unlistedLinks` array
+ - Removed link visibility methods
+ - Kept only display settings (showLinkCount, showClickStats)
+ - Link visibility now managed in Link model
+
+### API Endpoints Needed
+
+The following endpoints need to be implemented:
+
+1. **Settings Endpoints**:
+ - `POST /settings/get` - Get user settings
+ - `POST /settings/update` - Update user settings
+
+2. **Link Visibility Endpoint**:
+ - `POST /source/updatevisibility` - Update link visibility
+
+3. **Search Endpoint**:
+ - `POST /search/users` - Search for users (returns searchable profiles)
+
+4. **Public Profile Endpoint**:
+ - `POST /profile/getpublicprofile` - Get public profile data with settings and public links only
+
+### Added - 2024-12-XX (Latest)
+
+- **Private Link Password Protection**: Complete password protection system for private links
+ - Password prompt page (`password_prompt.ejs`) for collecting passwords
+ - Secure password verification endpoint (`/link/verify-password`)
+ - Base64 encoding/decoding of username and source for security
+ - Direct HTTP redirect after successful password verification
+ - Support for both form submissions and JSON API requests
+ - Error handling with user-friendly error messages
+ - Click tracking and email notifications for password-protected link access
+ - Secure password hashing using bcryptjs
+ - Automatic redirect to destination URL after verification
+
+- **Link Visibility System**: Enhanced link visibility controls
+ - Three visibility levels: `public`, `unlisted`, `private`
+ - Public links: Visible in profile preview, searches, and link hub
+ - Unlisted links: Not in link hub or profile, but accessible via direct URL with password
+ - Private links: Completely hidden, require password for direct access
+ - Per-link visibility settings with visual indicators
+ - Helper methods: `isAccessible()`, `shouldShowInProfile()`, `shouldShowInSearch()`
+
+- **Public Profile System**: Public profile viewing and search
+ - Public profile endpoint with optional authentication (`verifyTokenOptional`)
+ - Profile preview component for visitors
+ - User search functionality in navigation
+ - Respects all privacy settings from UserSettings model
+ - Owner preview functionality to see how profile appears to visitors
+
+- **Settings Management**: Comprehensive privacy and visibility controls
+ - Settings page for managing all privacy preferences
+ - Profile visibility controls (8 toggle options)
+ - Link display settings
+ - Search & discovery settings
+ - Privacy and notification settings
+ - Backend integration with proper nested object updates
+
+### Notes
+
+- All models now support soft deletes
+- Timestamps are automatically managed by Mongoose
+- Link visibility is per-link, not global
+- UserSettings provides granular privacy controls
+- LinkAnalytics provides comprehensive click tracking
+- Private links use secure password verification with direct redirection
diff --git a/backend/controller/AnalyticsController.js b/backend/controller/AnalyticsController.js
new file mode 100644
index 0000000..fe4f7ff
--- /dev/null
+++ b/backend/controller/AnalyticsController.js
@@ -0,0 +1,679 @@
+const mongoose = require('mongoose');
+const Link = require('../model/linkModel');
+const LinkAnalytics = require('../model/linkAnalyticsModel');
+
+const getAnalytics = async (req, res) => {
+ try {
+ const userId = req.userId;
+ const { username, timeRange } = req.body;
+
+ if (!userId || !username) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username is required'
+ });
+ }
+
+ // Get all links for the user (excluding deleted links)
+ const links = await Link.find({
+ username,
+ userId,
+ deletedAt: null
+ }, {
+ source: 1,
+ destination: 1,
+ clicked: 1,
+ notSeen: 1,
+ visibility: 1,
+ _id: 1
+ });
+
+ const linkIds = links.map(link => link._id);
+
+ // Calculate date range
+ const today = new Date();
+ today.setHours(23, 59, 59, 999); // End of today
+ let days = 30;
+ let startDate = new Date(today);
+
+ switch (timeRange) {
+ case '7d':
+ days = 7;
+ startDate.setDate(today.getDate() - 6); // Include today + 6 days
+ break;
+ case '30d':
+ days = 30;
+ startDate.setDate(today.getDate() - 29); // Include today + 29 days
+ break;
+ case '90d':
+ days = 90;
+ startDate.setDate(today.getDate() - 89);
+ break;
+ case '1y':
+ days = 365;
+ startDate.setDate(today.getDate() - 364);
+ break;
+ case 'all':
+ startDate = null; // No start date limit
+ days = 365; // For display purposes
+ break;
+ default:
+ days = 30;
+ startDate.setDate(today.getDate() - 29);
+ }
+
+ startDate?.setHours(0, 0, 0, 0); // Start of the day
+
+ // Build match conditions for analytics
+ const matchConditions = {
+ userId: new mongoose.Types.ObjectId(userId),
+ username,
+ deletedAt: null
+ };
+
+ // Add date filter if not 'all'
+ if (timeRange !== 'all' && startDate) {
+ matchConditions.clickDate = {
+ $gte: startDate,
+ $lte: today
+ };
+ }
+
+ // Filter by links if available (only show analytics for existing links)
+ if (linkIds.length > 0) {
+ matchConditions.$or = [
+ { linkId: { $in: linkIds } },
+ { linkId: null } // Include profile visits (linkId is null for profile visits)
+ ];
+ } else {
+ // If no links, only show profile visits
+ matchConditions.linkId = null;
+ }
+
+ // Generate date range for time series data
+ const generateDateRange = () => {
+ const dates = [];
+ const rangeDays = timeRange === 'all' ? 365 : days;
+ for (let i = rangeDays - 1; i >= 0; i--) {
+ const date = new Date(today);
+ date.setDate(date.getDate() - i);
+ dates.push({
+ date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
+ fullDate: new Date(date.getFullYear(), date.getMonth(), date.getDate())
+ });
+ }
+ return dates;
+ };
+
+ const dateRange = generateDateRange();
+
+ // 1. Profile Visits (where linkId is null)
+ const profileVisitsQuery = { ...matchConditions, linkId: null };
+ const profileVisitsAggregation = await LinkAnalytics.aggregate([
+ { $match: profileVisitsQuery },
+ {
+ $group: {
+ _id: {
+ $dateToString: { format: "%Y-%m-%d", date: "$clickDate" }
+ },
+ visits: { $sum: 1 }
+ }
+ },
+ { $sort: { _id: 1 } }
+ ]);
+
+ // Map to date range
+ const profileVisitsMap = new Map(
+ profileVisitsAggregation.map(item => [item._id, item.visits])
+ );
+ const profileVisits = dateRange.map(({ date, fullDate }) => {
+ const dateStr = fullDate.toISOString().split('T')[0];
+ return {
+ date,
+ visits: profileVisitsMap.get(dateStr) || 0
+ };
+ });
+
+ // 2. Click Counts (all clicks including profile visits)
+ const clickCountsAggregation = await LinkAnalytics.aggregate([
+ { $match: matchConditions },
+ {
+ $group: {
+ _id: {
+ $dateToString: { format: "%Y-%m-%d", date: "$clickDate" }
+ },
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { _id: 1 } }
+ ]);
+
+ const clickCountsMap = new Map(
+ clickCountsAggregation.map(item => [item._id, item.clicks])
+ );
+ const clickCounts = dateRange.map(({ date, fullDate }) => {
+ const dateStr = fullDate.toISOString().split('T')[0];
+ return {
+ date,
+ clicks: clickCountsMap.get(dateStr) || 0
+ };
+ });
+
+ // 3. Location Data (real data)
+ const locationAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, 'location.country': { $ne: null } } },
+ {
+ $group: {
+ _id: '$location.country',
+ value: { $sum: 1 }
+ }
+ },
+ { $sort: { value: -1 } },
+ { $limit: 10 }
+ ]);
+ const locationData = locationAggregation.map(item => ({
+ name: item._id || 'Unknown',
+ value: item.value
+ }));
+
+ // 4. OS Data (real data)
+ const osAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, 'os.name': { $ne: null } } },
+ {
+ $group: {
+ _id: '$os.name',
+ value: { $sum: 1 }
+ }
+ },
+ { $sort: { value: -1 } },
+ { $limit: 10 }
+ ]);
+ const osData = osAggregation.map(item => ({
+ name: item._id || 'Unknown',
+ value: item.value
+ }));
+
+ // 5. Browser Data (real data)
+ const browserAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, 'browser.name': { $ne: null } } },
+ {
+ $group: {
+ _id: '$browser.name',
+ value: { $sum: 1 }
+ }
+ },
+ { $sort: { value: -1 } },
+ { $limit: 10 }
+ ]);
+ const browserData = browserAggregation.map(item => ({
+ name: item._id || 'Unknown',
+ value: item.value
+ }));
+
+ // 6. Device Data (real data)
+ const deviceAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, 'device.type': { $ne: null } } },
+ {
+ $group: {
+ _id: '$device.type',
+ value: { $sum: 1 }
+ }
+ },
+ { $sort: { value: -1 } }
+ ]);
+ const deviceData = deviceAggregation.map(item => ({
+ name: item._id.charAt(0).toUpperCase() + item._id.slice(1) || 'Unknown',
+ value: item.value
+ }));
+
+ // 7. Referrer Data (NEW - real referrer analytics)
+ const referrerAggregation = await LinkAnalytics.aggregate([
+ { $match: matchConditions },
+ {
+ $group: {
+ _id: '$referrer',
+ value: { $sum: 1 },
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { value: -1 } },
+ { $limit: 20 }
+ ]);
+
+ // Process referrer data - extract domain names and categorize
+ const referrerData = referrerAggregation.map(item => {
+ let name = item._id || 'direct';
+ let category = 'direct';
+
+ if (name !== 'direct' && name !== 'null' && name !== '') {
+ try {
+ const url = new URL(name);
+ name = url.hostname.replace('www.', '');
+
+ // Categorize referrers
+ if (name.includes('google') || name.includes('bing') || name.includes('yahoo') || name.includes('duckduckgo')) {
+ category = 'search';
+ } else if (name.includes('facebook') || name.includes('twitter') || name.includes('linkedin') || name.includes('instagram') || name.includes('youtube') || name.includes('tiktok')) {
+ category = 'social';
+ } else if (name.includes('allin1url.in') || name.includes(username)) {
+ category = 'internal';
+ } else {
+ category = 'external';
+ }
+ } catch (e) {
+ // Invalid URL, keep as is
+ category = 'other';
+ }
+ } else {
+ name = 'Direct';
+ category = 'direct';
+ }
+
+ return {
+ name,
+ value: item.value,
+ clicks: item.clicks,
+ category,
+ originalReferrer: item._id
+ };
+ });
+
+ // 8. Referrer by Category (group referrers by category)
+ const referrerByCategory = referrerData.reduce((acc, item) => {
+ const category = item.category;
+ if (!acc[category]) {
+ acc[category] = { name: category.charAt(0).toUpperCase() + category.slice(1), value: 0, clicks: 0 };
+ }
+ acc[category].value += item.value;
+ acc[category].clicks += item.clicks;
+ return acc;
+ }, {});
+ const referrerCategoryData = Object.values(referrerByCategory);
+
+ // 9. Platform Data (by link source - real data)
+ const platformAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, linkId: { $ne: null } } },
+ { $lookup: { from: 'links', localField: 'linkId', foreignField: '_id', as: 'link' } },
+ { $unwind: { path: '$link', preserveNullAndEmptyArrays: true } },
+ { $match: { 'link.source': { $ne: null } } },
+ {
+ $group: {
+ _id: '$link.source',
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { clicks: -1 } }
+ ]);
+ const platformData = platformAggregation.map(item => ({
+ name: item._id ? (item._id.charAt(0).toUpperCase() + item._id.slice(1)) : 'Unknown',
+ clicks: item.clicks,
+ value: item.clicks
+ }));
+
+ // 10. Link Data (by link with clicks and visits)
+ const linkAggregation = await LinkAnalytics.aggregate([
+ { $match: { ...matchConditions, linkId: { $ne: null } } },
+ { $lookup: { from: 'links', localField: 'linkId', foreignField: '_id', as: 'link' } },
+ { $unwind: { path: '$link', preserveNullAndEmptyArrays: true } },
+ { $match: { 'link.source': { $ne: null } } },
+ {
+ $group: {
+ _id: '$link.source',
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { clicks: -1 } }
+ ]);
+ const linkData = linkAggregation.map(item => ({
+ name: item._id ? (item._id.charAt(0).toUpperCase() + item._id.slice(1)) : 'Unknown',
+ clicks: item.clicks,
+ visits: item.clicks // Use clicks as visits for now
+ }));
+
+ // 11. Hourly Distribution (time-based analytics)
+ const hourlyAggregation = await LinkAnalytics.aggregate([
+ { $match: matchConditions },
+ {
+ $group: {
+ _id: { $hour: '$clickDate' },
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { _id: 1 } }
+ ]);
+ const hourlyData = Array.from({ length: 24 }, (_, hour) => {
+ const hourData = hourlyAggregation.find(h => h._id === hour);
+ return {
+ hour: `${hour}:00`,
+ clicks: hourData?.clicks || 0
+ };
+ });
+
+ // 12. Day of Week Distribution
+ const dayOfWeekAggregation = await LinkAnalytics.aggregate([
+ { $match: matchConditions },
+ {
+ $project: {
+ dayOfWeek: { $dayOfWeek: '$clickDate' }
+ }
+ },
+ {
+ $group: {
+ _id: '$dayOfWeek',
+ clicks: { $sum: 1 }
+ }
+ },
+ { $sort: { _id: 1 } }
+ ]);
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ const dayOfWeekData = Array.from({ length: 7 }, (_, day) => {
+ const dayData = dayOfWeekAggregation.find(d => d._id === day + 1);
+ return {
+ name: dayNames[day],
+ clicks: dayData?.clicks || 0,
+ value: dayData?.clicks || 0
+ };
+ });
+
+ return res.status(200).json({
+ success: true,
+ analytics: {
+ profileVisits,
+ clickCounts,
+ locationData,
+ osData,
+ browserData,
+ deviceData,
+ referrerData,
+ referrerCategoryData,
+ platformData,
+ linkData,
+ hourlyData,
+ dayOfWeekData
+ }
+ });
+
+ } catch (err) {
+ console.error('Error fetching analytics:', err);
+ return res.status(500).json({
+ success: false,
+ message: 'Server Internal Error',
+ error: err.message
+ });
+ }
+};
+
+const saveAnalytics = async ({
+ linkId,
+ userId,
+ username,
+ req
+}) => {
+ if (req.skipAnalytics) {
+ return;
+ }
+
+ try {
+ const payload = req?.analyticsPayload || {};
+ const details = req?.details || {}; // Keep for backward compatibility
+
+ const analytics = new LinkAnalytics({
+ linkId,
+ userId,
+ username,
+
+ // Location
+ location: {
+ country: payload?.location?.country || details?.country || null,
+ city: payload?.location?.city || details?.city || null,
+ region: payload?.location?.region || null,
+ ipAddress: payload?.location?.ipAddress || details?.ip || null
+ },
+
+ // Device
+ device: {
+ type: payload?.device?.type || null,
+ brand: payload?.device?.brand || null,
+ model: payload?.device?.model || null
+ },
+
+ // OS
+ os: {
+ name: payload?.os?.name || null,
+ version: payload?.os?.version || null
+ },
+
+ // Browser
+ browser: {
+ name: payload?.browser?.name || details?.browser || null,
+ version: payload?.browser?.version || null
+ },
+
+ // Referrer
+ referrer: payload?.referrer || 'direct',
+
+ // User Agent
+ userAgent: payload?.userAgent || null,
+
+ // clickDate is auto-set
+ // clickedTime is auto-derived in pre-save hook
+ });
+ // console.log(analytics)
+
+ await analytics.save();
+ } catch (err) {
+ console.error('❌ Failed to save analytics:', err);
+ }
+};
+
+const getClickDetails = async (req, res) => {
+ try {
+ const userId = req.userId;
+ const {
+ username,
+ linkId,
+ page = 1,
+ limit = 50,
+ search = '',
+ startDate,
+ endDate
+ } = req.body;
+
+ if (!userId || !username) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username is required'
+ });
+ }
+
+ // Build match conditions
+ const matchConditions = {
+ userId: new mongoose.Types.ObjectId(userId),
+ username,
+ deletedAt: null
+ };
+
+ // Filter by specific link if provided
+ if (linkId) {
+ matchConditions.linkId = new mongoose.Types.ObjectId(linkId);
+ }
+
+ // Date range filter
+ if (startDate || endDate) {
+ matchConditions.clickDate = {};
+ if (startDate) {
+ matchConditions.clickDate.$gte = new Date(startDate);
+ }
+ if (endDate) {
+ matchConditions.clickDate.$lte = new Date(endDate);
+ }
+ }
+
+ // Search filter (search in referrer, userAgent, browser.name, os.name, device info)
+ if (search) {
+ matchConditions.$or = [
+ { 'referrer': { $regex: search, $options: 'i' } },
+ { 'userAgent': { $regex: search, $options: 'i' } },
+ { 'browser.name': { $regex: search, $options: 'i' } },
+ { 'os.name': { $regex: search, $options: 'i' } },
+ { 'device.brand': { $regex: search, $options: 'i' } },
+ { 'device.model': { $regex: search, $options: 'i' } },
+ { 'location.country': { $regex: search, $options: 'i' } },
+ { 'location.city': { $regex: search, $options: 'i' } }
+ ];
+ }
+
+ // Get total count for pagination
+ const totalCount = await LinkAnalytics.countDocuments(matchConditions);
+
+ // Get paginated results
+ const clicks = await LinkAnalytics.find(matchConditions)
+ .populate('linkId', 'source destination')
+ .sort({ clickDate: -1 })
+ .skip((page - 1) * limit)
+ .limit(parseInt(limit))
+ .lean();
+
+ // Format the response
+ const formattedClicks = clicks.map(click => ({
+ _id: click._id,
+ linkId: click.linkId?._id || "linkhub" ,
+ linkSource: click.linkId?.source || 'Unknown',
+ linkDestination: click.linkId?.destination || 'linkhub',
+ clickDate: click.clickDate,
+ clickedTime: click.clickedTime,
+ location: click.location,
+ device: click.device,
+ os: click.os,
+ browser: click.browser,
+ referrer: click.referrer,
+ userAgent: click.userAgent,
+ seen: click.seen
+ }));
+
+ return res.status(200).json({
+ success: true,
+ data: {
+ clicks: formattedClicks,
+ pagination: {
+ currentPage: parseInt(page),
+ totalPages: Math.ceil(totalCount / limit),
+ totalClicks: totalCount,
+ hasNext: page * limit < totalCount,
+ hasPrev: page > 1
+ }
+ }
+ });
+
+ } catch (err) {
+ console.error('❌ Failed to get click details:', err);
+ return res.status(500).json({
+ success: false,
+ message: 'Server Internal Error'
+ });
+ }
+};
+
+const getClickDetailsV1 = async (req, res) => {
+ try {
+ const userId = req.userId;
+ const { username } = req.body;
+
+ if (!userId || !username) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username is required'
+ });
+ }
+
+ // Get all clicks for the user (no pagination, return all for mock format)
+ const clicks = await LinkAnalytics.find({
+ userId: new mongoose.Types.ObjectId(userId),
+ username,
+ deletedAt: null
+ })
+ .populate('linkId', 'source destination')
+ .sort({ clickDate: -1 }) // Most recent first
+ .lean();
+
+ // Transform data to match the original mock format
+ const mockFormattedClicks = clicks.map(click => ({
+ _id: click._id.toString(),
+ linkId: click.linkId?._id?.toString() || "undefined",
+ linkSource: click.linkId?.source || 'linkhub',
+ shortUrl: `/${click.linkId?.source || 'unknown'}`,
+ linkDestination: click.linkId?.destination || `${username}.allin1url.in`,
+ clickDate: click.clickDate,
+ clickedTime: click.clickedTime,
+ location: click.location,
+ device: click.device,
+ os: click.os,
+ browser: click.browser,
+ referrer: click.referrer,
+ userAgent: click.userAgent,
+ seen: click.seen
+ }));
+
+ return res.status(200).json({
+ success: true,
+ data: mockFormattedClicks
+ });
+
+ } catch (err) {
+ console.error('❌ Failed to get click details v1:', err);
+ return res.status(500).json({
+ success: false,
+ message: 'Server Internal Error'
+ });
+ }
+};
+
+const markReadNotification = async (req, res) => {
+ try {
+ const { clickId } = req.body;
+ const userId = req.userId;
+
+ if (!clickId || !userId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Click ID is required'
+ });
+ }
+
+ // Update the specific click to mark as seen
+ const updatedClick = await LinkAnalytics.findOneAndUpdate(
+ { _id: clickId, userId: userId },
+ { $set: { seen: true } },
+ { new: true }
+ );
+
+ if (!updatedClick) {
+ return res.status(404).json({
+ success: false,
+ message: 'Click not found or access denied'
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: 'Notification marked as read',
+ data: updatedClick
+ });
+
+ } catch (err) {
+ console.error('❌ Failed to mark notification as read:', err);
+ return res.status(500).json({
+ success: false,
+ message: 'Server Internal Error'
+ });
+ }
+};
+
+module.exports = {
+ getAnalytics,
+ getClickDetails,
+ getClickDetailsV1,
+ markReadNotification,
+ saveAnalytics,
+};
+
diff --git a/backend/controller/AuthController.js b/backend/controller/AuthController.js
index a048e50..8af0726 100644
--- a/backend/controller/AuthController.js
+++ b/backend/controller/AuthController.js
@@ -1,9 +1,10 @@
const User = require("../model/userModel");
const bcryptjs = require("bcryptjs");
const jwt = require("jsonwebtoken");
-const { sendOtpVerification } = require("../lib/mail");
+const { sendOtpVerification, sendWelcomeEmail, sendNewUserOnboardingEmail } = require("../lib/mail");
const Profile=require('../model/userProfile')
const Otp = require("../model/otpModel");
+const { clientUrl } = require("../utils");
function generateOTP() {
let otp = Math.floor(1000 + Math.random() * 9000);
@@ -14,7 +15,7 @@ function generateOTP() {
const signUpController = async (req, res, next) => {
try {
const { email, password, username } = req.body;
- if (!email || !username || !password) {
+ if (!email || !username ) {
return res
.status(400)
.json({ success: false, message: "All fields are required" });
@@ -34,17 +35,22 @@ const signUpController = async (req, res, next) => {
.status(409)
.json({ success: false, message: "user allready exists !" });
}
- const hashedPassword = await bcryptjs.hash(password, 10);
+ // const hashedPassword = await bcryptjs.hash(password, 10);
const user = await User.create({
email,
- password: hashedPassword,
- username,
+ // password: hashedPassword,
+ username:username.toLowerCase(),
});
const userinfo=await Profile.create({username,image:"/images/panda.png"});
if (user&&userinfo) {
console.log("user created");
+ // Use name from request body or fallback to username
+ const displayName = username;
+ sendWelcomeEmail(email, username, displayName, "All in1 url");
+ adminEmail=process.env.ADMIN_EMAIL || "d.wizard.techno@gmail.com";
+ sendNewUserOnboardingEmail("d.wizard.techno@gmail.com", username, displayName, "All in1 url");
return res
.status(201)
.json({ success: true, message: "user registerd !", user });
@@ -240,6 +246,124 @@ const changePassword = async (req, res, next) => {
}
};
+const handleAuthCallback=async (req, res) => {
+ try {
+
+ const { code, state } = req.query;
+ // console.log("code and status=",code,state)
+
+ if (!code) {
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Authorization code missing`);
+ }
+
+ // 🔁 Exchange auth code for tokens (SERVER ONLY)
+ const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams({
+ code,
+ client_id: process.env.GOOGLE_CLIENT_ID,
+ client_secret: process.env.GOOGLE_CLIENT_SECRET,
+ redirect_uri: `${process.env.TIER=='dev'?"http://localhost:8080":"https://allin1url.in"}/auth/google`,
+ grant_type: "authorization_code",
+ }),
+ });
+
+ const tokens = await tokenRes.json();
+
+ if (!tokens.id_token) {
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Failed to get ID token`);
+ }
+
+ // 🔐 Decode ID token (basic decode for now)
+ const payload = JSON.parse(
+ Buffer.from(tokens.id_token.split(".")[1], "base64").toString()
+ );
+
+ /**
+ * payload contains:
+ * sub, email, name, picture, email_verified, aud, iss, exp
+ */
+
+ // ✅ Verify audience
+
+ if (payload.aud !== process.env.GOOGLE_CLIENT_ID) {
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Invalid audience`);
+ }
+
+ const {username,usertype}=JSON.parse(
+ Buffer.from(state,'base64').toString()
+ )
+
+ const email=payload.email
+ const picture=payload.picture
+ const email_verified=payload.email_verified
+
+ // console.log("user payload and username",payload,username)
+
+ //check user already exist or have to create
+ let user = await User.findOne({ email }).lean();
+
+
+ if (!user && usertype=='onboarding'){
+ const newUser = await User.create({
+ email,
+ // password: hashedPassword,
+ username:username.toLowerCase(),
+ });
+ const newUserInfo=await Profile.create({username,image:picture});
+ if (newUser&&newUserInfo) {
+ console.log("user created");
+ // Update user variable to reference the newly created user
+ user = await User.findById(newUser._id).lean();
+ // Use name from request body or fallback to username
+ const displayName = username;
+ sendWelcomeEmail(email, username, displayName, "All in1 url");
+ adminEmail=process.env.ADMIN_EMAIL || "d.wizard.techno@gmail.com";
+ sendNewUserOnboardingEmail("d.wizard.techno@gmail.com", username, displayName, "All in1 url");
+ }
+ }
+
+ if (!user && usertype=='onboarded') {
+ // Redirect to login page with error message instead of returning JSON
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Email does not exist`);
+ }
+
+ if (!user) {
+ // Fallback: if user still doesn't exist for any reason, redirect with error
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Authentication failed`);
+ }
+
+ // console.log("id=",user._id)
+
+ // 🧠 Create your app JWT
+ const token = jwt.sign(
+ { email:email, id: user._id },
+ process.env.JWT_KEY,
+ { expiresIn: "24h" }
+ );
+ console.log("generated token:",token)
+ res.cookie("token", token, {
+ maxAge: 24 * 60 * 60 * 1000,
+ sameSite: "None",
+ secure: true,
+ });
+
+ // 🔁 Redirect to frontend dashboard (authenticated users go to /home)
+ const frontendUrl = `${clientUrl(process.env.TIER)}/home`;
+ res.redirect(frontendUrl);
+
+ // 🔵 Option 2 (testing only): return JSON
+ // res.json({ tokens, user: payload });
+
+ } catch (err) {
+ console.error("Google auth error:", err);
+ return res.redirect(`${clientUrl(process.env.TIER)}/?error=Google authentication failed`);
+ }
+}
+
module.exports = {
signUpController,
signInController,
@@ -248,4 +372,5 @@ module.exports = {
checkAvailablity,
sendOtp,
changePassword,
+ handleAuthCallback
};
diff --git a/backend/controller/LinkBridgerController.js b/backend/controller/LinkBridgerController.js
new file mode 100644
index 0000000..5021ddf
--- /dev/null
+++ b/backend/controller/LinkBridgerController.js
@@ -0,0 +1,269 @@
+const Link = require('../model/linkModel');
+const Profile = require('../model/userProfile');
+const User = require('../model/userModel');
+const UserSettings = require('../model/userSettingsModel');
+const { sendLinkClickEmail, sendLinkHubVisitEmail } = require('../lib/mail');
+const { decodeData, encodeData } = require('../utils');
+const bcryptjs = require('bcryptjs');
+
+// Handle password submission for private links
+const verifyPassword = async (req, res) => {
+ const { hashedUsername, hashedSource, password } = req.body;
+
+ const username = decodeData(hashedUsername);
+ const source = decodeData(hashedSource);
+ console.log(password)
+
+ const doc = await Link.findOne({
+ username,
+ source,
+ deletedAt: null
+ });
+
+ if (!doc) {
+ return res.status(404).json({ success: false });
+ }
+
+ if (!doc.password || !(await bcryptjs.compare(password, doc.password))) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid password"
+ });
+ }
+
+ // Password correct, redirect directly to destination
+ const {destination,clicked,notSeen}=doc
+ await Link.updateOne({username,source},{$set:{clicked:clicked+1,notSeen:notSeen+1}})
+
+ const info=await User.findOne({username},{email:1,name:1})
+ if(info) {
+ const {email,name}=info
+ const deviceDetails=req.details || {}
+ // Send link click email notification if enabled
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnClick()) {
+ // Send email asynchronously, don't wait for it
+ sendLinkClickEmail(email,username,name,deviceDetails,source).catch(err => {
+ console.error(`Failed to send link click email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ }
+ }
+
+ return res.json({
+ success: true,
+ destination: destination
+ });
+};
+
+// Handle link access by username and source
+const getLinkByUsernameAndSource = async (req, res) => {
+ const {username,source}=req.params;
+ const linkHub=`Available link: ${req.protocol}://${req.get('host')}/${username}`
+
+ const doc=await Link.findOne({
+ username,
+ source,
+ deletedAt: null
+ })
+
+ const info=await User.findOne({username},{email:1,name:1})
+ if(!info){
+ return res.render('not_exists',{
+ linkHub:""
+ })
+ }
+ const {email,name}=info
+
+ if(!doc) {
+ return res.render('not_exists',{
+ linkHub:linkHub
+ })
+ }
+
+ // Check link visibility
+ // private links should render password prompt page directly
+ if(!doc.isAccessible()) {
+ console.log("not accessible")
+ // Encode username and source before sending to EJS
+ const hashedUsername = encodeData(username);
+ const hashedSource = encodeData(source);
+ return res.render('password_prompt', {
+ hashedUsername:hashedUsername,
+ hashedSource:hashedSource,
+ linkId:doc.linkId
+ });
+ }
+
+ // unlisted links are accessible via direct URL (password protection can be added later)
+ // public links are accessible
+ const {destination,clicked,notSeen}=doc
+ await Link.updateOne({username,source},{$set:{clicked:clicked+1,notSeen:notSeen+1}})
+
+ const deviceDetails=req.details
+
+ // Check if email notification is enabled for link clicks
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnClick()) {
+ sendLinkClickEmail(email,username,name,deviceDetails,source).catch(err => {
+ console.error(`Failed to send link click email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ // Don't send email if there's an error checking settings
+ }
+
+ return res.redirect(307,destination)
+};
+
+// Handle profile page by username
+const getProfileByUsername = async (req, res) => {
+ // Allow iframe embedding for preview (allow from frontend origins)
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
+ const frontendOrigins = "http://localhost:5173 https://allin1url.in https://linkbriger.vercel.app 'self'";
+ res.setHeader('Content-Security-Policy', `frame-ancestors ${frontendOrigins}`);
+
+ console.log("backend profile search start")
+ const username=req.params.username
+ // Only show public links in linkhub - unlisted and private links should not appear
+ const tree=await Link.find({
+ username: username,
+ visibility: 'public',
+ deletedAt: null
+ })
+ const dp=await Profile.findOne({username},{image:1,bio:1});
+
+ const info=await User.findOne({username},{email:1,name:1})
+ if(!info){
+ return res.render('not_exists')
+ }
+ const {email,name}=info
+ const deviceDetails=req.details
+
+ // Get visitor information if they're logged in - ALWAYS capture username when available
+ let visitorUsername = null;
+ let visitorName = null;
+ let isOwner = false;
+
+ if (req.userId) {
+ try {
+ const visitor = await User.findById(req.userId, { username: 1, name: 1 });
+ if (visitor) {
+ // Always capture visitor info if they're logged in
+ visitorUsername = visitor.username;
+ visitorName = visitor.name;
+ // Check if visitor is the profile owner
+ isOwner = visitor.username === username;
+ }
+ } catch (err) {
+ console.error(`Error fetching visitor info:`, err);
+ }
+ }
+
+ // Log visitor info for debugging
+ if (visitorUsername) {
+ console.log(`LinkHub visit by logged-in user: @${visitorUsername} viewing @${username} (isOwner: ${isOwner})`);
+ } else {
+ console.log(`LinkHub visit by anonymous user viewing @${username}`);
+ }
+
+ // Send LinkHub visit email notification ONLY if:
+ // 1. Visitor is NOT the profile owner (prevent self-visit emails)
+ // 2. Email notifications are enabled
+ // 3. Visitor username is captured (logged-in user) or anonymous visit
+ if (!isOwner) {
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnLinkHubView()) {
+ // Only send email if visitor is different from profile owner
+ if (visitorUsername && visitorUsername !== username) {
+ console.log(`Sending LinkHub visit email to @${username} - visited by @${visitorUsername}`);
+ sendLinkHubVisitEmail(
+ email,
+ username,
+ name,
+ deviceDetails,
+ visitorUsername,
+ visitorName
+ ).catch(err => {
+ console.error(`Failed to send LinkHub visit email to ${username}:`, err);
+ });
+ } else if (!visitorUsername) {
+ // Anonymous visitor - still send email but without username
+ console.log(`Sending LinkHub visit email to @${username} - visited by anonymous user`);
+ sendLinkHubVisitEmail(
+ email,
+ username,
+ name,
+ deviceDetails,
+ null,
+ null
+ ).catch(err => {
+ console.error(`Failed to send LinkHub visit email to ${username}:`, err);
+ });
+ } else {
+ console.log(`Skipping email: Visitor @${visitorUsername} is the profile owner @${username}`);
+ }
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ // Don't send email if there's an error checking settings
+ }
+ } else {
+ console.log(`Skipping email: Profile owner @${username} is viewing their own LinkHub`);
+ }
+
+ if(tree&&dp){
+ // Get user settings to determine template
+ let template = 'default'; // Default template
+
+ // Check if template query parameter is provided (for preview)
+ const previewTemplate = req.query.template;
+ if (previewTemplate) {
+ template = previewTemplate;
+ } else {
+ // Otherwise, use user's saved template from settings
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.template) {
+ template = settings.template;
+ }
+ } catch (err) {
+ console.log('Error fetching template settings, using default:', err.message);
+ }
+ }
+
+ // Construct template name (templates/linktree-{template})
+ const templateName = `templates/linktree-${template}`;
+ console.log("templateName",templateName)
+ // Render template, fallback to default if template doesn't exist
+ try {
+ return res.render(templateName,{
+ username:username,
+ tree:tree,
+ dp:dp
+ });
+ } catch (renderErr) {
+ // If template doesn't exist, fallback to default
+ console.log(`Template ${templateName} not found, using default:`, renderErr.message);
+ return res.render('templates/linktree-default',{
+ username:username,
+ tree:tree,
+ dp:dp
+ });
+ }
+ }
+
+ return res.render('not_exists', { linkHub: '' })
+};
+
+module.exports = {
+ verifyPassword,
+ getLinkByUsernameAndSource,
+ getProfileByUsername
+};
diff --git a/backend/controller/LinkController.js b/backend/controller/LinkController.js
index dfb6da9..eceab4a 100644
--- a/backend/controller/LinkController.js
+++ b/backend/controller/LinkController.js
@@ -1,8 +1,10 @@
+const LinkAnalytics = require("../model/linkAnalyticsModel");
const Link = require("../model/linkModel");
+const crypto = require('crypto');
const addNewSource=async(req,res)=>{
try{
- const userId=req.userId;
+ const userId=req.userId;
console.log(userId)
const {username,source,destination}=req.body
if(!userId||!source||!username||!destination){
@@ -28,8 +30,11 @@ const addNewSource=async(req,res)=>{
return res.status(409).json({success:false,message:`${normalizedSource} already exists !`})
}
+ // Generate unique linkId
+ const linkId = crypto.randomBytes(16).toString('hex');
+
// Store normalized source to ensure consistency
- const doc=await Link.create({userId,username,source:normalizedSource,destination})
+ const doc=await Link.create({userId,username,source:normalizedSource,destination,linkId})
if(doc)
return res.status(201).json({success:true,message:`${normalizedSource} added !`,link:doc})
@@ -49,7 +54,7 @@ const getAllSource=async(req,res)=>{
return res.status(400).json({success:false,message:"looks like you entered link directely ! please login first"})
}
- const sources=await Link.find({username,userId},{source:1,destination:1,clicked:1,notSeen:1});
+ const sources=await Link.find({username,userId,deletedAt:null},{source:1,destination:1,clicked:1,notSeen:1,visibility:1,linkId:1});
if(!sources)
return res.status(404).json({success:false,message:'sources not found !'})
return res.status(200).json({success:true,message:'sources fetched successfully',sources})
@@ -86,6 +91,9 @@ const deleteLink=async(req,res)=>{
const userId=req.userId;
console.log('updating')
await Link.updateMany({userId},{$set:{notSeen:0}});
+
+ console.log('updating')
+ await LinkAnalytics.updateMany({userId,seen:false},{$set:{seen:true}});
return res.status(201).json({success:true})
}catch(err){
console.log(err);
@@ -158,10 +166,95 @@ const deleteLink=async(req,res)=>{
}
}
+const updateVisibility = async (req, res) => {
+ try {
+ if (!req.userId) {
+ return res.status(401).json({
+ success: false,
+ message: "Authentication required"
+ });
+ }
+
+ const { id, visibility, password } = req.body;
+
+ if (!id || !visibility) {
+ return res.status(400).json({
+ success: false,
+ message: "Link ID and visibility are required"
+ });
+ }
+
+ // Validate visibility value
+ if (!['public', 'unlisted', 'private'].includes(visibility)) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid visibility value. Must be 'public', 'unlisted', or 'private'"
+ });
+ }
+
+ // Find the link
+ const link = await Link.findById(id);
+ if (!link) {
+ return res.status(404).json({
+ success: false,
+ message: "Link not found"
+ });
+ }
+
+ // Verify the link belongs to the user
+ if (link.userId.toString() !== req.userId.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "You don't have permission to update this link"
+ });
+ }
+
+ // Prepare update data
+ const updateData = { visibility };
+
+ // Handle password for private links
+ if (visibility === 'private') {
+ if (!password) {
+ return res.status(400).json({
+ success: false,
+ message: "Password is required for private links"
+ });
+ }
+ // Hash password using bcryptjs (consistent with AuthController)
+ const bcryptjs = require('bcryptjs');
+ const hashedPassword = await bcryptjs.hash(password, 10);
+ updateData.password = hashedPassword;
+ } else {
+ // Clear password for public and unlisted links
+ updateData.password = null;
+ }
+
+ // Update the link
+ const updatedLink = await Link.findByIdAndUpdate(
+ id,
+ { $set: updateData },
+ { new: true }
+ );
+
+ return res.status(200).json({
+ success: true,
+ message: "Link visibility updated successfully",
+ link: updatedLink
+ });
+ } catch (err) {
+ console.log("Update visibility error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
module.exports={
addNewSource,
getAllSource,
deleteLink,
setNotificationToZero,
editLink,
+ updateVisibility,
}
\ No newline at end of file
diff --git a/backend/controller/ProfileController.js b/backend/controller/ProfileController.js
index c48eac7..8a1f516 100644
--- a/backend/controller/ProfileController.js
+++ b/backend/controller/ProfileController.js
@@ -1,5 +1,10 @@
const Profile = require("../model/userProfile");
+const User = require("../model/userModel");
const cloudinary = require('cloudinary')
+const { sendProfileVisitEmail } = require("../lib/mail");
+const geoip = require('geoip-lite');
+const useragent = require('useragent');
+
const updateProfile = async (req, res) => {
let { username, name, location, bio, passion } = req.body;
console.log(name, location, bio, passion);
@@ -97,8 +102,157 @@ const updateProfile = async (req, res) => {
}
}
+const getPublicProfile = async (req, res) => {
+ try {
+ const { username } = req.body;
+ const userId = req.userId; // Optional: userId from verifyTokenOptional middleware (can be null for public access)
+
+ if (!username) {
+ return res.status(400).json({
+ success: false,
+ message: "Username is required"
+ });
+ }
+
+ // No authentication required - public profiles can be viewed by anyone
+ // userId will be null for unauthenticated users, or set if user is logged in
+
+ // Get user
+ const user = await User.findOne({ username, deletedAt: null });
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ // Get user settings
+ const UserSettings = require("../model/userSettingsModel");
+ const settings = await UserSettings.getUserSettings(username, user);
+
+ // Check if profile is public and viewable
+ // Allow access if user is viewing their own profile (for preview) - owners can preview even if private
+ // But all content will still respect permissions from settings
+ const isOwner = userId && user._id.toString() === userId.toString();
+ if (!isOwner && (!settings.profile.isPublic || !settings.profile.allowProfileView)) {
+ return res.status(403).json({
+ success: false,
+ message: "Profile is private"
+ });
+ }
+
+ // Extract device information for profile visit notification
+ const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
+ const geo = geoip.lookup(ip) || { city: "Unknown", country: "Unknown" };
+ const agent = useragent.parse(req.headers['user-agent']);
+ const browser = agent.toAgent();
+ const visitTime = new Date().toLocaleTimeString("en-IN", { hour: '2-digit', minute: '2-digit' });
+
+ const deviceDetails = {
+ ip: ip,
+ city: geo.city || "Unknown",
+ country: geo.country || "Unknown",
+ browser: browser,
+ time: visitTime,
+ };
+
+ // Get visitor information if they're logged in
+ let visitorUsername = null;
+ let visitorName = null;
+ if (userId && !isOwner) {
+ try {
+ const visitor = await User.findById(userId, { username: 1, name: 1 });
+ if (visitor) {
+ visitorUsername = visitor.username;
+ visitorName = visitor.name;
+ }
+ } catch (err) {
+ console.error(`Error fetching visitor info:`, err);
+ }
+ }
+
+ // Send profile visit email notification if enabled
+ if (!isOwner) {
+ try {
+ if (settings && settings.shouldEmailOnProfileView()) {
+ sendProfileVisitEmail(
+ user.email,
+ username,
+ user.name,
+ deviceDetails,
+ visitorUsername,
+ visitorName
+ ).catch(err => {
+ console.error(`Failed to send profile visit email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ }
+ }
+
+ // Get profile
+ const profile = await Profile.findOne({ username, deletedAt: null });
+ if (!profile) {
+ return res.status(404).json({
+ success: false,
+ message: "Profile not found"
+ });
+ }
+
+ // Get public and unlisted links (unlisted appear in ProfilePreview but not linkhub)
+ const Link = require("../model/linkModel");
+ const allLinks = await Link.find({
+ username,
+ deletedAt: null
+ }).select('source destination clicked visibility');
+
+ // Filter public and unlisted links (exclude private)
+ const visibleLinks = allLinks.filter(link =>
+ link.visibility === 'public' || link.visibility === 'unlisted'
+ );
+
+ // Calculate stats (only count public links for stats)
+ const publicLinks = visibleLinks.filter(link => link.visibility === 'public');
+ const totalLinks = publicLinks.length;
+ const totalClicks = publicLinks.reduce((sum, link) => sum + (link.clicked || 0), 0);
+
+ // Build response based on settings
+ const response = {
+ success: true,
+ profile: {
+ username: profile.username,
+ name: profile.name || user.name,
+ location: settings.profile.showLocation ? profile.location : null,
+ passion: settings.profile.showPassion ? profile.passion : null,
+ bio: settings.profile.showBio ? profile.bio : null,
+ image: settings.profile.showProfileImage ? profile.image : null
+ },
+ links: visibleLinks, // Include both public and unlisted links
+ settings: {
+ profile: settings.profile,
+ links: settings.links,
+ privacy: settings.privacy
+ },
+ stats: {
+ totalLinks: settings.links.showLinkCount ? totalLinks : null,
+ totalClicks: settings.links.showClickStats ? totalClicks : null
+ }
+ };
+
+ return res.status(200).json(response);
+ } catch (err) {
+ console.log("Get public profile error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
module.exports = {
updateProfile,
getProfile,
- updateProfilePic
+ updateProfilePic,
+ getPublicProfile
};
diff --git a/backend/controller/ProjectController.js b/backend/controller/ProjectController.js
new file mode 100644
index 0000000..db15d07
--- /dev/null
+++ b/backend/controller/ProjectController.js
@@ -0,0 +1,67 @@
+const Project = require("../model/projectModel");
+
+// Get available templates (only those with status: true)
+const getAvailableTemplates = async (req, res) => {
+ try {
+ const project = await Project.getProjectConfig();
+
+ if (!project) {
+ return res.status(404).json({
+ success: false,
+ message: "Project configuration not found"
+ });
+ }
+
+ // Filter templates where status is true and format for frontend
+ const availableTemplates = project.availableTemplates
+ .filter(t => t.status === true)
+ .map(t => ({
+ name: t.template,
+ label: t.displayName || t.template
+ }));
+
+ return res.status(200).json({
+ success: true,
+ message: "Available templates retrieved successfully",
+ templates: availableTemplates
+ });
+ } catch (err) {
+ console.log("Get available templates error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
+// Get all templates (including disabled ones) - for admin use
+const getAllTemplates = async (req, res) => {
+ try {
+ const project = await Project.getProjectConfig();
+
+ if (!project) {
+ return res.status(404).json({
+ success: false,
+ message: "Project configuration not found"
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "All templates retrieved successfully",
+ templates: project.availableTemplates
+ });
+ } catch (err) {
+ console.log("Get all templates error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
+module.exports = {
+ getAvailableTemplates,
+ getAllTemplates
+};
+
diff --git a/backend/controller/SearchController.js b/backend/controller/SearchController.js
new file mode 100644
index 0000000..eddbb08
--- /dev/null
+++ b/backend/controller/SearchController.js
@@ -0,0 +1,84 @@
+const User = require("../model/userModel");
+const UserSettings = require("../model/userSettingsModel");
+const UserProfile = require("../model/userProfile");
+
+// Search for users
+const searchUsers = async (req, res) => {
+ try {
+ const { query } = req.body;
+
+ if (!query || !query.trim()) {
+ return res.status(400).json({
+ success: false,
+ message: "Search query is required"
+ });
+ }
+
+ const searchQuery = query.trim().toLowerCase();
+
+ // Find users whose username or name matches the search query
+ // Only include users that are not deleted
+ const users = await User.find({
+ $or: [
+ { username: { $regex: searchQuery, $options: 'i' } },
+ { name: { $regex: searchQuery, $options: 'i' } }
+ ],
+ deletedAt: null
+ }).select('username name email').limit(20);
+
+ if (!users || users.length === 0) {
+ return res.status(200).json({
+ success: true,
+ message: "No users found",
+ results: []
+ });
+ }
+
+ // Filter users based on their search settings
+ const searchableUsers = [];
+
+ for (const user of users) {
+ try {
+ const settings = await UserSettings.findOne({
+ userId: user._id,
+ deletedAt: null
+ });
+
+ // Check if user is searchable
+ // User must have isPublic=true and allowSearch=true
+ if (settings && settings.isSearchable()) {
+ // Get profile for image
+ const profile = await UserProfile.findOne({
+ username: user.username
+ }).select('image');
+
+ searchableUsers.push({
+ username: user.username,
+ name: user.name || user.username,
+ image: profile?.image || null
+ });
+ }
+ } catch (err) {
+ // If settings don't exist or error, skip this user
+ // (default settings make profile not searchable)
+ continue;
+ }
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "Search completed",
+ results: searchableUsers
+ });
+ } catch (err) {
+ console.log("Search users error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
+module.exports = {
+ searchUsers
+};
diff --git a/backend/controller/SettingsController.js b/backend/controller/SettingsController.js
new file mode 100644
index 0000000..589e97a
--- /dev/null
+++ b/backend/controller/SettingsController.js
@@ -0,0 +1,325 @@
+const UserSettings = require("../model/userSettingsModel");
+const User = require("../model/userModel");
+const Project = require("../model/projectModel");
+
+// Get user settings
+const getSettings = async (req, res) => {
+ try {
+ const userId = req.userId;
+ const { username } = req.body;
+
+ if (!userId) {
+ return res.status(401).json({
+ success: false,
+ message: "Authentication required"
+ });
+ }
+
+ // Get user info if username not provided
+ let userData = null;
+ if (!username) {
+ userData = await User.findById(userId);
+ if (!userData) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+ }
+
+ // Use the static method to get or create settings
+ const settings = await UserSettings.getUserSettings(
+ username || userId,
+ userData
+ );
+
+ if (!settings) {
+ return res.status(404).json({
+ success: false,
+ message: "Settings not found"
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "Settings retrieved successfully",
+ settings
+ });
+ } catch (err) {
+ console.log("Get settings error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error"
+ });
+ }
+};
+
+// Update user settings
+const updateSettings = async (req, res) => {
+ try {
+ const userId = req.userId;
+ const { username, category, field, value, ...settingsData } = req.body;
+ console.log("category",category)
+ console.log("field",field)
+ console.log("value",value)
+ console.log("settingsData",settingsData)
+ if (!userId) {
+ return res.status(401).json({
+ success: false,
+ message: "Authentication required"
+ });
+ }
+
+ // Get user info
+ const userData = await User.findById(userId);
+ if (!userData) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found"
+ });
+ }
+
+ // Get or create settings
+ let settings = await UserSettings.getUserSettings(userId, userData);
+
+ if (!settings) {
+ return res.status(404).json({
+ success: false,
+ message: "Settings not found"
+ });
+ }
+
+ // Handle single field update (new format)
+ if (category && field !== undefined && value !== undefined) {
+ const validCategories = ['profile', 'links', 'search', 'privacy', 'notifications', 'template'];
+
+ if (!validCategories.includes(category)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid category. Valid options are: ${validCategories.join(', ')}`
+ });
+ }
+
+ // Handle template update
+ if (category === 'template') {
+ // Get valid templates from database
+ const project = await Project.getProjectConfig();
+ const validTemplates = project && project.availableTemplates
+ ? project.availableTemplates
+ .filter(t => t.status === true)
+ .map(t => t.template)
+ : ['default']; // Fallback to default if project config not found
+
+ if (!validTemplates.includes(value)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid template. Valid options are: ${validTemplates.join(', ')}`
+ });
+ }
+ settings.template = value;
+ await settings.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Template updated successfully",
+ settings
+ });
+ }
+
+ // Check if profile is private and field requires public profile
+ const requiresPublicProfile = [
+ 'showInSearch',
+ 'allowProfileView',
+ 'showEmail'
+ ];
+
+ const searchRequiresPublicProfile = [
+ 'allowSearch',
+ 'showInFeatured'
+ ];
+
+ // If profile is private, don't allow updates to fields that require public profile
+ if (category === 'profile' && requiresPublicProfile.includes(field)) {
+ if (!settings.profile || !settings.profile.isPublic) {
+ return res.status(400).json({
+ success: false,
+ message: "Cannot update this setting. Profile must be public first."
+ });
+ }
+ }
+
+ // If profile is private, don't allow updates to search fields that require public profile
+ if (category === 'search' && searchRequiresPublicProfile.includes(field)) {
+ if (!settings.profile || !settings.profile.isPublic) {
+ return res.status(400).json({
+ success: false,
+ message: "Cannot update this setting. Profile must be public first."
+ });
+ }
+ }
+
+ // Validate and update the specific field
+ if (category === 'profile') {
+ const validFields = ['isPublic', 'showInSearch', 'allowProfileView', 'showEmail', 'showLocation', 'showBio', 'showPassion', 'showProfileImage'];
+ if (!validFields.includes(field)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid field for profile category. Valid options are: ${validFields.join(', ')}`
+ });
+ }
+ // Convert boolean fields properly
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+ settings.set(`profile.${field}`, boolValue);
+ settings.markModified('profile');
+ } else if (category === 'links') {
+ const validFields = ['showLinkCount', 'showClickStats'];
+ if (!validFields.includes(field)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid field for links category. Valid options are: ${validFields.join(', ')}`
+ });
+ }
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+ settings.set(`links.${field}`, boolValue);
+ settings.markModified('links');
+ } else if (category === 'search') {
+ const validFields = ['allowSearch', 'showInFeatured', 'searchKeywords'];
+ if (!validFields.includes(field)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid field for search category. Valid options are: ${validFields.join(', ')}`
+ });
+ }
+ if (field === 'searchKeywords') {
+ // Validate that value is an array
+ if (!Array.isArray(value)) {
+ return res.status(400).json({
+ success: false,
+ message: "searchKeywords must be an array"
+ });
+ }
+ settings.set('search.searchKeywords', value);
+ } else {
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+ settings.set(`search.${field}`, boolValue);
+ }
+ settings.markModified('search');
+ } else if (category === 'privacy') {
+ const validFields = ['showAnalytics', 'showLastUpdated', 'requireAuth'];
+ if (!validFields.includes(field)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid field for privacy category. Valid options are: ${validFields.join(', ')}`
+ });
+ }
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+ settings.set(`privacy.${field}`, boolValue);
+ settings.markModified('privacy');
+ } else if (category === 'notifications') {
+ const validFields = ['emailOnNewClick', 'emailOnProfileView', 'emailOnLinkHubView', 'weeklyReport'];
+ if (!validFields.includes(field)) {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid field for notifications category. Valid options are: ${validFields.join(', ')}`
+ });
+ }
+ // Convert value to boolean if needed
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+
+ // Use set() method with dot notation for nested fields (more reliable in Mongoose)
+ settings.set(`notifications.${field}`, boolValue);
+ settings.markModified('notifications');
+
+ console.log(`Updating notifications.${field} to ${boolValue} (type: ${typeof boolValue}) for user ${userId}`);
+ }
+
+ const savedSettings = await settings.save();
+
+ // Verify the save worked (especially for notifications)
+ if (category === 'notifications') {
+ const boolValue = typeof value === 'string' ? value === 'true' : Boolean(value);
+ const verification = await UserSettings.findById(settings._id);
+ console.log(`Verification - notifications.${field} saved as:`, verification?.notifications?.[field], `(expected: ${boolValue})`);
+ }
+
+ return res.status(200).json({
+ success: true,
+ message: "Setting updated successfully",
+ settings
+ });
+ }
+
+ // Handle bulk update (old format for backward compatibility)
+ // Update template if provided
+ if (settingsData.template !== undefined) {
+ // Get valid templates from database
+ const project = await Project.getProjectConfig();
+ const validTemplates = project && project.availableTemplates
+ ? project.availableTemplates
+ .filter(t => t.status === true)
+ .map(t => t.template)
+ : ['default']; // Fallback to default if project config not found
+
+ if (validTemplates.includes(settingsData.template)) {
+ settings.template = settingsData.template;
+ } else {
+ return res.status(400).json({
+ success: false,
+ message: `Invalid template. Valid options are: ${validTemplates.join(', ')}`
+ });
+ }
+ }
+
+ // Update settings - use Object.assign and markModified for nested objects
+ if (settingsData.profile) {
+ Object.assign(settings.profile, settingsData.profile);
+ settings.markModified('profile');
+ }
+ if (settingsData.links) {
+ Object.assign(settings.links, settingsData.links);
+ settings.markModified('links');
+ }
+ if (settingsData.search) {
+ // Handle searchKeywords array separately
+ if (settingsData.search.searchKeywords !== undefined) {
+ settings.search.searchKeywords = settingsData.search.searchKeywords;
+ }
+ // Update other search fields
+ if (settingsData.search.allowSearch !== undefined) {
+ settings.search.allowSearch = settingsData.search.allowSearch;
+ }
+ if (settingsData.search.showInFeatured !== undefined) {
+ settings.search.showInFeatured = settingsData.search.showInFeatured;
+ }
+ settings.markModified('search');
+ }
+ if (settingsData.privacy) {
+ Object.assign(settings.privacy, settingsData.privacy);
+ settings.markModified('privacy');
+ }
+ if (settingsData.notifications) {
+ Object.assign(settings.notifications, settingsData.notifications);
+ settings.markModified('notifications');
+ }
+
+ await settings.save();
+
+ return res.status(200).json({
+ success: true,
+ message: "Settings updated successfully",
+ settings
+ });
+ } catch (err) {
+ console.error("Update settings error:", err);
+ return res.status(500).json({
+ success: false,
+ message: "Server internal error",
+ error: process.env.NODE_ENV === 'development' ? err.message : undefined
+ });
+ }
+};
+
+module.exports = {
+ getSettings,
+ updateSettings
+};
diff --git a/backend/doc/README.md b/backend/doc/README.md
new file mode 100644
index 0000000..22201f6
--- /dev/null
+++ b/backend/doc/README.md
@@ -0,0 +1,192 @@
+# Database Models Documentation
+
+This directory contains comprehensive documentation for all database models used in the All in1 url application.
+
+## Overview
+
+The All in1 url application uses MongoDB with Mongoose ODM. All models are designed with:
+- **Timestamps**: Automatic `createdAt` and `updatedAt` fields
+- **Soft Deletes**: `deletedAt` field for data retention
+- **Indexes**: Optimized for common query patterns
+- **Relationships**: Foreign key references between models
+
+## Models
+
+### 1. [Link Model](./linkModel.md)
+Stores user-created personalized links (bridges) that redirect to social media profiles or external destinations.
+
+**Key Features:**
+- Unique `linkId` for each link
+- User association via `userId` and `username`
+- Click tracking counters
+- Soft delete support
+
+### 2. [Link Analytics Model](./linkAnalyticsModel.md)
+Tracks detailed information about every click on user links, including timestamps, device info, location, and browser details.
+
+**Key Features:**
+- Comprehensive timestamp tracking (date, time, timezone)
+- Device, OS, and browser information
+- Geographic location data
+- Full timestamp formatting methods
+
+### 3. [User Model](./userModel.md)
+Core authentication and account information for registered users.
+
+**Key Features:**
+- Email and username authentication
+- Hashed password storage
+- Account management
+- Soft delete support
+
+### 4. [User Profile Model](./userProfile.md)
+Public-facing profile information including name, bio, location, passion, and profile image.
+
+**Key Features:**
+- Display information separate from auth data
+- Profile customization
+- Public profile support
+
+### 5. [User Settings Model](./userSettingsModel.md)
+Privacy and visibility preferences controlling what information is visible to other users.
+
+**Key Features:**
+- Profile visibility controls
+- Link visibility settings (public/private/unlisted)
+- Search and discovery settings
+- Privacy preferences
+- Helper methods for visibility checks
+
+### 6. [OTP Model](./otpModel.md)
+Temporary verification codes for email verification, password reset, and authentication.
+
+**Key Features:**
+- Automatic expiration (5 minutes)
+- Hashed OTP storage
+- Email-based verification
+
+## Model Relationships
+
+```
+User
+├── UserProfile (1:1 via username)
+├── UserSettings (1:1 via userId/username)
+├── Links (1:many via userId)
+└── LinkAnalytics (1:many via userId)
+
+Link
+└── LinkAnalytics (1:many via linkId)
+```
+
+## Common Patterns
+
+### Soft Deletes
+All models support soft deletes using the `deletedAt` field:
+```javascript
+// Soft delete
+record.deletedAt = new Date();
+await record.save();
+
+// Query active records
+const active = await Model.find({ deletedAt: null });
+```
+
+### Timestamps
+All models automatically track `createdAt` and `updatedAt`:
+```javascript
+// Automatically set on creation
+const record = new Model({ ... });
+await record.save(); // createdAt and updatedAt set automatically
+
+// updatedAt automatically updated on save
+record.field = 'new value';
+await record.save(); // updatedAt updated automatically
+```
+
+### Foreign Keys
+Models use references for relationships:
+```javascript
+// In Link model
+userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'user'
+}
+
+// Populate when querying
+const link = await Link.findById(id).populate('userId');
+```
+
+## Best Practices
+
+1. **Always filter by `deletedAt: null`** when querying active records
+2. **Hash sensitive data** (passwords, OTPs) before storing
+3. **Use indexes** for frequently queried fields
+4. **Validate data** before saving
+5. **Use soft deletes** instead of hard deletes for data retention
+6. **Check relationships** before deleting related records
+
+## Security Considerations
+
+- **Passwords**: Always hash using bcrypt or similar
+- **OTPs**: Hash before storage, expire quickly
+- **Email**: Validate format, handle case-insensitive matching
+- **Privacy**: Respect user settings for data visibility
+- **GDPR**: Handle user data according to privacy regulations
+
+## Query Examples
+
+### Finding Active User Links
+```javascript
+const links = await Link.find({
+ userId: user._id,
+ deletedAt: null
+});
+```
+
+### Getting User Settings
+```javascript
+const settings = await UserSettings.getUserSettings(username);
+```
+
+### Finding Searchable Profiles
+```javascript
+const profiles = await UserSettings.find({
+ 'profile.isPublic': true,
+ 'search.allowSearch': true,
+ deletedAt: null
+});
+```
+
+### Querying Analytics
+```javascript
+const clicks = await LinkAnalytics.find({
+ linkId: 'linkedin',
+ deletedAt: null
+}).sort({ clickDate: -1 });
+```
+
+## Documentation Structure
+
+Each model documentation file includes:
+- **Overview**: Purpose and use case
+- **Schema Fields**: Detailed field documentation
+ - Type and constraints
+ - Purpose and examples
+ - Why it's required/optional
+ - Enum values (if applicable)
+- **Indexes**: Performance optimizations
+- **Relationships**: Model connections
+- **Usage Examples**: Code examples
+- **Notes**: Important considerations
+
+## Contributing
+
+When adding new models or modifying existing ones:
+1. Update the model file
+2. Update the corresponding documentation
+3. Update this README if needed
+4. Document any breaking changes
+
+## Questions?
+
+Refer to individual model documentation files for detailed information about each model's fields, methods, and usage patterns.
diff --git a/backend/doc/linkAnalyticsModel.md b/backend/doc/linkAnalyticsModel.md
new file mode 100644
index 0000000..bd15756
--- /dev/null
+++ b/backend/doc/linkAnalyticsModel.md
@@ -0,0 +1,289 @@
+# Link Analytics Model Documentation
+
+## Overview
+The Link Analytics model stores detailed information about every click on user links. This includes timestamp data, device information, location, browser details, and more. Used for comprehensive analytics and reporting.
+
+## Model Name
+`linkAnalytics` (collection name in MongoDB)
+
+## Schema Fields
+
+### `linkId` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Indexed**: Yes
+- **Ref**: `link`
+- **Purpose**: Foreign key reference to the Link model. Identifies which link was clicked. Used to aggregate all clicks for a specific link.
+- **Example**: `"linkedin"`, `"github"`
+- **Why Required**: Essential for linking analytics data to specific links and querying click history for a link.
+
+### `userId` (ObjectId, Required)
+- **Type**: mongoose.Schema.Types.ObjectId
+- **Required**: Yes
+- **Indexed**: Yes
+- **Ref**: `user`
+- **Purpose**: Foreign key reference to the User model. Identifies which user owns the link that was clicked. Used for user-level analytics aggregation.
+- **Example**: `ObjectId("507f1f77bcf86cd799439011")`
+- **Why Required**: Enables efficient user-based analytics queries and reporting.
+
+### `username` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Indexed**: Yes
+- **Purpose**: Username of the link owner. Stored redundantly for faster queries without joins. Used for filtering analytics by username.
+- **Example**: `"johndoe"`
+- **Why Required**: Allows quick username-based queries without joining the User collection.
+
+### `clickDate` (Date, Required)
+- **Type**: Date
+- **Required**: Yes
+- **Default**: `Date.now`
+- **Indexed**: Yes
+- **Purpose**: The exact date and time when the click occurred. Primary timestamp for all analytics queries. Stored with millisecond precision.
+- **Example**: `2024-01-15T14:30:45.123Z`
+- **Why Required**: Core field for time-based analytics, sorting, and filtering clicks by date/time.
+
+### `timestamp` (Object, Auto-generated)
+- **Type**: Object containing detailed timestamp information
+- **Auto-generated**: Yes (via pre-save middleware)
+- **Purpose**: Provides multiple formatted versions of the click timestamp for easy display and querying without date manipulation.
+
+#### `timestamp.date` (String)
+- **Format**: `YYYY-MM-DD`
+- **Example**: `"2024-01-15"`
+- **Purpose**: Date-only string for date-based grouping and filtering.
+
+#### `timestamp.time` (String)
+- **Format**: `HH:MM:SS`
+- **Example**: `"14:30:45"`
+- **Purpose**: Time-only string for time-based analysis.
+
+#### `timestamp.datetime` (String)
+- **Format**: `YYYY-MM-DD HH:MM:SS`
+- **Example**: `"2024-01-15 14:30:45"`
+- **Purpose**: Human-readable full datetime string.
+
+#### `timestamp.unixTimestamp` (Number)
+- **Format**: Unix timestamp in milliseconds
+- **Example**: `1705332645123`
+- **Purpose**: Numeric timestamp for calculations and comparisons.
+
+#### `timestamp.timezone` (String)
+- **Example**: `"America/New_York"`, `"UTC"`, `"Asia/Tokyo"`
+- **Purpose**: Detected timezone of the server/environment when click was recorded.
+
+#### `timestamp.timezoneOffset` (Number)
+- **Format**: Minutes offset from UTC
+- **Example**: `300` (UTC-5), `-60` (UTC+1)
+- **Purpose**: Timezone offset for accurate time conversion.
+
+### `location` (Object, Optional)
+- **Type**: Object
+- **Required**: No
+- **Purpose**: Geographic information about where the click originated.
+
+#### `location.country` (String, Optional)
+- **Example**: `"United States"`, `"India"`, `"United Kingdom"`
+- **Purpose**: Country where the click originated. Useful for geographic analytics.
+
+#### `location.city` (String, Optional)
+- **Example**: `"New York"`, `"Mumbai"`, `"London"`
+- **Purpose**: City where the click originated. Provides more granular location data.
+
+#### `location.region` (String, Optional)
+- **Example**: `"New York"`, `"Maharashtra"`, `"England"`
+- **Purpose**: State/province/region information.
+
+#### `location.ipAddress` (String, Optional)
+- **Example**: `"192.168.1.1"`, `"2001:0db8:85a3:0000:0000:8a2e:0370:7334"`
+- **Purpose**: IP address of the clicker. Used for location detection and fraud prevention. Should be handled according to privacy regulations.
+
+### `device` (Object, Optional)
+- **Type**: Object
+- **Required**: No
+- **Purpose**: Information about the device used to click the link.
+
+#### `device.type` (String, Optional)
+- **Enum Values**: `'mobile'`, `'desktop'`, `'tablet'`, `'unknown'`
+- **Default**: `'unknown'`
+- **Purpose**: Type of device. Used for device-based analytics and responsive design insights.
+
+#### `device.brand` (String, Optional)
+- **Example**: `"Apple"`, `"Samsung"`, `"Google"`
+- **Purpose**: Device manufacturer/brand.
+
+#### `device.model` (String, Optional)
+- **Example**: `"iPhone 14 Pro"`, `"Galaxy S23"`, `"Pixel 7"`
+- **Purpose**: Specific device model.
+
+### `os` (Object, Optional)
+- **Type**: Object
+- **Required**: No
+- **Purpose**: Operating system information.
+
+#### `os.name` (String, Optional)
+- **Example**: `"Windows"`, `"macOS"`, `"iOS"`, `"Android"`, `"Linux"`
+- **Purpose**: Operating system name. Used for OS-based analytics.
+
+#### `os.version` (String, Optional)
+- **Example**: `"14.2"`, `"13.0"`, `"Windows 11"`
+- **Purpose**: OS version for more detailed analytics.
+
+### `browser` (Object, Optional)
+- **Type**: Object
+- **Required**: No
+- **Purpose**: Browser information.
+
+#### `browser.name` (String, Optional)
+- **Example**: `"Chrome"`, `"Firefox"`, `"Safari"`, `"Edge"`
+- **Purpose**: Browser name. Used for browser compatibility insights.
+
+#### `browser.version` (String, Optional)
+- **Example**: `"120.0.0.0"`, `"119.0"`
+- **Purpose**: Browser version.
+
+### `referrer` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Purpose**: The URL of the page that referred the user to the link. Useful for understanding traffic sources.
+- **Example**: `"https://twitter.com"`, `"https://google.com"`
+
+### `userAgent` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Purpose**: Full user agent string from the HTTP request. Used for parsing device, OS, and browser information.
+- **Example**: `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."`
+
+### `deletedAt` (Date, Optional)
+- **Type**: Date
+- **Required**: No
+- **Default**: `null`
+- **Purpose**: Soft delete field. When an analytics record is deleted, this field is set instead of removing the record. Allows for data retention and recovery.
+
+### `createdAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp when the analytics record was created. Should match `clickDate` for new records.
+
+### `updatedAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp when the record was last modified. Rarely changes after creation.
+
+## Indexes
+
+### Single Field Indexes
+- `linkId`: For fast queries by link
+- `userId`: For fast queries by user
+- `username`: For fast queries by username
+- `clickDate`: For date-based queries
+
+### Compound Indexes
+- `{ linkId: 1, clickDate: -1 }`: Get clicks for a link ordered by date (descending)
+- `{ userId: 1, clickDate: -1 }`: Get clicks for a user ordered by date (descending)
+- `{ linkId: 1, deletedAt: 1 }`: Soft delete queries for specific links
+- `{ userId: 1, deletedAt: 1 }`: Soft delete queries for specific users
+
+## Virtual Fields
+
+### `formattedDate` (Virtual)
+- **Type**: String
+- **Format**: Human-readable formatted date
+- **Example**: `"January 15, 2024, 02:30:45 PM"`
+- **Purpose**: Easy-to-read date format for display in UI.
+
+### `isoString` (Virtual)
+- **Type**: String
+- **Format**: ISO 8601 string
+- **Example**: `"2024-01-15T14:30:45.123Z"`
+- **Purpose**: Standard ISO format for API responses.
+
+## Instance Methods
+
+### `getFullTimestamp()`
+Returns a comprehensive timestamp object with all formatted versions and components.
+
+**Returns**:
+```javascript
+{
+ date: "2024-01-15",
+ time: "14:30:45",
+ datetime: "2024-01-15 14:30:45",
+ unixTimestamp: 1705332645123,
+ isoString: "2024-01-15T14:30:45.123Z",
+ formatted: "January 15, 2024, 02:30:45 PM",
+ timezone: "America/New_York",
+ timezoneOffset: 300,
+ year: 2024,
+ month: 1,
+ day: 15,
+ hour: 14,
+ minute: 30,
+ second: 45,
+ millisecond: 123,
+ dayOfWeek: "Monday",
+ dateOnly: "01/15/2024",
+ timeOnly: "02:30:45 PM",
+ utcDate: "Mon, 15 Jan 2024 14:30:45 GMT",
+ utcISO: "2024-01-15T14:30:45.123Z"
+}
+```
+
+## Relationships
+- **Belongs to**: Link (via `linkId`)
+- **Belongs to**: User (via `userId`)
+
+## Usage Examples
+
+### Creating an Analytics Record
+```javascript
+const analytics = new LinkAnalytics({
+ linkId: 'linkedin',
+ userId: user._id,
+ username: 'johndoe',
+ clickDate: new Date(),
+ location: {
+ country: 'United States',
+ city: 'New York',
+ ipAddress: '192.168.1.1'
+ },
+ device: {
+ type: 'mobile',
+ brand: 'Apple',
+ model: 'iPhone 14'
+ },
+ os: {
+ name: 'iOS',
+ version: '17.0'
+ },
+ browser: {
+ name: 'Safari',
+ version: '17.0'
+ },
+ referrer: 'https://twitter.com',
+ userAgent: req.headers['user-agent']
+});
+await analytics.save();
+```
+
+### Querying Clicks for a Link
+```javascript
+const clicks = await LinkAnalytics.find({
+ linkId: 'linkedin',
+ deletedAt: null
+}).sort({ clickDate: -1 });
+```
+
+### Getting Full Timestamp Information
+```javascript
+const analytics = await LinkAnalytics.findById(id);
+const timestamp = analytics.getFullTimestamp();
+console.log(timestamp.formatted); // "January 15, 2024, 02:30:45 PM"
+```
+
+## Notes
+- The `timestamp` object is automatically populated by pre-save middleware
+- Always filter by `deletedAt: null` when querying active records
+- IP addresses should be handled according to GDPR/privacy regulations
+- Consider data retention policies for analytics records
+- The model supports high-volume writes (many clicks per second)
diff --git a/backend/doc/linkModel.md b/backend/doc/linkModel.md
new file mode 100644
index 0000000..7e6a1b4
--- /dev/null
+++ b/backend/doc/linkModel.md
@@ -0,0 +1,248 @@
+# Link Model Documentation
+
+## Overview
+The Link model stores information about user-created personalized links (bridges) that redirect users from a custom URL to their social media profiles or external destinations.
+
+## Model Name
+`link` (collection name in MongoDB)
+
+## Schema Fields
+
+### `username` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: No (multiple links per user)
+- **Purpose**: Identifies which user owns this link. Used for querying all links belonging to a specific user.
+- **Example**: `"johndoe"`
+- **Why Required**: Essential for associating links with users and filtering user-specific links.
+
+### `userId` (ObjectId, Required)
+- **Type**: mongoose.Schema.Types.ObjectId
+- **Required**: Yes
+- **Unique**: No
+- **Ref**: `user`
+- **Purpose**: Foreign key reference to the User model. Provides a direct relationship to the user who owns this link. More efficient than username for database queries.
+- **Example**: `ObjectId("507f1f77bcf86cd799439011")`
+- **Why Required**: Enables efficient joins and referential integrity. Used for user-based queries and analytics.
+
+### `linkId` (String, Required, Unique)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: Yes
+- **Purpose**: Unique identifier for each link. Used as a foreign key in the linkAnalytics model to track clicks and analytics for this specific link. Also used in URL generation (e.g., `allin1url.in/username/linkId`).
+- **Example**: `"linkedin"`, `"github"`, `"instagram"`
+- **Why Required**: Essential for:
+ - Creating unique URLs for each link
+ - Linking analytics data to specific links
+ - Identifying links in API requests
+ - Preventing duplicate links for the same platform per user
+
+### `source` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Purpose**: The platform or service name (e.g., "linkedin", "github", "instagram"). This is typically the part of the URL path that identifies the platform.
+- **Example**: `"linkedin"`, `"github"`, `"twitter"`
+- **Why Required**: Used to generate the personalized URL and identify the type of link. Must be unique per user (enforced by linkId).
+
+### `destination` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Purpose**: The actual URL where the link redirects to. This is the user's profile URL on the specified platform.
+- **Example**: `"https://www.linkedin.com/in/johndoe"`, `"https://github.com/johndoe"`
+- **Why Required**: The core purpose of the link - where users are redirected when they click the personalized link.
+
+### `notSeen` (Number, Optional)
+- **Type**: Number
+- **Required**: No
+- **Default**: 0
+- **Purpose**: Counter for tracking how many times the link has been viewed but not clicked. Useful for analytics to understand engagement rates.
+- **Example**: `5`
+- **Why Optional**: Not critical for core functionality, but useful for analytics.
+
+### `clicked` (Number, Optional)
+- **Type**: Number
+- **Required**: No
+- **Default**: 0
+- **Purpose**: Counter for total number of clicks on this link. Used for displaying click statistics to users.
+- **Example**: `42`
+- **Why Optional**: Can be calculated from analytics, but storing it here provides faster access for display purposes.
+
+### `visibility` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Default**: `'public'`
+- **Enum Values**: `'public'`, `'unlisted'`, `'private'`
+- **Indexed**: Yes
+- **Purpose**: Controls link visibility and accessibility:
+ - **`'public'`**: Visible in profile preview, searches by other users, and link hub. Fully accessible without restrictions.
+ - **`'unlisted'`**: Visible in profile preview, but NOT shown in link hub. Accessible via direct URL. Does not require password.
+ - **`'private'`**: NOT visible in profile preview or link hub. If accessed directly, requires password to access. Completely hidden from public view.
+- **Example**: `"public"`, `"unlisted"`, `"private"`
+- **Why Required**: Essential for privacy control. Defaults to `'public'` for better sharing and discoverability - links are public by default, but users can change to `'unlisted'` or `'private'` for privacy.
+
+### `password` (String, Optional)
+- **Type**: String (hashed using bcryptjs)
+- **Required**: Yes when `visibility` is `'private'`, otherwise optional
+- **Default**: `null`
+- **Purpose**: Hashed password for private links. Used to protect private links - users must enter the password to access the link via direct URL. **Should always be hashed** before storage (using bcryptjs). Only used when `visibility` is `'private'`.
+- **Example**: `"$2a$10$hashedpasswordstring..."` (hashed) or `null`
+- **Why Required for Private**: Private links require password protection. Public and unlisted links don't require passwords.
+
+### `deletedAt` (Date, Optional)
+- **Type**: Date
+- **Required**: No
+- **Default**: null
+- **Purpose**: Soft delete field. When a link is deleted, this field is set to the deletion timestamp instead of actually removing the record. Allows for:
+ - Data recovery
+ - Audit trails
+ - Analytics on deleted links
+- **Example**: `2024-01-15T10:30:00.000Z` or `null`
+- **Why Optional**: Links are active by default. Only set when a link is deleted.
+
+### `createdAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the link was created. Useful for:
+ - Displaying creation date to users
+ - Sorting links by creation date
+ - Analytics on link creation patterns
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+### `updatedAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the link was last modified. Automatically updated whenever the document is saved.
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+## Indexes
+- `userId`: Indexed for fast user-based queries
+- `linkId`: Indexed for fast link lookups (unique index)
+- `visibility`: Indexed for visibility-based queries
+- `{ userId: 1, visibility: 1, deletedAt: 1 }`: Compound index for querying user's links by visibility
+- `{ username: 1, visibility: 1, deletedAt: 1 }`: Compound index for querying links by username and visibility
+
+## Relationships
+- **Belongs to**: User (via `userId`)
+- **Has many**: LinkAnalytics (via `linkId`)
+
+## Instance Methods
+
+### `isAccessible(requirePassword = false)`
+Checks if the link is accessible based on its visibility.
+
+**Parameters**:
+- `requirePassword` (Boolean, Optional): If `true`, checks if password is set for unlisted links
+
+**Returns**: Boolean
+
+**Logic**:
+- Returns `false` if link is deleted
+- Returns `true` if visibility is `'public'`
+- Returns `true` for `'unlisted'` if password is not required or password exists
+- Returns `false` for `'private'` links
+
+### `shouldShowInProfile()`
+Checks if link should be displayed in profile/hub.
+
+**Returns**: Boolean - `true` only if visibility is `'public'` and link is not deleted
+
+### `shouldShowInSearch()`
+Checks if link should appear in search results.
+
+**Returns**: Boolean - `true` only if visibility is `'public'` and link is not deleted
+
+## Usage Examples
+
+### Creating a Public Link
+```javascript
+const link = new Link({
+ username: 'johndoe',
+ userId: user._id,
+ linkId: 'linkedin',
+ source: 'linkedin',
+ destination: 'https://www.linkedin.com/in/johndoe',
+ visibility: 'public'
+});
+await link.save();
+```
+
+### Creating an Unlisted Link with Password
+```javascript
+const bcrypt = require('bcrypt');
+const password = 'mySecretPassword';
+const hashedPassword = await bcrypt.hash(password, 10);
+
+const link = new Link({
+ username: 'johndoe',
+ userId: user._id,
+ linkId: 'private-portfolio',
+ source: 'portfolio',
+ destination: 'https://myportfolio.com',
+ visibility: 'unlisted',
+ password: hashedPassword
+});
+await link.save();
+```
+
+### Creating a Private Link
+```javascript
+const link = new Link({
+ username: 'johndoe',
+ userId: user._id,
+ linkId: 'secret-link',
+ source: 'secret',
+ destination: 'https://secret.com',
+ visibility: 'private'
+ // password not needed for private links
+});
+await link.save();
+```
+
+### Querying Public Links for Profile
+```javascript
+const publicLinks = await Link.find({
+ userId: user._id,
+ visibility: 'public',
+ deletedAt: null
+});
+```
+
+### Checking Link Accessibility
+```javascript
+const link = await Link.findOne({ linkId: 'linkedin' });
+
+if (link.visibility === 'unlisted' && link.password) {
+ // Show password form
+ // Verify password: await bcrypt.compare(enteredPassword, link.password);
+}
+
+if (link.shouldShowInProfile()) {
+ // Display in profile
+}
+```
+
+### Querying User Links
+```javascript
+const userLinks = await Link.find({
+ userId: user._id,
+ deletedAt: null
+});
+```
+
+### Soft Delete
+```javascript
+link.deletedAt = new Date();
+await link.save();
+```
+
+## Notes
+- The `linkId` should typically match the `source` value for consistency, but can be customized
+- When a link is deleted, set `deletedAt` instead of using `remove()` or `deleteOne()`
+- Always filter by `deletedAt: null` when querying active links
+- **Visibility defaults to `'public'`** for better sharing and discoverability - links are public by default, but users can change to `'unlisted'` or `'private'` for privacy
+- **Password must be hashed** before storing (use bcrypt, argon2, or similar)
+- For `'unlisted'` links, password is required - validate this in your controller
+- Use helper methods (`shouldShowInProfile()`, `shouldShowInSearch()`) for consistent visibility checks
+- Public links are visible everywhere (profile, search, hub)
+- Unlisted links are accessible via direct URL but require password
+- Private links show "link protected" message if accessed directly
diff --git a/backend/doc/otpModel.md b/backend/doc/otpModel.md
new file mode 100644
index 0000000..c4a241e
--- /dev/null
+++ b/backend/doc/otpModel.md
@@ -0,0 +1,179 @@
+# OTP Model Documentation
+
+## Overview
+The OTP (One-Time Password) model stores temporary verification codes used for email verification, password reset, and other authentication-related operations. OTPs are automatically expired after a set time period.
+
+## Model Name
+`Otp` (collection name in MongoDB)
+
+## Schema Fields
+
+### `email` (String, Required)
+- **Type**: String
+- **Required**: Yes
+- **Purpose**: Email address of the user requesting the OTP. Used to:
+ - Identify which user the OTP belongs to
+ - Send the OTP via email
+ - Validate OTP requests
+ - Prevent OTP abuse (rate limiting per email)
+- **Example**: `"john.doe@example.com"`
+- **Why Required**: Essential for associating OTPs with users and sending verification codes. Without email, the OTP cannot be delivered or validated.
+
+### `otp` (String, Required)
+- **Type**: String (hashed)
+- **Required**: Yes
+- **Purpose**: The actual one-time password code. **Should always be hashed** before storage for security. Never store plain text OTPs.
+- **Example**: `"$2b$10$hashedotpstring..."` (hashed)
+- **Why Required**: The core verification code. Required for OTP validation. Must be hashed using a secure hashing algorithm (bcrypt, argon2, etc.).
+
+### `createdAt` (Date, Auto-generated, Auto-expires)
+- **Type**: Date
+- **Auto-generated**: Yes
+- **Default**: `Date.now`
+- **Expires**: 300 seconds (5 minutes)
+- **Purpose**:
+ - Timestamp when the OTP was created
+ - Used for automatic expiration via MongoDB TTL (Time To Live) index
+ - OTPs expire 5 minutes after creation for security
+- **Example**: `2024-01-15T10:30:00.000Z`
+- **Why Required**: Essential for OTP expiration. The `expires: 300` option automatically deletes the document after 5 minutes, ensuring OTPs cannot be used after expiration.
+
+## Indexes
+- `createdAt`: Automatically indexed with TTL (Time To Live) for auto-expiration
+- Consider adding index on `email` for faster lookups if needed
+
+## Security Considerations
+
+### OTP Hashing
+- **Never** store OTPs in plain text
+- Always hash OTPs before saving to database
+- Use strong hashing algorithms (bcrypt recommended)
+- Recommended: bcrypt with salt rounds >= 10
+
+### OTP Generation
+- Generate cryptographically secure random OTPs
+- Use appropriate length (typically 4-6 digits for user-friendly, 6-8 for higher security)
+- Consider alphanumeric OTPs for better security
+
+### Expiration
+- OTPs expire after 5 minutes (300 seconds)
+- This is enforced by MongoDB TTL index
+- Expired OTPs are automatically deleted
+- Consider shorter expiration for sensitive operations
+
+### Rate Limiting
+- Implement rate limiting per email address
+- Prevent OTP spam/abuse
+- Consider maximum OTP requests per hour/day per email
+
+### Single Use
+- OTPs should be single-use only
+- Delete OTP after successful verification
+- Prevent replay attacks
+
+## Usage Examples
+
+### Creating an OTP
+```javascript
+const bcrypt = require('bcrypt');
+const crypto = require('crypto');
+
+// Generate 6-digit OTP
+const otpCode = crypto.randomInt(100000, 999999).toString();
+
+// Hash the OTP
+const hashedOtp = await bcrypt.hash(otpCode, 10);
+
+// Create OTP record
+const otp = new Otp({
+ email: 'user@example.com',
+ otp: hashedOtp
+});
+await otp.save();
+
+// Send plain OTP via email (not stored)
+await sendEmail(user.email, `Your OTP is: ${otpCode}`);
+```
+
+### Verifying an OTP
+```javascript
+const otpRecord = await Otp.findOne({
+ email: 'user@example.com'
+});
+
+if (!otpRecord) {
+ throw new Error('OTP not found or expired');
+}
+
+const isValid = await bcrypt.compare(enteredOtp, otpRecord.otp);
+
+if (isValid) {
+ // Delete OTP after successful verification
+ await Otp.deleteOne({ _id: otpRecord._id });
+ // Proceed with verification
+} else {
+ throw new Error('Invalid OTP');
+}
+```
+
+### Checking OTP Existence
+```javascript
+const otpExists = await Otp.findOne({
+ email: 'user@example.com'
+});
+
+if (otpExists) {
+ // OTP exists and is not expired (MongoDB TTL handles expiration)
+ console.log('OTP found');
+} else {
+ // OTP doesn't exist or has expired
+ console.log('OTP not found or expired');
+}
+```
+
+## Expiration Behavior
+- MongoDB automatically deletes documents where `createdAt` is older than 300 seconds
+- This happens via TTL (Time To Live) index
+- No manual cleanup needed
+- Expired OTPs are permanently deleted (not soft deleted)
+
+## Best Practices
+
+### OTP Generation
+1. Use cryptographically secure random number generator
+2. Generate appropriate length (6 digits recommended)
+3. Store only hashed version
+
+### OTP Delivery
+1. Send OTP via secure channel (email, SMS)
+2. Include expiration time in message
+3. Warn users about security (don't share OTP)
+
+### OTP Validation
+1. Check if OTP exists
+2. Verify OTP hash matches
+3. Check if OTP is expired (MongoDB handles this)
+4. Delete OTP after successful verification
+5. Implement rate limiting
+
+### Security
+1. Hash all OTPs before storage
+2. Use HTTPS for OTP transmission
+3. Implement rate limiting
+4. Log failed OTP attempts
+5. Consider CAPTCHA for OTP requests
+
+## Notes
+- OTPs are automatically deleted after 5 minutes
+- Only one OTP per email can exist at a time (consider this in implementation)
+- Always hash OTPs before storing
+- Delete OTP immediately after successful verification
+- Consider implementing OTP attempt limits (max 3-5 attempts)
+- The model is simple by design - expiration is handled automatically by MongoDB
+
+## Common Use Cases
+1. **Email Verification**: Send OTP to verify email address during registration
+2. **Password Reset**: Send OTP to verify identity before password reset
+3. **Two-Factor Authentication**: Additional security layer for login
+4. **Account Recovery**: Verify identity for account recovery
+5. **Sensitive Operations**: Verify identity before allowing sensitive actions
diff --git a/backend/doc/userModel.md b/backend/doc/userModel.md
new file mode 100644
index 0000000..3d0efb0
--- /dev/null
+++ b/backend/doc/userModel.md
@@ -0,0 +1,151 @@
+# User Model Documentation
+
+## Overview
+The User model stores core authentication and account information for registered users. This is the primary user account model used for login, authentication, and user identification.
+
+## Model Name
+`user` (collection name in MongoDB)
+
+## Schema Fields
+
+### `name` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Purpose**: User's full name or display name. Used for personalization and display purposes. Can be updated later through the profile.
+- **Example**: `"John Doe"`, `"Jane Smith"`
+- **Why Optional**: Users may register with just email/username initially and add their name later.
+
+### `email` (String, Required, Unique)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: Yes
+- **Purpose**: User's email address. Used for:
+ - Account identification
+ - Password reset
+ - Email verification
+ - Communication
+ - Login (alternative to username)
+- **Example**: `"john.doe@example.com"`
+- **Why Required**: Essential for account recovery, notifications, and user identification. Must be unique to prevent duplicate accounts.
+
+### `password` (String, Required)
+- **Type**: String (hashed)
+- **Required**: Yes
+- **Purpose**: User's password. **Should always be hashed** before storage (using bcrypt, argon2, or similar). Never store plain text passwords.
+- **Example**: `"$2b$10$hashedpasswordstring..."` (hashed)
+- **Why Required**: Required for authentication and account security. Must be hashed using a secure hashing algorithm.
+
+### `username` (String, Required, Unique)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: Yes
+- **Purpose**: Unique username for the user. Used for:
+ - Public profile URLs (e.g., `allin1url.in/username`)
+ - User identification
+ - Login
+ - Display purposes
+- **Example**: `"johndoe"`, `"jane_smith"`
+- **Why Required**: Essential for creating unique user identifiers and public URLs. Must be unique to prevent conflicts.
+
+### `deletedAt` (Date, Optional)
+- **Type**: Date
+- **Required**: No
+- **Default**: `null`
+- **Purpose**: Soft delete field. When a user account is deleted, this field is set to the deletion timestamp instead of actually removing the record. Allows for:
+ - Account recovery within a grace period
+ - Data retention for legal/compliance reasons
+ - Audit trails
+ - Preventing username reuse immediately after deletion
+- **Example**: `2024-01-15T10:30:00.000Z` or `null`
+- **Why Optional**: Accounts are active by default. Only set when an account is deleted.
+
+### `createdAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the user account was created. Useful for:
+ - Displaying account age
+ - Analytics on user registration patterns
+ - Account verification timelines
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+### `updatedAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the user account was last modified. Automatically updated whenever the document is saved. Useful for tracking account activity.
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+## Indexes
+- `email`: Automatically indexed due to `unique: true`
+- `username`: Automatically indexed due to `unique: true`
+- Consider adding compound index on `{ email: 1, deletedAt: 1 }` for soft delete queries
+
+## Relationships
+- **Has one**: UserProfile (via `username`)
+- **Has one**: UserSettings (via `userId` or `username`)
+- **Has many**: Links (via `userId`)
+- **Has many**: LinkAnalytics (via `userId`)
+
+## Security Considerations
+
+### Password Storage
+- **Never** store passwords in plain text
+- Always hash passwords before saving
+- Use strong hashing algorithms (bcrypt, argon2, scrypt)
+- Recommended: bcrypt with salt rounds >= 10
+
+### Email Validation
+- Validate email format before saving
+- Consider email verification flow
+- Handle case-insensitive email matching
+
+### Username Validation
+- Enforce username rules (length, characters allowed)
+- Prevent reserved usernames
+- Handle case-insensitive username matching if needed
+
+## Usage Examples
+
+### Creating a User
+```javascript
+const bcrypt = require('bcrypt');
+
+const hashedPassword = await bcrypt.hash(password, 10);
+const user = new User({
+ name: 'John Doe',
+ email: 'john.doe@example.com',
+ password: hashedPassword,
+ username: 'johndoe'
+});
+await user.save();
+```
+
+### Finding Active Users
+```javascript
+const activeUsers = await User.find({
+ deletedAt: null
+});
+```
+
+### Soft Delete User
+```javascript
+user.deletedAt = new Date();
+await user.save();
+```
+
+### Finding User by Email or Username
+```javascript
+const user = await User.findOne({
+ $or: [
+ { email: identifier },
+ { username: identifier }
+ ],
+ deletedAt: null
+});
+```
+
+## Notes
+- Always filter by `deletedAt: null` when querying active users
+- Passwords must be hashed before saving
+- Email and username are case-sensitive by default (consider normalization)
+- When deleting a user, also consider soft-deleting related records (links, profiles, etc.)
+- The `name` field can be synced with the UserProfile model for consistency
diff --git a/backend/doc/userProfile.md b/backend/doc/userProfile.md
new file mode 100644
index 0000000..4bfe389
--- /dev/null
+++ b/backend/doc/userProfile.md
@@ -0,0 +1,153 @@
+# User Profile Model Documentation
+
+## Overview
+The User Profile model stores public-facing profile information for users. This includes display name, bio, location, passion, and profile image. This data is separate from authentication data and is used for public profile pages and search features.
+
+## Model Name
+`userinfo` (collection name in MongoDB)
+
+## Schema Fields
+
+### `username` (String, Required, Unique)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: Yes
+- **Ref**: `user`
+- **Purpose**: Foreign key reference to the User model. Links the profile to the user account. Used as the primary identifier for profile lookups.
+- **Example**: `"johndoe"`, `"jane_smith"`
+- **Why Required**: Essential for linking profile data to user accounts. Must be unique (one profile per user). Used in URL generation for public profiles.
+
+### `name` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Default**: `""` (empty string)
+- **Purpose**: User's display name or full name. Shown on public profile pages and in search results. Can differ from the username.
+- **Example**: `"John Doe"`, `"Jane Smith"`, `"Dr. Sarah Johnson"`
+- **Why Optional**: Users may want to set up their account first and add their name later. Empty string allows the field to exist but be empty.
+
+### `passion` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Default**: `""` (empty string)
+- **Purpose**: A short description of what the user is passionate about or their profession. Used for:
+ - Profile personalization
+ - Search keywords
+ - Display on public profiles
+- **Example**: `"Software Engineer"`, `"Digital Artist"`, `"Entrepreneur"`
+- **Why Optional**: Not all users may want to share this information. Can be added later.
+
+### `bio` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Default**: `""` (empty string)
+- **Purpose**: Longer description about the user. Can include:
+ - Professional background
+ - Interests
+ - Personal information
+ - Links to other profiles
+- **Example**: `"Full-stack developer passionate about creating beautiful web experiences. Love open source and coffee."`
+- **Why Optional**: Users may not want to write a bio immediately. Can be a longer text field (consider textarea in UI).
+
+### `location` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Default**: `""` (empty string)
+- **Purpose**: User's location. Can be:
+ - City and country
+ - Just city
+ - Just country
+ - Any location format
+- **Example**: `"New York, USA"`, `"London, UK"`, `"Mumbai, India"`
+- **Why Optional**: Privacy concerns - some users may not want to share their location.
+
+### `image` (String, Optional)
+- **Type**: String
+- **Required**: No
+- **Default**: `"profile.jpg"`
+- **Purpose**: URL or path to the user's profile picture. Can be:
+ - Relative path to uploaded image
+ - Full URL to external image
+ - Default placeholder image
+- **Example**: `"profile.jpg"`, `"/uploads/profiles/user123.jpg"`, `"https://example.com/avatar.jpg"`
+- **Why Optional**: Users may not have a profile picture initially. Default value provides a fallback.
+
+### `deletedAt` (Date, Optional)
+- **Type**: Date
+- **Required**: No
+- **Default**: `null`
+- **Purpose**: Soft delete field. When a profile is deleted, this field is set to the deletion timestamp instead of removing the record. Allows for:
+ - Profile recovery
+ - Data retention
+ - Audit trails
+- **Example**: `2024-01-15T10:30:00.000Z` or `null`
+- **Why Optional**: Profiles are active by default. Only set when a profile is deleted.
+
+### `createdAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the profile was created. Usually matches the user account creation time.
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+### `updatedAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes (via `timestamps: true`)
+- **Purpose**: Timestamp of when the profile was last modified. Updated whenever profile information is changed.
+- **Example**: `2024-01-15T10:30:00.000Z`
+
+## Indexes
+- `username`: Automatically indexed due to `unique: true`
+- Consider adding index on `deletedAt` for soft delete queries
+
+## Relationships
+- **Belongs to**: User (via `username`)
+- **Related to**: UserSettings (controls profile visibility)
+
+## Privacy Considerations
+- Profile visibility is controlled by the UserSettings model
+- Some fields may be hidden based on user privacy settings
+- Consider GDPR compliance for profile data
+
+## Usage Examples
+
+### Creating a Profile
+```javascript
+const profile = new UserProfile({
+ username: 'johndoe',
+ name: 'John Doe',
+ passion: 'Software Engineer',
+ bio: 'Full-stack developer passionate about creating beautiful web experiences.',
+ location: 'New York, USA',
+ image: 'profile.jpg'
+});
+await profile.save();
+```
+
+### Updating Profile
+```javascript
+const profile = await UserProfile.findOne({ username: 'johndoe' });
+profile.name = 'John A. Doe';
+profile.bio = 'Updated bio text';
+await profile.save();
+```
+
+### Finding Public Profiles
+```javascript
+// Note: Actual visibility should be checked against UserSettings
+const profiles = await UserProfile.find({
+ deletedAt: null
+});
+```
+
+### Soft Delete Profile
+```javascript
+profile.deletedAt = new Date();
+await profile.save();
+```
+
+## Notes
+- Always filter by `deletedAt: null` when querying active profiles
+- Profile visibility should be checked against UserSettings model
+- The `image` field should handle both relative and absolute URLs
+- Consider image validation and size limits for uploaded images
+- Profile data can be synced with User model's `name` field for consistency
+- Empty strings are used instead of null to ensure fields exist in the document
diff --git a/backend/doc/userSettingsModel.md b/backend/doc/userSettingsModel.md
new file mode 100644
index 0000000..d9e8541
--- /dev/null
+++ b/backend/doc/userSettingsModel.md
@@ -0,0 +1,313 @@
+# User Settings Model Documentation
+
+## Overview
+The User Settings model stores privacy and visibility preferences for users. It controls what information is visible to other users, which links appear in search results, and various privacy settings. This model is essential for the search and public profile features.
+
+## Model Name
+`userSettings` (collection name in MongoDB)
+
+## Schema Fields
+
+### `userId` (ObjectId, Required, Unique)
+- **Type**: mongoose.Schema.Types.ObjectId
+- **Required**: Yes
+- **Unique**: Yes
+- **Ref**: `user`
+- **Indexed**: Yes
+- **Purpose**: Foreign key reference to the User model. Links settings to a specific user account. One settings document per user.
+- **Example**: `ObjectId("507f1f77bcf86cd799439011")`
+- **Why Required**: Essential for associating settings with users. Must be unique to ensure one settings document per user.
+
+### `username` (String, Required, Unique)
+- **Type**: String
+- **Required**: Yes
+- **Unique**: Yes
+- **Ref**: `user`
+- **Indexed**: Yes
+- **Purpose**: Username of the user. Stored redundantly for faster queries without joins. Used for username-based settings lookups.
+- **Example**: `"johndoe"`
+- **Why Required**: Allows quick username-based queries. Must match the User model's username.
+
+## Profile Visibility Settings
+
+### `profile.isPublic` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Master switch for profile visibility. When `true`, the profile can be viewed by others (subject to other settings). When `false`, profile is private.
+- **Why Optional**: Defaults to private for user privacy. Users must explicitly opt-in to make profiles public.
+
+### `profile.showInSearch` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether the profile appears in search results. Even if `isPublic` is true, the profile won't appear in search unless this is also `true`.
+- **Why Optional**: Users may want public profiles but not in search results. Provides granular control.
+
+### `profile.allowProfileView` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Allows other users to view the full profile page. When `false`, even if profile is public, it may not be fully accessible.
+- **Why Optional**: Additional privacy layer. Users can control profile page access separately from search visibility.
+
+### `profile.showEmail` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether email is visible on public profile. Should default to false for privacy.
+- **Why Optional**: Email is sensitive information. Should be hidden by default.
+
+### `profile.showLocation` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `true`
+- **Purpose**: Controls whether location is shown on public profile.
+- **Why Optional**: Some users may want to hide location for privacy.
+
+### `profile.showBio` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `true`
+- **Purpose**: Controls whether bio is shown on public profile.
+- **Why Optional**: Users may want to hide bio while keeping other info public.
+
+### `profile.showPassion` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `true`
+- **Purpose**: Controls whether passion/profession is shown on public profile.
+- **Why Optional**: Granular control over profile information visibility.
+
+### `profile.showProfileImage` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `true`
+- **Purpose**: Controls whether profile image is shown on public profile.
+- **Why Optional**: Some users may want to hide their profile picture.
+
+## Link Visibility Settings
+
+### `links.defaultVisibility` (String, Optional)
+- **Type**: String
+- **Enum Values**: `'public'`, `'private'`, `'unlisted'`
+- **Default**: `'private'`
+- **Purpose**: Global default visibility for all links. Can be overridden per link.
+ - `'public'`: Visible to everyone in search and on profile
+ - `'private'`: Only visible to the owner
+ - `'unlisted'`: Visible via direct link but not in search or on profile
+- **Why Optional**: Defaults to private for privacy. Users must explicitly make links public.
+
+### `links.publicLinks` (Array, Optional)
+- **Type**: Array of Strings (linkIds)
+- **Ref**: `link`
+- **Default**: `[]`
+- **Purpose**: Array of `linkId`s that are explicitly set to public. These links will be visible in search and on public profiles, regardless of `defaultVisibility`.
+- **Example**: `["linkedin", "github", "twitter"]`
+- **Why Optional**: Not all links need to be public. Empty array means no links are explicitly public.
+
+### `links.unlistedLinks` (Array, Optional)
+- **Type**: Array of Strings (linkIds)
+- **Ref**: `link`
+- **Default**: `[]`
+- **Purpose**: Array of `linkId`s that are unlisted. These links are accessible via direct URL but won't appear in search results or on public profiles.
+- **Example**: `["personal-blog", "private-portfolio"]`
+- **Why Optional**: Not all links need special handling. Empty array by default.
+
+### `links.showLinkCount` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `true`
+- **Purpose**: Controls whether the total number of links is shown on public profile.
+- **Why Optional**: Some users may want to hide link count for privacy.
+
+### `links.showClickStats` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether click statistics are shown on public profile. Should default to false for privacy.
+- **Why Optional**: Click statistics are sensitive business metrics. Should be private by default.
+
+## Search & Discovery Settings
+
+### `search.allowSearch` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Master switch for search visibility. When `true` and `profile.isPublic` is true, the profile can appear in search results.
+- **Why Optional**: Users must explicitly opt-in to search visibility.
+
+### `search.showInFeatured` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether profile appears in "featured" or "popular" sections. Typically requires admin approval.
+- **Why Optional**: Featured status is usually curated, not user-controlled.
+
+### `search.searchKeywords` (Array, Optional)
+- **Type**: Array of Strings
+- **Default**: `[]`
+- **Purpose**: Custom keywords/tags for better searchability. Helps users find profiles by topics, skills, or interests.
+- **Example**: `["developer", "javascript", "react", "open-source"]`
+- **Why Optional**: Not required for basic search. Enhances discoverability.
+
+## Privacy Settings
+
+### `privacy.showAnalytics` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether analytics data is visible to public. Should default to false.
+- **Why Optional**: Analytics are sensitive. Should be private by default.
+
+### `privacy.showLastUpdated` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Controls whether "last updated" timestamp is shown on public profile.
+- **Why Optional**: Some users may not want to reveal activity patterns.
+
+### `privacy.requireAuth` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: When `true`, requires users to be logged in to view the profile. Adds an extra privacy layer.
+- **Why Optional**: Most profiles are viewable without authentication. This is an advanced privacy option.
+
+## Notification Settings
+
+### `notifications.emailOnNewClick` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Sends email notification when a link receives a new click.
+- **Why Optional**: Can be noisy. Users opt-in if they want notifications.
+
+### `notifications.emailOnProfileView` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Sends email notification when profile is viewed.
+- **Why Optional**: Can be very noisy. Should be opt-in only.
+
+### `notifications.weeklyReport` (Boolean, Optional)
+- **Type**: Boolean
+- **Default**: `false`
+- **Purpose**: Sends weekly analytics report via email.
+- **Why Optional**: Users opt-in for weekly summaries.
+
+### `deletedAt` (Date, Optional)
+- **Type**: Date
+- **Default**: `null`
+- **Purpose**: Soft delete field. When settings are deleted, this field is set instead of removing the record.
+- **Why Optional**: Settings are active by default.
+
+### `createdAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes
+- **Purpose**: Timestamp when settings were created.
+
+### `updatedAt` (Date, Auto-generated)
+- **Type**: Date
+- **Auto-generated**: Yes
+- **Purpose**: Timestamp when settings were last modified.
+
+## Indexes
+
+### Single Field Indexes
+- `userId`: For fast user-based queries
+- `username`: For fast username-based queries
+
+### Compound Indexes
+- `{ username: 1, deletedAt: 1 }`: For active settings queries
+- `{ userId: 1, deletedAt: 1 }`: For active settings queries
+- `{ 'profile.isPublic': 1, 'search.allowSearch': 1 }`: For finding searchable profiles
+
+## Instance Methods
+
+### `isLinkPublic(linkId)`
+Checks if a specific link is visible to public.
+
+**Parameters**:
+- `linkId` (String): The linkId to check
+
+**Returns**: Boolean
+
+**Logic**:
+1. If linkId is in `publicLinks` array → returns `true`
+2. If `defaultVisibility` is `'public'` and linkId is not in `unlistedLinks` → returns `true`
+3. Otherwise → returns `false`
+
+### `isLinkUnlisted(linkId)`
+Checks if a link is unlisted.
+
+**Parameters**:
+- `linkId` (String): The linkId to check
+
+**Returns**: Boolean
+
+### `isSearchable()`
+Checks if profile should appear in search results.
+
+**Returns**: Boolean
+
+**Logic**: Returns `true` if:
+- `profile.isPublic` is `true`
+- `search.allowSearch` is `true`
+- `deletedAt` is `null`
+
+### `getPublicLinks(allLinks)`
+Filters an array of links to return only public ones.
+
+**Parameters**:
+- `allLinks` (Array): Array of link objects
+
+**Returns**: Array of public link objects
+
+## Static Methods
+
+### `getUserSettings(userIdOrUsername, userData)`
+Gets or creates settings for a user.
+
+**Parameters**:
+- `userIdOrUsername` (String|ObjectId): User ID or username
+- `userData` (Object, Optional): User object with `_id` or `username` to avoid extra queries
+
+**Returns**: UserSettings document
+
+**Behavior**:
+- If settings exist, returns them
+- If not, creates default settings
+- Automatically fetches missing userId/username from User model if needed
+
+## Usage Examples
+
+### Getting User Settings
+```javascript
+const settings = await UserSettings.getUserSettings('johndoe');
+// or
+const settings = await UserSettings.getUserSettings(userId);
+```
+
+### Making Profile Public and Searchable
+```javascript
+const settings = await UserSettings.getUserSettings('johndoe');
+settings.profile.isPublic = true;
+settings.search.allowSearch = true;
+await settings.save();
+```
+
+### Making Specific Links Public
+```javascript
+const settings = await UserSettings.getUserSettings('johndoe');
+if (!settings.links.publicLinks.includes('linkedin')) {
+ settings.links.publicLinks.push('linkedin');
+}
+await settings.save();
+```
+
+### Checking Link Visibility
+```javascript
+const settings = await UserSettings.getUserSettings('johndoe');
+const isPublic = settings.isLinkPublic('linkedin');
+```
+
+### Finding Searchable Profiles
+```javascript
+const searchableProfiles = await UserSettings.find({
+ 'profile.isPublic': true,
+ 'search.allowSearch': true,
+ deletedAt: null
+});
+```
+
+## Notes
+- Always use `getUserSettings()` to ensure settings exist
+- Default visibility is private for security
+- Link visibility can be controlled globally or per-link
+- Settings are created automatically when first accessed
+- Always filter by `deletedAt: null` when querying active settings
+- Consider caching settings for frequently accessed users
diff --git a/backend/index.js b/backend/index.js
index 3b7b1bb..3ffca19 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -6,20 +6,36 @@ const mongoose=require('mongoose')
const dotenv = require('dotenv')
const helmet = require('helmet');
const cloudinary = require('cloudinary')
+const crypto = require('crypto')
const authRoute=require('./routes/AuthRoute')
const linkRoute=require('./routes/LinkRoute')
+const analyticsRoute=require('./routes/AnalyticsRoute')
const Link = require('./model/linkModel')
const Profile=require('./model/userProfile')
const User=require('./model/userModel')
+const UserSettings=require('./model/userSettingsModel')
const profileRoute=require('./routes/ProfileRoute')
const { extractInfo } = require('./middleware/deviceInfo')
-const { sendNotificationEmail } = require('./lib/mail')
+const { sendVisitEmail, sendProfileVisitEmail } = require('./lib/mail')
+const { verifyTokenOptional } = require('./middleware/verifyToken')
+const resolveUsername = require('./middleware/resolveUsername')
+const { getUserLinkUrl, getTemplateScripts, getFaviconScript } = require('./utils')
+const bcryptjs = require('bcryptjs')
+const { time } = require('console')
+const { saveAnalytics } = require('./controller/AnalyticsController')
dotenv.config()
+// Log environment variables for debugging
+const tier = process.env.TIER || 'NOT SET (defaults to production)';
+console.log('Environment check:');
+console.log(' TIER:', tier);
+console.log(' Mode:', process.env.TIER === 'dev' ? 'DEVELOPMENT' : 'PRODUCTION');
+console.log(' PORT:', process.env.PORT || '8080 (default)');
+
cloudinary.config({
cloud_name:process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
@@ -34,114 +50,525 @@ const db_url=process.env.DATABASE_URL;
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
-// console.log('Views directory:', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
+// Make helper functions available to all EJS templates
+// Ensure it always reads the current environment variable
+app.locals.getUserLinkUrl = getUserLinkUrl;
+app.locals.getTemplateScripts = getTemplateScripts;
+app.locals.getFaviconScript = getFaviconScript;
+
+// Allowed origins for CORS
const allowedOrigins = [
- 'https://clickly.cv',
- 'https://www.clickly.cv',
+ 'https://allin1url.in',
+ 'https://www.allin1url.in',
'https://linkbriger.vercel.app',
- 'http://localhost:5173'
+ 'http://localhost:5173',
+ 'http://localhost:8080'
];
+// CORS configuration with subdomain support
app.use(cors({
origin: function (origin, callback) {
- if (!origin) return callback(null, true);
+ // Allow requests with no origin (like mobile apps, Postman, etc.)
+ if (!origin) {
+ return callback(null, true);
+ }
+
+ // Check if origin is in explicit allowed list
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
- return callback(new Error('Not allowed by CORS'));
+
+ // Allow all subdomains of allin1url.in (for custom user domains)
+ // Examples: https://dpkrn.allin1url.in, https://username.allin1url.in
+ try {
+ const url = new URL(origin);
+ const hostname = url.hostname.toLowerCase();
+
+ // Allow exact match for allin1url.in
+ if (hostname === 'allin1url.in') {
+ return callback(null, true);
+ }
+
+ // Allow all subdomains (*.allin1url.in)
+ // Supports both single-level (dpkrn.allin1url.in) and multi-level (api.dpkrn.allin1url.in)
+ if (hostname.endsWith('.allin1url.in')) {
+ const subdomain = hostname.replace('.allin1url.in', '');
+ // Subdomain should be non-empty (allows multi-level subdomains)
+ if (subdomain && subdomain.length > 0) {
+ return callback(null, true);
+ }
+ }
+ } catch (e) {
+ // Invalid URL format, reject
+ console.warn('Invalid CORS origin format:', origin);
+ }
+
+ // Reject all other origins
+ return callback(new Error(`CORS: Origin ${origin} is not allowed`));
},
credentials: true,
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin'],
+ exposedHeaders: ['Content-Length', 'Content-Type'],
+ maxAge: 86400 // 24 hours
}));
-// Optional: explicitly handle OPTIONS for all routes
app.options('*', cors());
app.use(cookieParser());
app.use(express.json({limit:'100mb'}))
-app.use(helmet());
+app.use(express.urlencoded({ extended: true, limit: '100mb' })) // For form submissions
+// Configure helmet with iframe support for preview
+app.use(helmet({
+ frameguard: {
+ action: 'sameorigin' // Allow iframes from same origin, but we'll override for preview
+ }
+}));
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "https://vercel.live", "https://*.vercel.app"], // Allow Vercel scripts
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://vercel.live", "https://*.vercel.app"], // Allow inline scripts for EJS templates
imgSrc: ["'self'", "data:", "https://res.cloudinary.com"], // Add your image host if needed
- styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles if needed
- connectSrc: ["'self'", "https://linkb-one.vercel.app","https://linkb-one.vercel.app/*","https://clickly.cv/*"], // Add your API backend here
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], // Allow Google Fonts stylesheets
+ fontSrc: ["'self'", "https://fonts.gstatic.com"], // Allow Google Fonts actual font files
+ connectSrc: ["'self'", "https://allin1url.in","https://allin1url.in/*", "http://localhost:8080"], // Add your API backend here
+ frameAncestors: ["'self'", "http://localhost:5173", "https://allin1url.in", "https://linkbriger.vercel.app"], // Allow iframes from these origins
// Add more directives as needed
}
}));
-app.get('/',(req,res)=>{
- console.log("redirecting to frontend")
- return res.redirect(307,"https://clickly.cv/app/")
-})
+// Root route - handle main domain redirect and subdomain routing
+app.get('/', resolveUsername, extractInfo, async (req, res) => {
+
+ // If it's the main domain (allin1url.in or www.allin1url.in), redirect to frontend
+ if (req.isMainDomain || !req.params.username) {
+ console.log("Main domain detected, redirecting to frontend");
+ return res.redirect(307, "https://allin1url.in/app/");
+ }
+
+ // If it's a subdomain, treat it as username route (show linkhub)
+ const username = req.params.username;
+
+ // Use the same logic as /:username route
+ const tree = await Link.find({
+ username: username,
+ visibility: 'public',
+ deletedAt: null
+ });
+ const dp = await Profile.findOne({ username }, { image: 1, bio: 1 });
+ const info = await User.findOne({ username }, { email: 1, name: 1, _id:1, username:1 });
+
+ if (!info) {
+ return res.render('not_exists');
+ }
+
+ const { email, name } = info;
+ const deviceDetails = req.details || {};
+
+ // Get visitor information if they're logged in
+ let visitorUsername = null;
+ let visitorName = null;
+ if (req.userId) {
+ try {
+ const visitor = await User.findById(req.userId, { username: 1, name: 1 });
+ if (visitor && visitor.username !== username) {
+ visitorUsername = visitor.username;
+ visitorName = visitor.name;
+ }
+ } catch (err) {
+ console.error(`Error fetching visitor info:`, err);
+ }
+ }
+
+ // Check if email notification is enabled for LinkHub views
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnLinkHubView()) {
+ sendProfileVisitEmail(
+ email,
+ username,
+ name,
+ deviceDetails,
+ visitorUsername,
+ visitorName
+ ).catch(err => {
+ console.error(`Failed to send LinkHub visit email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ }
+
+ saveAnalytics({
+ linkId: null,
+ userId: info._id,
+ username: info.username,
+ req
+ }).catch(err => {
+ console.error('Analytics error:', err);
+ });
+
+ if (tree && dp){
+ // Get user settings to determine template
+ let template = 'default';
+ const previewTemplate = req.query.template;
+ if (previewTemplate) {
+ template = previewTemplate;
+ } else {
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.template) {
+ template = settings.template;
+ }
+ } catch (err) {
+ console.log('Error fetching template settings, using default:', err.message);
+ }
+ }
+
+ const templateName = `templates/linktree-${template}`;
+ console.log("templateName", templateName);
+ try {
+ return res.render(templateName, {
+ username: username,
+ tree: tree,
+ dp: dp
+ });
+ } catch (renderErr) {
+ console.log(`Template ${templateName} not found, using default:`, renderErr.message);
+ return res.render('templates/linktree-default', {
+ username: username,
+ tree: tree,
+ dp: dp
+ });
+ }
+ }
+
+ return res.render('not_exists', { linkHub: '' });
+});
app.use('/auth',authRoute)
app.use('/source',linkRoute)
app.use('/profile',profileRoute)
+app.use('/settings',require('./routes/SettingsRoute'))
+app.use('/search',require('./routes/SearchRoute'))
+app.use('/analytics',analyticsRoute)
+app.use('/project',require('./routes/ProjectRoute'))
+
+// Helper function to encode username and source (base64)
+const encodeData = (data) => {
+ return Buffer.from(data).toString('base64');
+};
+
+// Helper function to decode username and source
+const decodeData = (encodedData) => {
+ try {
+ return Buffer.from(encodedData, 'base64').toString('utf8');
+ } catch (error) {
+ return null;
+ }
+};
-app.get('/:username',extractInfo, async (req, res) => {
- console.log("backend profile search start")
- const username=req.params.username
- const tree=await Link.find({username:username})
- const dp=await Profile.findOne({username},{image:1,bio:1});
- const info=await User.findOne({username},{email:1,name:1})
- if(!info){
- return res.render('not_exists')
+
+
+// Handle password submission for private links
+app.post('/link/verify-password', extractInfo, async (req, res) => {
+ const { hashedUsername, hashedSource, password } = req.body;
+
+ const username = decodeData(hashedUsername);
+ const source = decodeData(hashedSource);
+ console.log(password)
+
+ const doc = await Link.findOne({
+ username,
+ source,
+ deletedAt: null
+ });
+
+ if (!doc) {
+ return res.status(404).json({ success: false });
}
- const {email,name}=info
- const deviceDetails=req.details
- sendNotificationEmail(email,username,name,deviceDetails,"LinkHub")
- if(tree&&dp){
- return res.render('linktree',{
- username:username,
- tree:tree,
- dp:dp
- })
+ // const bcryptjs = require('bcryptjs');
+ if (!doc.password || !(await bcryptjs.compare(password, doc.password))) {
+ return res.status(401).json({
+ success: false,
+ message: "Invalid password"
+ });
}
+ // // Password correct, redirect directly to destination
+ const {destination,clicked,notSeen}=doc
+ await Link.updateOne({username,source},{$set:{clicked:clicked+1,notSeen:notSeen+1}})
+
+ const info=await User.findOne({username},{email:1,name:1})
+ if(info) {
+ const {email,name}=info
+ const deviceDetails=req.details || {}
+ // Send email asynchronously, don't wait for it
+ sendVisitEmail(email,username,name,deviceDetails,source).catch(err => {
+ console.error(`Failed to send visit to ${username}:`, err);
+ });
+ }
+
+
+ return res.json({
+ success: true,
+ destination: destination
+ });
+});
+
+// Subdomain route handler: dpkrn.allin1url.in/github
+// This route handles subdomain-based source access
+// Note: API routes (defined with app.use above) will match first, so this won't interfere
+app.get('/:source', resolveUsername, extractInfo, async (req, res) => {
+ // Only process if username was extracted from subdomain (not main domain)
- return res.render('not_exists')
-})
+ if (req.isMainDomain || !req.params.username) {
+ // This is main domain, let it fall through to other routes
+ return res.redirect(307, "https://allin1url.in/app/");
+ }
+
+
+ const username = req.params.username;
+ const source = req.params.source;
+ // Generate linkHub in subdomain format for subdomain requests
+ const linkHub = `Available link: ${req.protocol}://${username}.allin1url.in`;
+
+ const link = await Link.findOne({
+ username,
+ source,
+ deletedAt: null
+ });
+ const info = await User.findOne({ username }, { email: 1, name: 1, _id:1,username:1 });
+ if (!info) {
+ return res.render('not_exists', {
+ linkHub: ""
+ });
+ }
+ const { email, name } = info;
-app.get('/:username/:source',extractInfo, async (req, res) => {
-
+ if (!link) {
+ return res.render('not_exists', {
+ linkHub: linkHub
+ });
+ }
+ // Check link visibility
+ if (!link.isAccessible()) {
+ const hashedUsername = encodeData(username);
+ const hashedSource = encodeData(source);
+ return res.render('password_prompt', {
+ hashedUsername: hashedUsername,
+ hashedSource: hashedSource,
+ linkId: link.linkId
+ });
+ }
+
+ const { destination, clicked, notSeen } = link;
+ await Link.updateOne({ username, source }, { $set: { clicked: clicked + 1, notSeen: notSeen + 1 } });
+
+
+ const deviceDetails = req.details;
+
+ saveAnalytics({
+ linkId: link._id,
+ userId: info._id,
+ username: info.username,
+ req
+ }).catch(err => {
+ console.error('Analytics error:', err);
+ });
+
+
+ // Check if email notification is enabled for link clicks
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnClick()) {
+ sendVisitEmail(email, username, name, deviceDetails, source).catch(err => {
+ console.error(`Failed to send visit email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ }
+
+ return res.redirect(307, destination);
+});
+
+// Main domain route handler: allin1url.in/username/source
+app.get('/:username/:source', extractInfo, async (req, res) => {
const {username,source}=req.params;
const linkHub=`Available link: ${req.protocol}://${req.get('host')}/${username}`
- const doc=await Link.findOne({username,source})
+ const doc=await Link.findOne({
+ username,
+ source,
+ deletedAt: null
+ })
const info=await User.findOne({username},{email:1,name:1})
if(!info){
- return res.render('not_exists',{
- linkHub:""
- })
+ return res.render('not_exists',{
+ linkHub:""
+ })
}
const {email,name}=info
+
if(!doc) {
return res.render('not_exists',{
- linkHub:linkHub
+ linkHub:linkHub
})
}
- // return res.status(404).json({success:false,message:`${source} not has been added for this user !`})
+ // Check link visibility
+ // private links should render password prompt page directly
+ if(!doc.isAccessible()) {
+ console.log("not accessible")
+ // Encode username and source before sending to EJS
+ const hashedUsername = encodeData(username);
+ const hashedSource = encodeData(source);
+ return res.render('password_prompt', {
+ hashedUsername:hashedUsername,
+ hashedSource:hashedSource,
+ linkId:doc.linkId
+ });
+ }
+
+ // unlisted links are accessible via direct URL (password protection can be added later)
+ // public links are accessible
const {destination,clicked,notSeen}=doc
await Link.updateOne({username,source},{$set:{clicked:clicked+1,notSeen:notSeen+1}})
const deviceDetails=req.details
- sendNotificationEmail(email,username,name,deviceDetails,source)
+
+ // Check if email notification is enabled for link clicks
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnClick()) {
+ sendVisitEmail(email,username,name,deviceDetails,source).catch(err => {
+ console.error(`Failed to send visit email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ // Don't send email if there's an error checking settings
+ }
+
return res.redirect(307,destination)
})
+app.get('/:username', extractInfo, verifyTokenOptional, async (req, res) => {
+ // Allow iframe embedding for preview (allow from frontend origins)
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
+ const frontendOrigins = "http://localhost:5173 https://allin1url.in https://linkbriger.vercel.app 'self'";
+ res.setHeader('Content-Security-Policy', `frame-ancestors ${frontendOrigins}`);
+
+ console.log("backend profile search start")
+ const username=req.params.username
+ // Only show public links in linkhub - unlisted and private links should not appear
+ const tree=await Link.find({
+ username: username,
+ visibility: 'public',
+ deletedAt: null
+ })
+ const dp=await Profile.findOne({username},{image:1,bio:1});
+
+ const info=await User.findOne({username},{email:1,name:1})
+ if(!info){
+ return res.render('not_exists')
+ }
+ const {email,name}=info
+ const deviceDetails=req.details
+
+ // Get visitor information if they're logged in
+ let visitorUsername = null;
+ let visitorName = null;
+ if (req.userId) {
+ try {
+ const visitor = await User.findById(req.userId, { username: 1, name: 1 });
+ if (visitor && visitor.username !== username) {
+ // Only track if visitor is different from profile owner
+ visitorUsername = visitor.username;
+ visitorName = visitor.name;
+ }
+ } catch (err) {
+ console.error(`Error fetching visitor info:`, err);
+ }
+ }
+
+ // Check if email notification is enabled for LinkHub views
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.shouldEmailOnLinkHubView()) {
+ sendProfileVisitEmail(
+ email,
+ username,
+ name,
+ deviceDetails,
+ visitorUsername,
+ visitorName
+ ).catch(err => {
+ console.error(`Failed to send LinkHub visit email to ${username}:`, err);
+ });
+ }
+ } catch (err) {
+ console.error(`Error checking notification settings for ${username}:`, err);
+ // Don't send email if there's an error checking settings
+ }
+
+ if(tree&&dp){
+ // Get user settings to determine template
+ let template = 'default'; // Default template
+
+ // Check if template query parameter is provided (for preview)
+ const previewTemplate = req.query.template;
+ if (previewTemplate) {
+ template = previewTemplate;
+ } else {
+ // Otherwise, use user's saved template from settings
+ try {
+ const settings = await UserSettings.getUserSettings(username);
+ if (settings && settings.template) {
+ template = settings.template;
+ }
+ } catch (err) {
+ console.log('Error fetching template settings, using default:', err.message);
+ }
+ }
+
+ // Construct template name (templates/linktree-{template})
+ const templateName = `templates/linktree-${template}`;
+ console.log("templateName",templateName)
+ // Render template, fallback to default if template doesn't exist
+ try {
+ return res.render(templateName,{
+ username:username,
+ tree:tree,
+ dp:dp
+ });
+ } catch (renderErr) {
+ // If template doesn't exist, fallback to default
+ console.log(`Template ${templateName} not found, using default:`, renderErr.message);
+ return res.render('templates/linktree-default',{
+ username:username,
+ tree:tree,
+ dp:dp
+ });
+ }
+ }
+
+ return res.render('not_exists', { linkHub: '' })
+})
+
+
+
mongoose.connect(db_url).then(()=>{
console.log('db connected')
diff --git a/backend/lib/emailTemplate.js b/backend/lib/emailTemplate.js
index 2c9af25..7cc8585 100644
--- a/backend/lib/emailTemplate.js
+++ b/backend/lib/emailTemplate.js
@@ -86,86 +86,346 @@ const Welcome_Email_Template = `
- Welcome to Our Community
+ Welcome to All in1 url!
-
-
+
+
-
Hello {name},
-
We’re thrilled to have you join us! Your registration was successful, and we’re committed to providing you with the best experience possible.
-
Here’s how you can get started:
-
- - Explore our features and customize your experience.
- - Stay informed by checking out our blog for the latest updates and tips.
- - Reach out to our support team if you have any questions or need assistance.
-
-
Get Started
-
If you need any help, don’t hesitate to contact us. We’re here to support you every step of the way.
+
Hello {name} (@{username})!
+
+
+ We're absolutely thrilled to have you join the All in1 url community! 🚀 Your account has been successfully created, and you're just moments away from transforming how you share and manage your social media links.
+
+
+
+
✨ Your Personalized Link is Ready!
+
+ Share all your profiles with one simple link: https://allin1url.in/{username}
+
+
+
+
+
🌟 What You Can Do Now:
+
+
+
🔗
+
+
One Link, All Your Profiles
+
Create a single, memorable link that leads to all your social media profiles. No more long, complicated URLs!
+
+
+
+
+
📊
+
+
Track Every Click
+
Get real-time analytics on who's visiting your links. Know exactly when and where your audience is engaging.
+
+
+
+
+
📧
+
+
Instant Email Notifications
+
Receive email alerts every time someone clicks your links. Stay connected with your audience in real-time.
+
+
+
+
+
⚡
+
+
Update Once, Reflect Everywhere
+
Change a link once, and it updates across all platforms instantly. No more manual updates everywhere!
+
+
+
+
+
🎨
+
+
Beautiful Landing Page
+
Your profile gets a stunning, customizable landing page that showcases all your links in one beautiful place.
+
+
+
+
+
+
+
+
Your personalized link:
+
https://allin1url.in/{username}
+
+
+
+
📋 Quick Start Guide:
+
+
+
1
+
+
Add Your First Link
+
Go to your dashboard and click "Create Bridge" to add your first social media link. Start with your most important profile!
+
+
+
+
+
2
+
+
Customize Your Profile
+
Visit your profile page to add a bio, profile picture, and personalize your landing page. Make it uniquely yours!
+
+
+
+
+
3
+
+
Share Your Link
+
Copy your personalized link https://allin1url.in/{username} and share it everywhere - in your bio, email signature, business cards, and more!
+
+
+
+
+
4
+
+
Track Your Analytics
+
Monitor who's clicking your links in real-time. You'll receive email notifications for every visit, so you never miss an engagement!
+
+
+
+
+
+
💡 Pro Tips for Success:
+
+ - Keep your link short and memorable - it's already done for you!
+ - Update your links in one place - changes reflect everywhere instantly
+ - Use descriptive platform names (e.g., "Instagram" instead of "ig")
+ - Check your email notifications to see who's visiting your links
+ - Share your link on all your social media profiles for maximum reach
+
+
+
+
+ Questions? We're here to help! Check out our documentation or reply to this email.
+
@@ -260,8 +520,501 @@ const Notification_Email_Template=`