initial commit
This commit is contained in:
101
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
101
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import ActionSection from '@/Components/ActionSection.vue';
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const confirmingUserDeletion = ref(false);
|
||||
const passwordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const confirmUserDeletion = () => {
|
||||
confirmingUserDeletion.value = true;
|
||||
|
||||
setTimeout(() => passwordInput.value.focus(), 250);
|
||||
};
|
||||
|
||||
const deleteUser = () => {
|
||||
form.delete(route('current-user.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeModal(),
|
||||
onError: () => passwordInput.value.focus(),
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmingUserDeletion.value = false;
|
||||
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ActionSection>
|
||||
<template #title>
|
||||
Delete Account
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Permanently delete your account.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<DangerButton @click="confirmUserDeletion">
|
||||
Delete Account
|
||||
</DangerButton>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account Confirmation Modal -->
|
||||
<DialogModal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<template #title>
|
||||
Delete Account
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
|
||||
|
||||
<div class="mt-4">
|
||||
<TextInput
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Delete Account
|
||||
</DangerButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
</ActionSection>
|
||||
</template>
|
@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import ActionSection from '@/Components/ActionSection.vue';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
defineProps({
|
||||
sessions: Array,
|
||||
});
|
||||
|
||||
const confirmingLogout = ref(false);
|
||||
const passwordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const confirmLogout = () => {
|
||||
confirmingLogout.value = true;
|
||||
|
||||
setTimeout(() => passwordInput.value.focus(), 250);
|
||||
};
|
||||
|
||||
const logoutOtherBrowserSessions = () => {
|
||||
form.delete(route('other-browser-sessions.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeModal(),
|
||||
onError: () => passwordInput.value.focus(),
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmingLogout.value = false;
|
||||
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ActionSection>
|
||||
<template #title>
|
||||
Browser Sessions
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Manage and log out your active sessions on other browsers and devices.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.
|
||||
</div>
|
||||
|
||||
<!-- Other Browser Sessions -->
|
||||
<div v-if="sessions.length > 0" class="mt-5 space-y-6">
|
||||
<div v-for="(session, i) in sessions" :key="i" class="flex items-center">
|
||||
<div>
|
||||
<svg
|
||||
v-if="session.agent.is_desktop"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
>
|
||||
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" stroke="none" /><rect
|
||||
x="7"
|
||||
y="4"
|
||||
width="10"
|
||||
height="16"
|
||||
rx="1"
|
||||
/><path d="M11 5h2M12 17v.01" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="ml-3">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ session.agent.platform ? session.agent.platform : 'Unknown' }} - {{ session.agent.browser ? session.agent.browser : 'Unknown' }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ session.ip_address }},
|
||||
|
||||
<span v-if="session.is_current_device" class="text-green-500 font-semibold">This device</span>
|
||||
<span v-else>Last active {{ session.last_active }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-5">
|
||||
<PrimaryButton @click="confirmLogout">
|
||||
Log Out Other Browser Sessions
|
||||
</PrimaryButton>
|
||||
|
||||
<ActionMessage :on="form.recentlySuccessful" class="ml-3">
|
||||
Done.
|
||||
</ActionMessage>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<DialogModal :show="confirmingLogout" @close="closeModal">
|
||||
<template #title>
|
||||
Log Out Other Browser Sessions
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.
|
||||
|
||||
<div class="mt-4">
|
||||
<TextInput
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="logoutOtherBrowserSessions"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="logoutOtherBrowserSessions"
|
||||
>
|
||||
Log Out Other Browser Sessions
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
</ActionSection>
|
||||
</template>
|
@ -0,0 +1,253 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
import { useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||
import ActionSection from '@/Components/ActionSection.vue';
|
||||
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
requiresConfirmation: Boolean,
|
||||
});
|
||||
|
||||
const enabling = ref(false);
|
||||
const confirming = ref(false);
|
||||
const disabling = ref(false);
|
||||
const qrCode = ref(null);
|
||||
const setupKey = ref(null);
|
||||
const recoveryCodes = ref([]);
|
||||
|
||||
const confirmationForm = useForm({
|
||||
code: '',
|
||||
});
|
||||
|
||||
const twoFactorEnabled = computed(
|
||||
() => ! enabling.value && usePage().props.value.user?.two_factor_enabled,
|
||||
);
|
||||
|
||||
watch(twoFactorEnabled, () => {
|
||||
if (! twoFactorEnabled.value) {
|
||||
confirmationForm.reset();
|
||||
confirmationForm.clearErrors();
|
||||
}
|
||||
});
|
||||
|
||||
const enableTwoFactorAuthentication = () => {
|
||||
enabling.value = true;
|
||||
|
||||
Inertia.post('/user/two-factor-authentication', {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => Promise.all([
|
||||
showQrCode(),
|
||||
showSetupKey(),
|
||||
showRecoveryCodes(),
|
||||
]),
|
||||
onFinish: () => {
|
||||
enabling.value = false;
|
||||
confirming.value = props.requiresConfirmation;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showQrCode = () => {
|
||||
return axios.get('/user/two-factor-qr-code').then(response => {
|
||||
qrCode.value = response.data.svg;
|
||||
});
|
||||
};
|
||||
|
||||
const showSetupKey = () => {
|
||||
return axios.get('/user/two-factor-secret-key').then(response => {
|
||||
setupKey.value = response.data.secretKey;
|
||||
});
|
||||
}
|
||||
|
||||
const showRecoveryCodes = () => {
|
||||
return axios.get('/user/two-factor-recovery-codes').then(response => {
|
||||
recoveryCodes.value = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
const confirmTwoFactorAuthentication = () => {
|
||||
confirmationForm.post('/user/confirmed-two-factor-authentication', {
|
||||
errorBag: "confirmTwoFactorAuthentication",
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => {
|
||||
confirming.value = false;
|
||||
qrCode.value = null;
|
||||
setupKey.value = null;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const regenerateRecoveryCodes = () => {
|
||||
axios
|
||||
.post('/user/two-factor-recovery-codes')
|
||||
.then(() => showRecoveryCodes());
|
||||
};
|
||||
|
||||
const disableTwoFactorAuthentication = () => {
|
||||
disabling.value = true;
|
||||
|
||||
Inertia.delete('/user/two-factor-authentication', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
disabling.value = false;
|
||||
confirming.value = false;
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ActionSection>
|
||||
<template #title>
|
||||
Two Factor Authentication
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Add additional security to your account using two factor authentication.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-medium text-gray-900">
|
||||
You have enabled two factor authentication.
|
||||
</h3>
|
||||
|
||||
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-medium text-gray-900">
|
||||
Finish enabling two factor authentication.
|
||||
</h3>
|
||||
|
||||
<h3 v-else class="text-lg font-medium text-gray-900">
|
||||
You have not enabled two factor authentication.
|
||||
</h3>
|
||||
|
||||
<div class="mt-3 max-w-xl text-sm text-gray-600">
|
||||
<p>
|
||||
When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="twoFactorEnabled">
|
||||
<div v-if="qrCode">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p v-if="confirming" class="font-semibold">
|
||||
To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application or enter the setup key and provide the generated OTP code.
|
||||
</p>
|
||||
|
||||
<p v-else>
|
||||
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" v-html="qrCode" />
|
||||
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600" v-if="setupKey">
|
||||
<p class="font-semibold">
|
||||
Setup Key: <span v-html="setupKey"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="confirming" class="mt-4">
|
||||
<InputLabel for="code" value="Code" />
|
||||
|
||||
<TextInput
|
||||
id="code"
|
||||
v-model="confirmationForm.code"
|
||||
type="text"
|
||||
name="code"
|
||||
class="block mt-1 w-1/2"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="confirmTwoFactorAuthentication"
|
||||
/>
|
||||
|
||||
<InputError :message="confirmationForm.errors.code" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="recoveryCodes.length > 0 && ! confirming">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
|
||||
<div v-for="code in recoveryCodes" :key="code">
|
||||
{{ code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div v-if="! twoFactorEnabled">
|
||||
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||
<PrimaryButton type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
|
||||
Enable
|
||||
</PrimaryButton>
|
||||
</ConfirmsPassword>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||
<PrimaryButton
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
:class="{ 'opacity-25': enabling }"
|
||||
:disabled="enabling"
|
||||
>
|
||||
Confirm
|
||||
</PrimaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
|
||||
<SecondaryButton
|
||||
v-if="recoveryCodes.length > 0 && ! confirming"
|
||||
class="mr-3"
|
||||
>
|
||||
Regenerate Recovery Codes
|
||||
</SecondaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword @confirmed="showRecoveryCodes">
|
||||
<SecondaryButton
|
||||
v-if="recoveryCodes.length === 0 && ! confirming"
|
||||
class="mr-3"
|
||||
>
|
||||
Show Recovery Codes
|
||||
</SecondaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<SecondaryButton
|
||||
v-if="confirming"
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<DangerButton
|
||||
v-if="! confirming"
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Disable
|
||||
</DangerButton>
|
||||
</ConfirmsPassword>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ActionSection>
|
||||
</template>
|
100
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
100
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const passwordInput = ref(null);
|
||||
const currentPasswordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const updatePassword = () => {
|
||||
form.put(route('user-password.update'), {
|
||||
errorBag: 'updatePassword',
|
||||
preserveScroll: true,
|
||||
onSuccess: () => form.reset(),
|
||||
onError: () => {
|
||||
if (form.errors.password) {
|
||||
form.reset('password', 'password_confirmation');
|
||||
passwordInput.value.focus();
|
||||
}
|
||||
|
||||
if (form.errors.current_password) {
|
||||
form.reset('current_password');
|
||||
currentPasswordInput.value.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection @submitted="updatePassword">
|
||||
<template #title>
|
||||
Update Password
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Ensure your account is using a long, random password to stay secure.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="current_password" value="Current Password" />
|
||||
<TextInput
|
||||
id="current_password"
|
||||
ref="currentPasswordInput"
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<InputError :message="form.errors.current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="password" value="New Password" />
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="password_confirmation" value="Confirm Password" />
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<InputError :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</ActionMessage>
|
||||
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
</FormSection>
|
||||
</template>
|
@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
import { Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
user: Object,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
_method: 'PUT',
|
||||
name: props.user.name,
|
||||
email: props.user.email,
|
||||
photo: null,
|
||||
});
|
||||
|
||||
const verificationLinkSent = ref(null);
|
||||
const photoPreview = ref(null);
|
||||
const photoInput = ref(null);
|
||||
|
||||
const updateProfileInformation = () => {
|
||||
if (photoInput.value) {
|
||||
form.photo = photoInput.value.files[0];
|
||||
}
|
||||
|
||||
form.post(route('user-profile-information.update'), {
|
||||
errorBag: 'updateProfileInformation',
|
||||
preserveScroll: true,
|
||||
onSuccess: () => clearPhotoFileInput(),
|
||||
});
|
||||
};
|
||||
|
||||
const sendEmailVerification = () => {
|
||||
verificationLinkSent.value = true;
|
||||
};
|
||||
|
||||
const selectNewPhoto = () => {
|
||||
photoInput.value.click();
|
||||
};
|
||||
|
||||
const updatePhotoPreview = () => {
|
||||
const photo = photoInput.value.files[0];
|
||||
|
||||
if (! photo) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
photoPreview.value = e.target.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(photo);
|
||||
};
|
||||
|
||||
const deletePhoto = () => {
|
||||
Inertia.delete(route('current-user-photo.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
photoPreview.value = null;
|
||||
clearPhotoFileInput();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearPhotoFileInput = () => {
|
||||
if (photoInput.value?.value) {
|
||||
photoInput.value.value = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection @submitted="updateProfileInformation">
|
||||
<template #title>
|
||||
Profile Information
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Update your account's profile information and email address.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<!-- Profile Photo -->
|
||||
<div v-if="$page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4">
|
||||
<!-- Profile Photo File Input -->
|
||||
<input
|
||||
ref="photoInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
@change="updatePhotoPreview"
|
||||
>
|
||||
|
||||
<InputLabel for="photo" value="Photo" />
|
||||
|
||||
<!-- Current Profile Photo -->
|
||||
<div v-show="! photoPreview" class="mt-2">
|
||||
<img :src="user.profile_photo_url" :alt="user.name" class="rounded-full h-20 w-20 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Profile Photo Preview -->
|
||||
<div v-show="photoPreview" class="mt-2">
|
||||
<span
|
||||
class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
:style="'background-image: url(\'' + photoPreview + '\');'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SecondaryButton class="mt-2 mr-2" type="button" @click.prevent="selectNewPhoto">
|
||||
Select A New Photo
|
||||
</SecondaryButton>
|
||||
|
||||
<SecondaryButton
|
||||
v-if="user.profile_photo_path"
|
||||
type="button"
|
||||
class="mt-2"
|
||||
@click.prevent="deletePhoto"
|
||||
>
|
||||
Remove Photo
|
||||
</SecondaryButton>
|
||||
|
||||
<InputError :message="form.errors.photo" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="name"
|
||||
/>
|
||||
<InputError :message="form.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
/>
|
||||
<InputError :message="form.errors.email" class="mt-2" />
|
||||
|
||||
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null">
|
||||
<p class="text-sm mt-2">
|
||||
Your email address is unverified.
|
||||
|
||||
<Link
|
||||
:href="route('verification.send')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="underline text-gray-600 hover:text-gray-900"
|
||||
@click.prevent="sendEmailVerification"
|
||||
>
|
||||
Click here to re-send the verification email.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600">
|
||||
A new verification link has been sent to your email address.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</ActionMessage>
|
||||
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
</FormSection>
|
||||
</template>
|
57
resources/js/Pages/Profile/Show.vue
Normal file
57
resources/js/Pages/Profile/Show.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import DeleteUserForm from '@/Pages/Profile/Partials/DeleteUserForm.vue';
|
||||
import LogoutOtherBrowserSessionsForm from '@/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue';
|
||||
import SectionBorder from '@/Components/SectionBorder.vue';
|
||||
import TwoFactorAuthenticationForm from '@/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue';
|
||||
import UpdatePasswordForm from '@/Pages/Profile/Partials/UpdatePasswordForm.vue';
|
||||
import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfileInformationForm.vue';
|
||||
|
||||
defineProps({
|
||||
confirmsTwoFactorAuthentication: Boolean,
|
||||
sessions: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Profile">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Profile
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
<div v-if="$page.props.jetstream.canUpdateProfileInformation">
|
||||
<UpdateProfileInformationForm :user="$page.props.user" />
|
||||
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canUpdatePassword">
|
||||
<UpdatePasswordForm class="mt-10 sm:mt-0" />
|
||||
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
|
||||
<TwoFactorAuthenticationForm
|
||||
:requires-confirmation="confirmsTwoFactorAuthentication"
|
||||
class="mt-10 sm:mt-0"
|
||||
/>
|
||||
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
|
||||
|
||||
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
|
||||
<SectionBorder />
|
||||
|
||||
<DeleteUserForm class="mt-10 sm:mt-0" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
Reference in New Issue
Block a user