From f610adf2fe09eae08b5574e7d869587de0624961 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 25 Apr 2026 09:52:21 +0530 Subject: [PATCH] feat: add user profile photo upload and show avatar in header --- app/Controllers/Home.php | 132 +++++++++++++++++++++++++++++-- app/Views/account/my_profile.php | 76 +++++++++++++++--- app/Views/partials/header.php | 16 +++- 3 files changed, 206 insertions(+), 18 deletions(-) diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index 05a6d8a..991c5a9 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -6,6 +6,8 @@ use CodeIgniter\Shield\Models\UserModel; class Home extends BaseController { + private const USER_PROFILE_PHOTO_DIR = 'assets/images/user_profile'; + private function ensureAuthenticated() { if (function_exists('auth') && auth()->loggedIn()) { @@ -233,6 +235,7 @@ class Home extends BaseController 'user' => $user, 'phone_no' => $meta['phone_no'] ?? '', 'person_name' => $meta['full_name'] ?? '', + 'profile_photo_url' => $this->buildUserProfilePhotoUrl((string) ($meta['profile_photo'] ?? '')), ]); } @@ -387,6 +390,57 @@ class Home extends BaseController $phoneNo = trim((string) $this->request->getPost('phone_no')); $password = (string) $this->request->getPost('password'); $passwordConfirm = (string) $this->request->getPost('password_confirm'); + $metaBeforeUpdate = $this->getCurrentUserProfileMeta($userId); + + $profilePhotoFile = $this->request->getFile('profile_photo'); + if ($profilePhotoFile !== null && $profilePhotoFile->getError() !== UPLOAD_ERR_NO_FILE && ! $profilePhotoFile->isValid()) { + return $this->response->setJSON([ + 'update' => 0, + 'error_text' => 'Invalid profile photo upload.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + $photoProvided = $profilePhotoFile !== null + && $profilePhotoFile->isValid() + && $profilePhotoFile->getError() !== UPLOAD_ERR_NO_FILE; + $profilePhotoFileName = ''; + + if ($photoProvided) { + $photoSize = (int) $profilePhotoFile->getSize(); + $photoMime = strtolower((string) $profilePhotoFile->getMimeType()); + $allowedMime = [ + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/webp', + ]; + + if ($photoSize <= 0 || $photoSize > (2 * 1024 * 1024)) { + return $this->response->setJSON([ + 'update' => 0, + 'error_text' => 'Profile photo must be less than 2MB.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + if (! in_array($photoMime, $allowedMime, true)) { + return $this->response->setJSON([ + 'update' => 0, + 'error_text' => 'Profile photo must be JPG, PNG, or WEBP.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + $extension = strtolower((string) $profilePhotoFile->getExtension()); + if ($extension === '') { + $extension = $photoMime === 'image/png' ? 'png' : ($photoMime === 'image/webp' ? 'webp' : 'jpg'); + } + $profilePhotoFileName = 'user_' . $userId . '_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $extension; + } if ($personName !== '' && mb_strlen($personName) > 120) { return $this->response->setJSON([ @@ -482,7 +536,45 @@ class Home extends BaseController ]); } - $this->setCurrentUserProfileMeta($userId, $phoneNo, $personName); + if ($photoProvided) { + $uploadDir = FCPATH . self::USER_PROFILE_PHOTO_DIR; + if (! is_dir($uploadDir) && ! @mkdir($uploadDir, 0755, true) && ! is_dir($uploadDir)) { + return $this->response->setJSON([ + 'update' => 0, + 'error_text' => 'Unable to create profile photo directory.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + try { + $profilePhotoFile->move($uploadDir, $profilePhotoFileName, true); + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'update' => 0, + 'error_text' => 'Unable to upload profile photo.', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + $oldPhoto = trim((string) ($metaBeforeUpdate['profile_photo'] ?? '')); + if ($oldPhoto !== '' && $oldPhoto !== $profilePhotoFileName) { + $oldPhotoPath = $uploadDir . DIRECTORY_SEPARATOR . basename($oldPhoto); + if (is_file($oldPhotoPath)) { + @unlink($oldPhotoPath); + } + } + } + + $this->setCurrentUserProfileMeta( + $userId, + $phoneNo, + $personName, + $photoProvided ? $profilePhotoFileName : null + ); + + $metaAfterUpdate = $this->getCurrentUserProfileMeta($userId); $displayName = trim((string) ($user->username ?? '')); if ($personName !== '') { @@ -500,24 +592,25 @@ class Home extends BaseController 'user_id' => $userId, 'email' => $email, 'phone_no' => $phoneNo, + 'profile_photo_url' => $this->buildUserProfilePhotoUrl((string) ($metaAfterUpdate['profile_photo'] ?? '')), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** - * @return array{phone_no:string, full_name:string} + * @return array{phone_no:string, full_name:string, profile_photo:string} */ private function getCurrentUserProfileMeta(int $userId): array { if ($userId <= 0) { - return ['phone_no' => '', 'full_name' => '']; + return ['phone_no' => '', 'full_name' => '', 'profile_photo' => '']; } $tables = config('Auth')->tables; $identitiesTable = (string) ($tables['identities'] ?? 'auth_identities'); if (! $this->db->tableExists($identitiesTable)) { - return ['phone_no' => '', 'full_name' => '']; + return ['phone_no' => '', 'full_name' => '', 'profile_photo' => '']; } $row = $this->db->table($identitiesTable) @@ -529,21 +622,22 @@ class Home extends BaseController $extraRaw = trim((string) ($row['extra'] ?? '')); if ($extraRaw === '') { - return ['phone_no' => '', 'full_name' => '']; + return ['phone_no' => '', 'full_name' => '', 'profile_photo' => '']; } $decoded = json_decode($extraRaw, true); if (! is_array($decoded)) { - return ['phone_no' => '', 'full_name' => '']; + return ['phone_no' => '', 'full_name' => '', 'profile_photo' => '']; } return [ 'phone_no' => trim((string) ($decoded['phone_no'] ?? '')), 'full_name' => trim((string) ($decoded['full_name'] ?? '')), + 'profile_photo' => trim((string) ($decoded['profile_photo'] ?? '')), ]; } - private function setCurrentUserProfileMeta(int $userId, string $phoneNo, string $fullName): void + private function setCurrentUserProfileMeta(int $userId, string $phoneNo, string $fullName, ?string $profilePhoto = null): void { if ($userId <= 0) { return; @@ -587,10 +681,34 @@ class Home extends BaseController $payload['full_name'] = $fullName; } + if ($profilePhoto !== null) { + $profilePhoto = trim($profilePhoto); + if ($profilePhoto === '') { + unset($payload['profile_photo']); + } else { + $payload['profile_photo'] = basename($profilePhoto); + } + } + $this->db->table($identitiesTable) ->where('id', (int) $row['id']) ->update([ 'extra' => empty($payload) ? null : json_encode($payload, JSON_UNESCAPED_UNICODE), ]); } + + private function buildUserProfilePhotoUrl(string $photoFileName): string + { + $photoFileName = trim($photoFileName); + if ($photoFileName === '') { + return base_url('assets/img/profile-img.jpg'); + } + + $absolutePath = FCPATH . self::USER_PROFILE_PHOTO_DIR . DIRECTORY_SEPARATOR . basename($photoFileName); + if (! is_file($absolutePath)) { + return base_url('assets/img/profile-img.jpg'); + } + + return base_url(self::USER_PROFILE_PHOTO_DIR . '/' . basename($photoFileName)); + } } diff --git a/app/Views/account/my_profile.php b/app/Views/account/my_profile.php index 00c529b..934d4bc 100644 --- a/app/Views/account/my_profile.php +++ b/app/Views/account/my_profile.php @@ -34,6 +34,21 @@ +
+
Profile Photo
+
+ Profile Photo +
+ +
Allowed: JPG, PNG, WEBP. Max size: 2MB.
+
+
+
+
Change Password (Optional)
@@ -95,22 +110,57 @@ setMsg('normal', 'Password fields cleared.'); }); + $('#profile_photo').on('change', function() { + var file = this.files && this.files[0] ? this.files[0] : null; + if (!file) { + return; + } + + var allowed = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (allowed.indexOf((file.type || '').toLowerCase()) < 0) { + $(this).val(''); + setMsg('err', 'Profile photo must be JPG, PNG, or WEBP.'); + return; + } + + if ((file.size || 0) > 2 * 1024 * 1024) { + $(this).val(''); + setMsg('err', 'Profile photo must be less than 2MB.'); + return; + } + + var blobUrl = URL.createObjectURL(file); + $('#profile_photo_preview').attr('src', blobUrl); + setMsg('normal', 'Photo selected. Click Save Profile to upload.'); + }); + $('#btn_profile_save').on('click', function() { - var payload = { - person_name: ($('#profile_person_name').val() || '').trim(), - email: ($('#profile_email').val() || '').trim(), - phone_no: ($('#profile_phone_no').val() || '').trim(), - password: ($('#profile_password').val() || '').toString(), - password_confirm: ($('#profile_password_confirm').val() || '').toString() - }; + var payload = new window.FormData(); + payload.append('person_name', ($('#profile_person_name').val() || '').trim()); + payload.append('email', ($('#profile_email').val() || '').trim()); + payload.append('phone_no', ($('#profile_phone_no').val() || '').trim()); + payload.append('password', ($('#profile_password').val() || '').toString()); + payload.append('password_confirm', ($('#profile_password_confirm').val() || '').toString()); + + var photoInput = document.getElementById('profile_photo'); + if (photoInput && photoInput.files && photoInput.files[0]) { + payload.append('profile_photo', photoInput.files[0]); + } var csrf = getCsrfPair(); - payload[csrf.name] = csrf.value; + payload.append(csrf.name, csrf.value); var $btn = $(this); $btn.prop('disabled', true).text('Saving...'); - $.post('', payload, function(data) { + $.ajax({ + url: '', + method: 'POST', + data: payload, + processData: false, + contentType: false, + dataType: 'json' + }).done(function(data) { updateCsrf(data); $btn.prop('disabled', false).text('Save Profile'); @@ -135,7 +185,13 @@ if (loginId) { $('#header-user-login-id').text('Login ID: ' + loginId); } - }, 'json').fail(function() { + + if (data.profile_photo_url) { + $('#profile_photo_preview').attr('src', data.profile_photo_url); + $('#header-user-avatar').attr('src', data.profile_photo_url); + $('#profile_photo').val(''); + } + }).fail(function() { $btn.prop('disabled', false).text('Save Profile'); setMsg('err', 'Unable to update profile'); }); diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php index 89332cc..c922087 100644 --- a/app/Views/partials/header.php +++ b/app/Views/partials/header.php @@ -51,6 +51,7 @@ $displayName = $loginId; $displayUserId = (int) ($authUser->id ?? 0); + $displayPhoto = ''; if ($displayUserId > 0) { $tables = config('Auth')->tables; @@ -73,12 +74,25 @@ if ($fullName !== '') { $displayName = $fullName; } + + $profilePhoto = trim((string) ($decoded['profile_photo'] ?? '')); + if ($profilePhoto !== '') { + $displayPhoto = basename($profilePhoto); + } } } } } } + $profileImageUrl = base_url('assets/img/profile-img.jpg'); + if ($displayPhoto !== '') { + $profileImageAbsolute = FCPATH . 'assets/images/user_profile/' . $displayPhoto; + if (is_file($profileImageAbsolute)) { + $profileImageUrl = base_url('assets/images/user_profile/' . $displayPhoto); + } + } + $serverTimeZoneId = 'Asia/Kolkata'; $serverNow = new DateTimeImmutable('now', new DateTimeZone($serverTimeZoneId)); $serverTimeZoneLabel = trim((string) $serverNow->format('T')); @@ -132,7 +146,7 @@