feat: add user profile photo upload and show avatar in header

This commit is contained in:
DevSoftTech Server 2026-04-25 09:52:21 +05:30
parent 4a4ef5b677
commit f610adf2fe
3 changed files with 206 additions and 18 deletions

View File

@ -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));
}
}

View File

@ -34,6 +34,21 @@
<input type="text" class="form-control form-control-sm" id="profile_phone_no" maxlength="20" value="<?= esc((string) ($phone_no ?? '')) ?>" placeholder="Enter phone number">
</div>
<div class="border rounded p-2 mb-3">
<div class="small fw-semibold mb-2">Profile Photo</div>
<div class="d-flex align-items-center gap-3 flex-wrap">
<img id="profile_photo_preview"
src="<?= esc((string) ($profile_photo_url ?? base_url('assets/img/profile-img.jpg'))) ?>"
alt="Profile Photo"
class="rounded-circle border"
style="width:72px;height:72px;object-fit:cover;">
<div>
<input type="file" class="form-control form-control-sm" id="profile_photo" accept="image/jpeg,image/png,image/webp">
<div class="small text-muted mt-1">Allowed: JPG, PNG, WEBP. Max size: 2MB.</div>
</div>
</div>
</div>
<div class="border rounded p-2 mb-3 bg-light">
<div class="small fw-semibold mb-2">Change Password (Optional)</div>
<div class="mb-2">
@ -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('<?= base_url('my-profile/save') ?>', payload, function(data) {
$.ajax({
url: '<?= base_url('my-profile/save') ?>',
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');
});

View File

@ -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 @@
</li>
<li class="nav-item dropdown pe-3">
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
<img src="<?= base_url('assets/img/profile-img.jpg') ?>" alt="Profile" class="rounded-circle">
<img src="<?= esc($profileImageUrl) ?>" alt="Profile" class="rounded-circle" id="header-user-avatar">
<span class="d-none d-md-block dropdown-toggle ps-2" id="header-user-with-id">
<?= esc($displayName) ?>
</span>