diff --git a/.htaccess b/.htaccess index 1f6a7322..1096b972 100644 --- a/.htaccess +++ b/.htaccess @@ -132,6 +132,8 @@ RewriteRule ^update_album$ src/processors/update_album.php [L] RewriteRule ^delete_album$ src/processors/delete_album.php [L] RewriteRule ^delete_photo$ src/processors/delete_photo.php [L] RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L] +RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L] +RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L] diff --git a/docs/MEMBERSHIP_LINKING.md b/docs/MEMBERSHIP_LINKING.md new file mode 100644 index 00000000..6642861a --- /dev/null +++ b/docs/MEMBERSHIP_LINKING.md @@ -0,0 +1,306 @@ +# Membership Linking Feature + +## Overview +The Membership Linking feature allows users to link secondary accounts (spouses, family members, etc.) to a primary membership account. This enables multiple users to access member-only areas and receive member pricing under a single membership. + +## Database Schema + +### membership_links Table +```sql +- link_id (INT, PK, AUTO_INCREMENT) +- primary_user_id (INT, FK to users) - Main membership holder +- secondary_user_id (INT, FK to users) - Secondary user sharing the membership +- relationship (VARCHAR 50) - Type of relationship (spouse, family_member, etc) +- linked_at (TIMESTAMP) +- created_at (TIMESTAMP) + +Constraints: +- UNIQUE(primary_user_id, secondary_user_id) - Prevent duplicate links +- Foreign keys on both user IDs with CASCADE DELETE +- Indexes on both user IDs for performance +``` + +### membership_permissions Table +```sql +- permission_id (INT, PK, AUTO_INCREMENT) +- link_id (INT, FK to membership_links) - Reference to the link +- permission_name (VARCHAR 100) - Permission type (access_member_areas, member_pricing, etc) +- granted_at (TIMESTAMP) + +Constraints: +- UNIQUE(link_id, permission_name) - Prevent duplicate permissions +- Foreign key to membership_links with CASCADE DELETE +- Index on link_id for performance + +Default Permissions Granted: +- access_member_areas +- member_pricing +- book_campsites +- book_courses +- book_trips +``` + +## Functions + +### linkSecondaryUserToMembership() +**Purpose**: Link a secondary user to a primary user's active membership + +**Parameters**: +- `int $primary_user_id` - The main membership holder +- `int $secondary_user_id` - The user to link +- `string $relationship` - Relationship type (default: 'spouse') + +**Returns**: `array` with keys: +- `success` (bool) - Whether the link was created +- `message` (string) - Status message +- `link_id` (int) - ID of created link (on success) + +**Validation**: +- Primary and secondary user IDs must be different +- Primary user must have active membership +- Secondary user must exist +- Link must not already exist + +**Side Effects**: +- Creates membership_links record +- Creates default permission records +- Uses transaction (rolls back on failure) + +### getUserMembershipLink() +**Purpose**: Check if a user has access through a secondary membership link + +**Parameters**: +- `int $user_id` - User to check + +**Returns**: `array` with keys: +- `has_access` (bool) - Whether user has access via link +- `primary_user_id` (int|null) - ID of primary account holder +- `relationship` (string|null) - Relationship type + +**Validation**: +- Verifies the link exists +- Checks primary user has active membership +- Validates payment status and expiration date +- Confirms indemnity waiver accepted + +### getLinkedSecondaryUsers() +**Purpose**: Get all secondary users linked to a primary user's membership + +**Parameters**: +- `int $primary_user_id` - The primary membership holder + +**Returns**: `array` of linked users with: +- `link_id` - Link ID +- `user_id` - Secondary user ID +- `first_name` - User's first name +- `last_name` - User's last name +- `email` - User's email +- `relationship` - Relationship type +- `linked_at` - When the link was created + +### unlinkSecondaryUser() +**Purpose**: Remove a secondary user from a primary user's membership + +**Parameters**: +- `int $link_id` - The membership link ID to remove +- `int $primary_user_id` - The primary user (for verification) + +**Returns**: `array` with keys: +- `success` (bool) - Whether the unlink was successful +- `message` (string) - Status message + +**Validation**: +- Verifies link exists +- Confirms primary user owns the link +- Uses transaction (rolls back on failure) + +## API Endpoints + +### POST /link_membership_user +**Purpose**: Link a new secondary user to the requester's membership + +**Required Parameters**: +- `secondary_email` (string) - Email of user to link +- `relationship` (string, optional) - Relationship type (default: 'spouse') +- `csrf_token` (string) - CSRF token + +**Response**: +```json +{ + "success": true, + "message": "User successfully linked to membership", + "link_id": 123 +} +``` + +**Error Responses**: +- 403: Forbidden (not authenticated or POST required) +- 400: Bad Request (invalid CSRF, missing email, user not found, or linking failed) + +**Access Control**: +- Authenticated users only +- Can only link to own membership + +### POST /unlink_membership_user +**Purpose**: Remove a secondary user from the requester's membership + +**Required Parameters**: +- `link_id` (int) - ID of the link to remove +- `csrf_token` (string) - CSRF token + +**Response**: +```json +{ + "success": true, + "message": "User successfully unlinked from membership" +} +``` + +**Error Responses**: +- 403: Forbidden (not authenticated or POST required) +- 400: Bad Request (invalid CSRF, link not found, or unauthorized) + +**Access Control**: +- Authenticated users only +- Can only remove links from own membership + +## Integration Points + +### Updated getUserMemberStatus() +The `getUserMemberStatus()` function now checks both: +1. Direct membership (user has membership_application and membership_fees) +2. Secondary membership (user is linked to another user's active membership) + +When user doesn't have direct membership, it automatically checks if they're linked to someone else's active membership. + +### Member Access Checks +All member-only pages should use `getUserMemberStatus()` which now automatically handles: +- Direct members +- Secondary members via links +- Expired memberships +- Indemnity waiver validation + +## Use Cases + +### Spouse/Partner Access +1. User A (primary) has active membership +2. User B (spouse) links to User A's membership +3. User B can now: + - Access member areas + - Receive member pricing + - Book campsites + - Book courses + - Book trips + +### Renewal +- When primary membership renews, secondary users automatically maintain access +- No need to re-create links on renewal + +### Membership Termination +- If primary membership expires, secondary users lose access +- Primary user can manually unlink secondary users anytime + +## Usage Examples + +### Linking a User +```php +$result = linkSecondaryUserToMembership( + $_SESSION['user_id'], + 'spouse@example.com', + 'spouse' +); + +if ($result['success']) { + $_SESSION['message'] = 'Successfully linked ' . $partner_email . ' to your membership'; +} else { + $_SESSION['error'] = $result['message']; +} +``` + +### Checking User Access +```php +if (getUserMemberStatus($user_id)) { + // User has direct or linked membership + echo "Welcome member!"; +} else { + // Redirect to membership page + header('Location: membership'); +} +``` + +### Getting Linked Users +```php +$linkedUsers = getLinkedSecondaryUsers($_SESSION['user_id']); +foreach ($linkedUsers as $user) { + echo "Linked: " . $user['first_name'] . ' (' . $user['relationship'] . ')'; +} +``` + +### Removing a Link +```php +$result = unlinkSecondaryUser($link_id, $_SESSION['user_id']); +if ($result['success']) { + echo "User unlinked successfully"; +} else { + echo "Error: " . $result['message']; +} +``` + +## Security Considerations + +### Authorization +- Users can only link to their own membership +- Users can only manage their own links +- Secondary users cannot create or modify links (primary user only) + +### Data Validation +- Email validation before linking +- User existence verification +- Duplicate link prevention +- CSRF token validation on all operations + +### Relationships +- Foreign keys prevent orphaned links +- CASCADE DELETE ensures cleanup when users are deleted +- Transactions ensure consistency + +## Testing Checklist + +- [ ] Link new user to own membership +- [ ] Attempt to link non-existent user (error) +- [ ] Attempt to link same user twice (error) +- [ ] Secondary user can access member areas +- [ ] Secondary user receives member pricing +- [ ] Unlink secondary user +- [ ] Unlinked user cannot access member areas +- [ ] Primary user can see list of linked users +- [ ] Linked user appears in notifications (if applicable) +- [ ] Membership renewal maintains links +- [ ] Expired membership removes secondary access +- [ ] Deleting user removes their links +- [ ] Permission records created on link +- [ ] Cannot link without active primary membership +- [ ] Cannot link if different user attempts +- [ ] CSRF token validation works + +## Future Enhancements + +1. **Admin Management**: Allow admins to create/remove links for members +2. **Selective Permissions**: Allow customizing which permissions each secondary user has +3. **Invitations**: Send email invitations to secondary users to accept +4. **Multiple Links**: Allow primary users to link multiple users (families) +5. **UI Dashboard**: Create page for managing linked accounts +6. **Notifications**: Notify secondary users when linked +7. **Payment Tracking**: Track which user made payments for membership +8. **Audit Log**: Log all link/unlink operations for compliance + +## Migration Instructions + +1. Run migration 004 to create tables and permissions table +2. Update `src/config/functions.php` with new linking functions +3. Update `getUserMemberStatus()` to check links +4. Add routes to `.htaccess` for new endpoints +5. Deploy processors for link/unlink operations +6. Test with married couple accounts +7. Document for users in membership information + diff --git a/docs/migrations/004_create_membership_linking_tables.sql b/docs/migrations/004_create_membership_linking_tables.sql new file mode 100644 index 00000000..82f19309 --- /dev/null +++ b/docs/migrations/004_create_membership_linking_tables.sql @@ -0,0 +1,63 @@ +-- Migration 004: Create membership linking tables +-- Purpose: Allow multiple users to share a single membership (for couples, families, etc) + +-- Create membership_links table to associate secondary users with primary membership accounts +CREATE TABLE IF NOT EXISTS `membership_links` ( + `link_id` INT AUTO_INCREMENT PRIMARY KEY, + `primary_user_id` INT NOT NULL, + `secondary_user_id` INT NOT NULL, + `relationship` VARCHAR(50) NOT NULL DEFAULT 'spouse', -- spouse, family member, etc + `linked_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + CONSTRAINT `fk_membership_links_primary` FOREIGN KEY (`primary_user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_membership_links_secondary` FOREIGN KEY (`secondary_user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + + -- Indexes for performance + INDEX `idx_primary_user` (`primary_user_id`), + INDEX `idx_secondary_user` (`secondary_user_id`), + + -- Prevent duplicate links (user cannot be linked twice) + UNIQUE KEY `unique_link` (`primary_user_id`, `secondary_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Create membership_permissions table to define what secondary users can access +CREATE TABLE IF NOT EXISTS `membership_permissions` ( + `permission_id` INT AUTO_INCREMENT PRIMARY KEY, + `link_id` INT NOT NULL, + `permission_name` VARCHAR(100) NOT NULL, -- 'access_member_areas', 'member_pricing', 'book_campsites', etc + `granted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign key + CONSTRAINT `fk_membership_permissions_link` FOREIGN KEY (`link_id`) + REFERENCES `membership_links`(`link_id`) ON DELETE CASCADE ON UPDATE CASCADE, + + -- Indexes + INDEX `idx_link` (`link_id`), + + -- Prevent duplicate permissions + UNIQUE KEY `unique_permission` (`link_id`, `permission_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Add foreign key to membership_fees to support links (optional - for tracking which membership fee covers the linked users) +-- ALTER TABLE `membership_fees` ADD COLUMN `primary_user_id` INT AFTER `user_id`; +-- This allows you to see if a fee was paid by primary or secondary user while maintaining the relationship + +-- Create a view to easily get all users linked to a membership +CREATE OR REPLACE VIEW `linked_membership_users` AS +SELECT + primary_user_id, + secondary_user_id, + relationship, + linked_at +FROM membership_links +UNION ALL +SELECT + primary_user_id, + primary_user_id as secondary_user_id, + 'primary' as relationship, + linked_at +FROM membership_links; diff --git a/src/config/functions.php b/src/config/functions.php index 56ab884a..60157785 100644 --- a/src/config/functions.php +++ b/src/config/functions.php @@ -412,7 +412,7 @@ function getUserMemberStatus($user_id) return false; } - // Step 1: Check if the user is a member + // Step 1: Check if the user is a direct member $queryUser = "SELECT member FROM users WHERE user_id = ?"; $stmtUser = $conn->prepare($queryUser); if (!$stmtUser) { @@ -445,7 +445,10 @@ function getUserMemberStatus($user_id) if ($resultApplication->num_rows === 0) { error_log("No membership application found for user_id: $user_id"); - return false; + // Check if user is linked to another user's membership + $linkedStatus = getUserMembershipLink($user_id); + $conn->close(); + return $linkedStatus['has_access']; } $application = $resultApplication->fetch_assoc(); @@ -484,8 +487,10 @@ function getUserMemberStatus($user_id) $membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date); if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) { + $conn->close(); return true; // Membership is active } else { + $conn->close(); return false; } @@ -2906,3 +2911,249 @@ function optimizeImage($filePath, $maxWidth = 1920, $maxHeight = 1080) return $success; } + +/** + * Link a secondary user to a primary user's membership + * @param int $primary_user_id The main membership holder + * @param int $secondary_user_id The user to link (spouse, family member, etc) + * @param string $relationship The relationship type (spouse, family_member, etc) + * @return array ['success' => bool, 'message' => string] + */ +function linkSecondaryUserToMembership($primary_user_id, $secondary_user_id, $relationship = 'spouse') +{ + $conn = openDatabaseConnection(); + + if ($conn === null) { + return ['success' => false, 'message' => 'Database connection failed']; + } + + // Validation: primary and secondary user IDs must be different + if ($primary_user_id === $secondary_user_id) { + return ['success' => false, 'message' => 'Cannot link user to themselves']; + } + + // Validation: primary user must have active membership + if (!getUserMemberStatus($primary_user_id)) { + $conn->close(); + return ['success' => false, 'message' => 'Primary user does not have active membership']; + } + + // Validation: secondary user must exist + $userCheck = $conn->prepare("SELECT user_id FROM users WHERE user_id = ?"); + $userCheck->bind_param("i", $secondary_user_id); + $userCheck->execute(); + $userResult = $userCheck->get_result(); + $userCheck->close(); + + if ($userResult->num_rows === 0) { + $conn->close(); + return ['success' => false, 'message' => 'Secondary user does not exist']; + } + + // Check if link already exists + $existingLink = $conn->prepare("SELECT link_id FROM membership_links WHERE primary_user_id = ? AND secondary_user_id = ?"); + $existingLink->bind_param("ii", $primary_user_id, $secondary_user_id); + $existingLink->execute(); + $existingResult = $existingLink->get_result(); + $existingLink->close(); + + if ($existingResult->num_rows > 0) { + $conn->close(); + return ['success' => false, 'message' => 'Users are already linked']; + } + + try { + // Start transaction + $conn->begin_transaction(); + + // Insert link + $insertLink = $conn->prepare(" + INSERT INTO membership_links (primary_user_id, secondary_user_id, relationship, linked_at, created_at) + VALUES (?, ?, ?, NOW(), NOW()) + "); + $insertLink->bind_param("iis", $primary_user_id, $secondary_user_id, $relationship); + $insertLink->execute(); + $linkId = $conn->insert_id; + $insertLink->close(); + + // Grant default permissions to secondary user + $permissions = [ + 'access_member_areas', + 'member_pricing', + 'book_campsites', + 'book_courses', + 'book_trips' + ]; + + foreach ($permissions as $permission) { + $insertPerm = $conn->prepare(" + INSERT INTO membership_permissions (link_id, permission_name, granted_at) + VALUES (?, ?, NOW()) + "); + $insertPerm->bind_param("is", $linkId, $permission); + $insertPerm->execute(); + $insertPerm->close(); + } + + // Commit transaction + $conn->commit(); + $conn->close(); + + return ['success' => true, 'message' => 'User successfully linked to membership', 'link_id' => $linkId]; + + } catch (Exception $e) { + $conn->rollback(); + $conn->close(); + return ['success' => false, 'message' => 'Failed to create link: ' . $e->getMessage()]; + } +} + +/** + * Check if a user has access through a membership link + * @param int $user_id The user to check + * @return array ['has_access' => bool, 'primary_user_id' => int|null, 'relationship' => string|null] + */ +function getUserMembershipLink($user_id) +{ + $conn = openDatabaseConnection(); + + if ($conn === null) { + return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null]; + } + + // Check if user is a secondary user with active link + $query = " + SELECT ml.primary_user_id, ml.relationship + FROM membership_links ml + JOIN membership_fees mf ON ml.primary_user_id = mf.user_id + JOIN membership_application ma ON ml.primary_user_id = ma.user_id + WHERE ml.secondary_user_id = ? + AND ma.accept_indemnity = 1 + AND mf.payment_status = 'PAID' + AND mf.membership_end_date >= CURDATE() + LIMIT 1 + "; + + $stmt = $conn->prepare($query); + $stmt->bind_param("i", $user_id); + $stmt->execute(); + $result = $stmt->get_result(); + $stmt->close(); + + if ($result->num_rows > 0) { + $link = $result->fetch_assoc(); + $conn->close(); + return [ + 'has_access' => true, + 'primary_user_id' => $link['primary_user_id'], + 'relationship' => $link['relationship'] + ]; + } + + $conn->close(); + return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null]; +} + +/** + * Get all secondary users linked to a primary user + * @param int $primary_user_id The primary membership holder + * @return array Array of linked users with their info + */ +function getLinkedSecondaryUsers($primary_user_id) +{ + $conn = openDatabaseConnection(); + + if ($conn === null) { + return []; + } + + $query = " + SELECT + ml.link_id, + u.user_id, + u.first_name, + u.last_name, + u.email, + ml.relationship, + ml.linked_at + FROM membership_links ml + JOIN users u ON ml.secondary_user_id = u.user_id + WHERE ml.primary_user_id = ? + ORDER BY ml.linked_at DESC + "; + + $stmt = $conn->prepare($query); + $stmt->bind_param("i", $primary_user_id); + $stmt->execute(); + $result = $stmt->get_result(); + $stmt->close(); + + $linkedUsers = []; + while ($row = $result->fetch_assoc()) { + $linkedUsers[] = $row; + } + + $conn->close(); + return $linkedUsers; +} + +/** + * Unlink a secondary user from a primary user's membership + * @param int $link_id The membership link ID to remove + * @param int $primary_user_id The primary user (for verification) + * @return array ['success' => bool, 'message' => string] + */ +function unlinkSecondaryUser($link_id, $primary_user_id) +{ + $conn = openDatabaseConnection(); + + if ($conn === null) { + return ['success' => false, 'message' => 'Database connection failed']; + } + + // Verify that this link belongs to the primary user + $linkCheck = $conn->prepare("SELECT primary_user_id FROM membership_links WHERE link_id = ?"); + $linkCheck->bind_param("i", $link_id); + $linkCheck->execute(); + $linkResult = $linkCheck->get_result(); + $linkCheck->close(); + + if ($linkResult->num_rows === 0) { + $conn->close(); + return ['success' => false, 'message' => 'Link not found']; + } + + $linkData = $linkResult->fetch_assoc(); + if ($linkData['primary_user_id'] !== $primary_user_id) { + $conn->close(); + return ['success' => false, 'message' => 'Unauthorized: you do not have permission to remove this link']; + } + + try { + // Start transaction + $conn->begin_transaction(); + + // Delete permissions first (cascade should handle this but being explicit) + $deletePerm = $conn->prepare("DELETE FROM membership_permissions WHERE link_id = ?"); + $deletePerm->bind_param("i", $link_id); + $deletePerm->execute(); + $deletePerm->close(); + + // Delete the link + $deleteLink = $conn->prepare("DELETE FROM membership_links WHERE link_id = ?"); + $deleteLink->bind_param("i", $link_id); + $deleteLink->execute(); + $deleteLink->close(); + + // Commit transaction + $conn->commit(); + $conn->close(); + + return ['success' => true, 'message' => 'User successfully unlinked from membership']; + + } catch (Exception $e) { + $conn->rollback(); + $conn->close(); + return ['success' => false, 'message' => 'Failed to remove link: ' . $e->getMessage()]; + } +} diff --git a/src/processors/link_membership_user.php b/src/processors/link_membership_user.php new file mode 100644 index 00000000..7b45b66d --- /dev/null +++ b/src/processors/link_membership_user.php @@ -0,0 +1,57 @@ + false, 'message' => 'Forbidden'])); +} + +// Validate CSRF token +if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + http_response_code(400); + exit(json_encode(['success' => false, 'message' => 'Invalid request'])); +} + +$primary_user_id = intval($_SESSION['user_id']); +$secondary_email = trim($_POST['secondary_email'] ?? ''); +$relationship = trim($_POST['relationship'] ?? 'spouse'); + +if (empty($secondary_email)) { + http_response_code(400); + exit(json_encode(['success' => false, 'message' => 'Secondary user email is required'])); +} + +// Get the secondary user by email +$conn = openDatabaseConnection(); +$userQuery = $conn->prepare("SELECT user_id FROM users WHERE email = ?"); +$userQuery->bind_param("s", $secondary_email); +$userQuery->execute(); +$userResult = $userQuery->get_result(); +$userQuery->close(); + +if ($userResult->num_rows === 0) { + $conn->close(); + http_response_code(404); + exit(json_encode(['success' => false, 'message' => 'User with that email not found'])); +} + +$user = $userResult->fetch_assoc(); +$secondary_user_id = $user['user_id']; +$conn->close(); + +// Use the linking function from functions.php +$result = linkSecondaryUserToMembership($primary_user_id, $secondary_user_id, $relationship); + +http_response_code($result['success'] ? 200 : 400); +echo json_encode([ + 'success' => $result['success'], + 'message' => $result['message'], + 'link_id' => $result['link_id'] ?? null +]); +?> diff --git a/src/processors/unlink_membership_user.php b/src/processors/unlink_membership_user.php new file mode 100644 index 00000000..560e530c --- /dev/null +++ b/src/processors/unlink_membership_user.php @@ -0,0 +1,37 @@ + false, 'message' => 'Forbidden'])); +} + +// Validate CSRF token +if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + http_response_code(400); + exit(json_encode(['success' => false, 'message' => 'Invalid request'])); +} + +$primary_user_id = intval($_SESSION['user_id']); +$link_id = intval($_POST['link_id'] ?? 0); + +if (!$link_id) { + http_response_code(400); + exit(json_encode(['success' => false, 'message' => 'Link ID is required'])); +} + +// Use the unlinking function from functions.php +$result = unlinkSecondaryUser($link_id, $primary_user_id); + +http_response_code($result['success'] ? 200 : 400); +echo json_encode([ + 'success' => $result['success'], + 'message' => $result['message'] +]); +?> diff --git a/uploads/blogs/1/images/cover.jpg b/uploads/blogs/1/images/cover.jpg new file mode 100644 index 00000000..010a7fde Binary files /dev/null and b/uploads/blogs/1/images/cover.jpg differ diff --git a/uploads/blogs/10/images/blog_13.jpeg b/uploads/blogs/10/images/blog_13.jpeg new file mode 100644 index 00000000..3aa13efc Binary files /dev/null and b/uploads/blogs/10/images/blog_13.jpeg differ diff --git a/uploads/blogs/12/images/688505875d439-mceclip0.jpg b/uploads/blogs/12/images/688505875d439-mceclip0.jpg new file mode 100644 index 00000000..0252a564 Binary files /dev/null and b/uploads/blogs/12/images/688505875d439-mceclip0.jpg differ diff --git a/uploads/blogs/16/images/68852afa55ff7-blobid0.jpg b/uploads/blogs/16/images/68852afa55ff7-blobid0.jpg new file mode 100644 index 00000000..36719aac Binary files /dev/null and b/uploads/blogs/16/images/68852afa55ff7-blobid0.jpg differ diff --git a/uploads/blogs/16/images/68852afc7edc7-blobid1.jpg b/uploads/blogs/16/images/68852afc7edc7-blobid1.jpg new file mode 100644 index 00000000..1c9bb70b Binary files /dev/null and b/uploads/blogs/16/images/68852afc7edc7-blobid1.jpg differ diff --git a/uploads/blogs/16/images/68852aff41d75-blobid2.jpg b/uploads/blogs/16/images/68852aff41d75-blobid2.jpg new file mode 100644 index 00000000..de89cdaa Binary files /dev/null and b/uploads/blogs/16/images/68852aff41d75-blobid2.jpg differ diff --git a/uploads/blogs/2/images/agm.jpg b/uploads/blogs/2/images/agm.jpg new file mode 100644 index 00000000..b4bca1fc Binary files /dev/null and b/uploads/blogs/2/images/agm.jpg differ diff --git a/uploads/blogs/images/68832295c3fd2-mceclip0.jpg b/uploads/blogs/images/68832295c3fd2-mceclip0.jpg new file mode 100644 index 00000000..0252a564 Binary files /dev/null and b/uploads/blogs/images/68832295c3fd2-mceclip0.jpg differ diff --git a/uploads/blogs/images/6883231f78d29-mceclip0.jpg b/uploads/blogs/images/6883231f78d29-mceclip0.jpg new file mode 100644 index 00000000..0252a564 Binary files /dev/null and b/uploads/blogs/images/6883231f78d29-mceclip0.jpg differ diff --git a/uploads/blogs/images/688323d3546c7-mceclip0.jpg b/uploads/blogs/images/688323d3546c7-mceclip0.jpg new file mode 100644 index 00000000..0252a564 Binary files /dev/null and b/uploads/blogs/images/688323d3546c7-mceclip0.jpg differ diff --git a/uploads/blogs/images/688326384b664-blobid0.jpg b/uploads/blogs/images/688326384b664-blobid0.jpg new file mode 100644 index 00000000..52a0a420 Binary files /dev/null and b/uploads/blogs/images/688326384b664-blobid0.jpg differ diff --git a/uploads/blogs/images/ChatGPT_Image_Jul_10__2025__01_55_37_PM.png b/uploads/blogs/images/ChatGPT_Image_Jul_10__2025__01_55_37_PM.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/ChatGPT_Image_Jul_10__2025__01_55_37_PM.png differ diff --git a/uploads/blogs/images/ChatGPT_Image_Jul_5__2025__04_27_40_PM.png b/uploads/blogs/images/ChatGPT_Image_Jul_5__2025__04_27_40_PM.png new file mode 100644 index 00000000..7c3ee4fd Binary files /dev/null and b/uploads/blogs/images/ChatGPT_Image_Jul_5__2025__04_27_40_PM.png differ diff --git a/uploads/blogs/images/agm.jpg b/uploads/blogs/images/agm.jpg new file mode 100644 index 00000000..b4bca1fc Binary files /dev/null and b/uploads/blogs/images/agm.jpg differ diff --git a/uploads/blogs/images/cover.jpg b/uploads/blogs/images/cover.jpg new file mode 100644 index 00000000..010a7fde Binary files /dev/null and b/uploads/blogs/images/cover.jpg differ diff --git a/uploads/blogs/images/freepik__the-style-is-candid-image-photography-with-natural__95095.png b/uploads/blogs/images/freepik__the-style-is-candid-image-photography-with-natural__95095.png new file mode 100644 index 00000000..9a5777fa Binary files /dev/null and b/uploads/blogs/images/freepik__the-style-is-candid-image-photography-with-natural__95095.png differ diff --git a/uploads/blogs/images/img_688333223a9875.21195611.jpeg b/uploads/blogs/images/img_688333223a9875.21195611.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_688333223a9875.21195611.jpeg differ diff --git a/uploads/blogs/images/img_6883332d8ae810.53515312.jpeg b/uploads/blogs/images/img_6883332d8ae810.53515312.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_6883332d8ae810.53515312.jpeg differ diff --git a/uploads/blogs/images/img_6883333c8c73e9.31337036.jpeg b/uploads/blogs/images/img_6883333c8c73e9.31337036.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_6883333c8c73e9.31337036.jpeg differ diff --git a/uploads/blogs/images/img_688333443336c9.61193090.jpeg b/uploads/blogs/images/img_688333443336c9.61193090.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_688333443336c9.61193090.jpeg differ diff --git a/uploads/blogs/images/img_6883334b8ade90.16958069.jpeg b/uploads/blogs/images/img_6883334b8ade90.16958069.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_6883334b8ade90.16958069.jpeg differ diff --git a/uploads/blogs/images/img_6883335ac2de02.42545621.jpeg b/uploads/blogs/images/img_6883335ac2de02.42545621.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_6883335ac2de02.42545621.jpeg differ diff --git a/uploads/blogs/images/img_68833369c22dc0.61573038.jpeg b/uploads/blogs/images/img_68833369c22dc0.61573038.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_68833369c22dc0.61573038.jpeg differ diff --git a/uploads/blogs/images/img_68833378c3fd43.37434181.jpeg b/uploads/blogs/images/img_68833378c3fd43.37434181.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_68833378c3fd43.37434181.jpeg differ diff --git a/uploads/blogs/images/img_68833387c70b34.04495696.jpeg b/uploads/blogs/images/img_68833387c70b34.04495696.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_68833387c70b34.04495696.jpeg differ diff --git a/uploads/blogs/images/img_688333a8c544e8.82781438.jpeg b/uploads/blogs/images/img_688333a8c544e8.82781438.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_688333a8c544e8.82781438.jpeg differ diff --git a/uploads/blogs/images/img_688333e4c29329.87198676.jpeg b/uploads/blogs/images/img_688333e4c29329.87198676.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_688333e4c29329.87198676.jpeg differ diff --git a/uploads/blogs/images/img_688334169858d8.29663306.jpeg b/uploads/blogs/images/img_688334169858d8.29663306.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_688334169858d8.29663306.jpeg differ diff --git a/uploads/blogs/images/img_6883341d8a9925.24657182.jpeg b/uploads/blogs/images/img_6883341d8a9925.24657182.jpeg new file mode 100644 index 00000000..5348bb08 Binary files /dev/null and b/uploads/blogs/images/img_6883341d8a9925.24657182.jpeg differ diff --git a/uploads/blogs/images/img_68833ddde42767.42785358.png b/uploads/blogs/images/img_68833ddde42767.42785358.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833ddde42767.42785358.png differ diff --git a/uploads/blogs/images/img_68833de1c09f26.61535657.png b/uploads/blogs/images/img_68833de1c09f26.61535657.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833de1c09f26.61535657.png differ diff --git a/uploads/blogs/images/img_68833decf0a259.74677718.png b/uploads/blogs/images/img_68833decf0a259.74677718.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833decf0a259.74677718.png differ diff --git a/uploads/blogs/images/img_68833dfc295c47.36349054.png b/uploads/blogs/images/img_68833dfc295c47.36349054.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833dfc295c47.36349054.png differ diff --git a/uploads/blogs/images/img_68833e0b0f2534.16393329.png b/uploads/blogs/images/img_68833e0b0f2534.16393329.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833e0b0f2534.16393329.png differ diff --git a/uploads/blogs/images/img_68833e1a0d9ba6.15509169.png b/uploads/blogs/images/img_68833e1a0d9ba6.15509169.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833e1a0d9ba6.15509169.png differ diff --git a/uploads/blogs/images/img_68833e291b79a8.65551250.png b/uploads/blogs/images/img_68833e291b79a8.65551250.png new file mode 100644 index 00000000..ce9a2c85 Binary files /dev/null and b/uploads/blogs/images/img_68833e291b79a8.65551250.png differ diff --git a/uploads/pop/1 C. PINTO.pdf b/uploads/pop/1 C. PINTO.pdf new file mode 100644 index 00000000..0d357666 Binary files /dev/null and b/uploads/pop/1 C. PINTO.pdf differ diff --git a/uploads/pop/COURSE_08-23_C._PINTO.pdf b/uploads/pop/COURSE_08-23_C._PINTO.pdf new file mode 100644 index 00000000..46508966 Binary files /dev/null and b/uploads/pop/COURSE_08-23_C._PINTO.pdf differ diff --git a/uploads/pop/KZN2026_C._PINTO.pdf b/uploads/pop/KZN2026_C._PINTO.pdf new file mode 100644 index 00000000..a2921393 Binary files /dev/null and b/uploads/pop/KZN2026_C._PINTO.pdf differ diff --git a/uploads/signatures/signature_154.png b/uploads/signatures/signature_154.png new file mode 100644 index 00000000..8e0301f0 Binary files /dev/null and b/uploads/signatures/signature_154.png differ