feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<x-app-layout>
|
||||
<div class="container-fluid mt-2" id="main-content">
|
||||
<div class="row gx-3 gx-lg-4">
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
|
||||
{{-- Foto + Summary --}}
|
||||
<div class="card adminuiux-card border-0 overflow-hidden mb-3">
|
||||
<div class="text-center position-relative p-3">
|
||||
|
||||
{{-- Foto avatar utama --}}
|
||||
<div class="avatar avatar-120 rounded-circle border border-3 border-white shadow mt-n5">
|
||||
<img src="assets/img/profile.png" class="rounded-circle">
|
||||
</div>
|
||||
|
||||
{{-- Nama & role user --}}
|
||||
<h5 class="mt-3 mb-0 fw-bold">{{ Auth::user()->name ?? 'Mobileuxer' }}</h5>
|
||||
<p class="text-secondary small">Admin</p>
|
||||
|
||||
{{-- Informasi user --}}
|
||||
<div class="text-start mt-3 small">
|
||||
<p><i class="bi bi-envelope me-2"></i> {{ Auth::user()->email }}</p>
|
||||
<p><i class="bi bi-telephone me-2"></i> {{ Auth::user()->phone ?? 'N/A' }}</p>
|
||||
<p><i class="bi bi-cake me-2"></i> {{ Auth::user()->date_of_birth ?? 'N/A' }}</p>
|
||||
<p><i class="bi bi-geo-alt me-2"></i> {{ Auth::user()->city ?? 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Delete Account --}}
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8">
|
||||
|
||||
{{-- Profile Information --}}
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
|
||||
{{-- Update Password --}}
|
||||
@include('profile.partials.update-password-form')
|
||||
|
||||
{{-- Passkey Registration (WebAuthn) --}}
|
||||
@if(get_setting('webauthn_enabled', false))
|
||||
@include('profile.partials.passkey-registration-form')
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,71 @@
|
||||
<div class="card adminuiux-card border-danger shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
|
||||
{{-- header and description --}}
|
||||
<h4 class="fw-bold text-danger mb-2">Delete Account</h4>
|
||||
<p class="text-secondary small mb-3">
|
||||
Once deleted, all your data will be permanently removed. Download any information you want to keep.
|
||||
</p>
|
||||
|
||||
{{-- trigger button for sweetalert confirmation --}}
|
||||
<button id="deleteAccountBtn" class="btn btn-danger mt-3 px-3">
|
||||
Delete Account
|
||||
</button>
|
||||
|
||||
{{-- delete account form (submitted after sweetalert confirmation) --}}
|
||||
<form id="deleteAccountForm" method="post" action="{{ route('profile.destroy') }}" style="display:none;">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<input type="hidden" name="password" id="passwordInput" required>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- sweetalert2 cdn --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
document.getElementById('deleteAccountBtn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// show confirmation dialog
|
||||
StandardSwal.fire({
|
||||
title: 'Delete Your Account?',
|
||||
text: "This action is irreversible. All your personal data and resources will be permanently wiped. Please enter your password to authorize this deletion.",
|
||||
icon: 'warning',
|
||||
input: 'password',
|
||||
inputPlaceholder: 'Confirm your password',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Permanently Delete',
|
||||
customClass: {
|
||||
confirmButton: 'btn-pill-danger',
|
||||
cancelButton: 'btn-pill-cancel'
|
||||
},
|
||||
reverseButtons: false,
|
||||
preConfirm: (password) => {
|
||||
if (!password)
|
||||
StandardSwal.showValidationMessage('Authentication password is required');
|
||||
return password;
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
// if confirmed, submit form
|
||||
if (result.isConfirmed) {
|
||||
document.getElementById('passwordInput').value = result.value;
|
||||
document.getElementById('deleteAccountForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{{-- sweetalert error if password is incorrect (server validation) --}}
|
||||
@if ($errors->userDeletion->has('password'))
|
||||
<script>
|
||||
StandardSwal.fire({
|
||||
icon: 'error',
|
||||
title: 'Verification Failed!',
|
||||
text: '{{ $errors->userDeletion->first("password") }}' || 'The password you provided is incorrect.',
|
||||
confirmButtonText: 'Try Again'
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@@ -0,0 +1,121 @@
|
||||
<section>
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<header>
|
||||
<h4 class="fw-bold mb-3">
|
||||
<i class="bi bi-fingerprint me-2"></i> {{ __('Passkeys (WebAuthn)') }}
|
||||
</h6>
|
||||
<p class="text-secondary small mb-4">
|
||||
{{ __('Register your biometrics (Fingerprint, FaceID) or security keys to login securely without a password.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div id="passkey-list" class="mb-4">
|
||||
@if(Auth::user()->webAuthnCredentials->count() > 0)
|
||||
<div class="list-group list-group-flush border-top border-bottom mb-3">
|
||||
@foreach(Auth::user()->webAuthnCredentials as $credential)
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center py-2 px-0">
|
||||
<div>
|
||||
<span class="fw-semibold small d-block">{{ $credential->alias ?: __('Unnamed Device') }}</span>
|
||||
<small class="text-muted">{{ __('Registered on') }}: {{ $credential->created_at->format('d M Y') }}</small>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('webauthn.destroy', $credential->id) }}" onsubmit="return confirm('{{ __('Are you sure you want to remove this passkey?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger border-0">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-light border small text-center py-3">
|
||||
{{ __('No passkeys registered yet.') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<button type="button" id="register-passkey" class="btn btn-primary theme-black px-4">
|
||||
{{ __('Register New Passkey') }}
|
||||
</button>
|
||||
<div id="passkey-status" class="small text-info d-none">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span> {{ __('Processing...') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(get_setting('webauthn_enabled', false))
|
||||
<script src="{{ asset('vendor/webauthn/webauthn.js') }}"></script>
|
||||
<script>
|
||||
document.getElementById('register-passkey').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('register-passkey');
|
||||
const status = document.getElementById('passkey-status');
|
||||
|
||||
btn.disabled = true;
|
||||
status.classList.remove('d-none');
|
||||
|
||||
if (typeof WebAuthn === 'undefined') {
|
||||
console.error('WebAuthn library not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
const webauthn = new WebAuthn();
|
||||
|
||||
if (WebAuthn.supportsWebAuthn()) {
|
||||
try {
|
||||
// Ask for a nickname for the key
|
||||
const alias = prompt("{{ __('Enter a name for this device/key (e.g. My Laptop, iPhone):') }}", "My Device");
|
||||
|
||||
if (alias === null) {
|
||||
btn.disabled = false;
|
||||
status.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Laragear WebAuthn v5 registration
|
||||
const response = await webauthn.register({ alias: alias });
|
||||
|
||||
if (response) {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebAuthn Error:', error);
|
||||
|
||||
let errorMsg = 'Failed to register Passkey. Please use a secure connection.';
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
errorMsg = 'Passkeys require a secure context (HTTPS). \n\n' +
|
||||
'Developer Tip: If you are on a local domain (like .test), you can bypass this in Chrome/Edge by going to: \n' +
|
||||
'chrome://flags/#unsafely-treat-insecure-origin-as-secure \n' +
|
||||
'and adding your domain to the list.';
|
||||
} else if (error.name === 'NotAllowedError') {
|
||||
errorMsg = 'Registration was cancelled or timed out.';
|
||||
} else {
|
||||
errorMsg = 'Failed to register Passkey. Ensure your device supports biometrics and is configured correctly.';
|
||||
}
|
||||
|
||||
StandardSwal.fire({
|
||||
icon: 'error',
|
||||
title: 'Registration Failed!',
|
||||
text: errorMsg || 'The system was unable to register your Passkey. Please ensure your device is compatible.',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
StandardSwal.fire({
|
||||
icon: 'info',
|
||||
title: 'Passkey Not Supported',
|
||||
text: 'Your current browser or device does not support secure Passkey authentication.',
|
||||
confirmButtonText: 'Understood'
|
||||
});
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
status.classList.add('d-none');
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
</section>
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="card adminuiux-card mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
{{-- header --}}
|
||||
<h4 class="fw-bold mb-3">Update Password</h4>
|
||||
<p class="text-secondary small mb-4">Make sure your new password is secure.</p>
|
||||
{{-- form update password --}}
|
||||
<form method="POST" action="{{ route('password.update') }}" autocomplete="off" class="ajax-form">
|
||||
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
{{-- anti autofill trap --}}
|
||||
<input type="text" name="fakeuser" style="display:none">
|
||||
<input type="password" name="fakepass" style="display:none">
|
||||
|
||||
{{-- Current Password --}}
|
||||
<div class="mb-3">
|
||||
<label for="update_password_current_password" class="form-label fw-semibold">
|
||||
{{ __('Current Password') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="update_password_current_password" name="current_password"
|
||||
class="form-control border-end-0" placeholder="Enter current password" required autocomplete="off" minlength="12"
|
||||
title="Enter your current password">
|
||||
<button class="btn btn-outline-secondary bg-white border-start-0 password-toggle" type="button" style="border-color: #dee2e6;">
|
||||
<i class="bi bi-eye text-secondary"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- New Password --}}
|
||||
<div class="mb-3">
|
||||
<label for="update_password_password" class="form-label fw-semibold">
|
||||
{{ __('New Password') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="update_password_password" name="password" class="form-control border-end-0"
|
||||
placeholder="Enter new password" required minlength="12" autocomplete="new-password"
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$"
|
||||
title="Minimum 12 characters with uppercase, lowercase, number, and symbol">
|
||||
<button class="btn btn-outline-secondary bg-white border-start-0 password-toggle" type="button" style="border-color: #dee2e6;">
|
||||
<i class="bi bi-eye text-secondary"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Confirm Password --}}
|
||||
<div class="mb-3">
|
||||
<label for="update_password_password_confirmation" class="form-label fw-semibold">
|
||||
{{ __('Confirm Password') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="update_password_password_confirmation" name="password_confirmation"
|
||||
class="form-control border-end-0" placeholder="Confirm new password" required minlength="12"
|
||||
autocomplete="new-password" title="Must match the new password exactly">
|
||||
<button class="btn btn-outline-secondary bg-white border-start-0 password-toggle" type="button" style="border-color: #dee2e6;">
|
||||
<i class="bi bi-eye text-secondary"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- submit --}}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<button type="submit" class="btn btn-primary mt-3 px-3">
|
||||
{{ __('Update Password') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- redundant script removed --}}
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="card adminuiux-card mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
{{-- page information header --}}
|
||||
<h4 class="fw-bold mb-3">Profile Information</h4>
|
||||
<p class="text-secondary small mb-4">
|
||||
update your account's profile information and email address
|
||||
</p>
|
||||
|
||||
{{-- form for resending verification email (hidden, triggered in backend) --}}
|
||||
<form id="send-verification" method="post" action="{{ route('verification.send') }}" style="display:none;">
|
||||
@csrf
|
||||
</form>
|
||||
{{-- update profile form --}}
|
||||
<form method="POST" action="{{ route('profile.update') }}" autocomplete="off" class="ajax-form">
|
||||
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
{{-- anti autofill trap --}}
|
||||
<input type="text" name="fakeuser" style="display:none">
|
||||
<input type="password" name="fakepass" style="display:none">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
{{-- Name --}}
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Name') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name', auth()->user()->name) }}"
|
||||
placeholder="ex: John Wick" required minlength="3" maxlength="100" pattern="^[a-zA-Z\s]+$"
|
||||
title="Name must be at least 3 characters and contain letters and spaces only">
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
{{ __('Email') }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="email" name="email" class="form-control"
|
||||
value="{{ old('email', auth()->user()->email) }}" placeholder="john@email.com" required
|
||||
maxlength="150" title="Enter a valid and unique email address">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3 px-3">
|
||||
{{ __('Save Changes') }}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- redundant script removed --}}
|
||||
Reference in New Issue
Block a user