initial commit
This commit is contained in:
30
resources/js/Pages/API/Index.vue
Normal file
30
resources/js/Pages/API/Index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import ApiTokenManager from '@/Pages/API/Partials/ApiTokenManager.vue';
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
|
||||
defineProps({
|
||||
tokens: Array,
|
||||
availablePermissions: Array,
|
||||
defaultPermissions: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="API Tokens">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
API Tokens
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
<ApiTokenManager
|
||||
:tokens="tokens"
|
||||
:available-permissions="availablePermissions"
|
||||
:default-permissions="defaultPermissions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
255
resources/js/Pages/API/Partials/ApiTokenManager.vue
Normal file
255
resources/js/Pages/API/Partials/ApiTokenManager.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue';
|
||||
import JetActionSection from '@/Jetstream/ActionSection.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetConfirmationModal from '@/Jetstream/ConfirmationModal.vue';
|
||||
import JetDangerButton from '@/Jetstream/DangerButton.vue';
|
||||
import JetDialogModal from '@/Jetstream/DialogModal.vue';
|
||||
import JetFormSection from '@/Jetstream/FormSection.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetCheckbox from '@/Jetstream/Checkbox.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue';
|
||||
import JetSectionBorder from '@/Jetstream/SectionBorder.vue';
|
||||
|
||||
const props = defineProps({
|
||||
tokens: Array,
|
||||
availablePermissions: Array,
|
||||
defaultPermissions: Array,
|
||||
});
|
||||
|
||||
const createApiTokenForm = useForm({
|
||||
name: '',
|
||||
permissions: props.defaultPermissions,
|
||||
});
|
||||
|
||||
const updateApiTokenForm = useForm({
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
const deleteApiTokenForm = useForm();
|
||||
|
||||
const displayingToken = ref(false);
|
||||
const managingPermissionsFor = ref(null);
|
||||
const apiTokenBeingDeleted = ref(null);
|
||||
|
||||
const createApiToken = () => {
|
||||
createApiTokenForm.post(route('api-tokens.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
displayingToken.value = true;
|
||||
createApiTokenForm.reset();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const manageApiTokenPermissions = (token) => {
|
||||
updateApiTokenForm.permissions = token.abilities;
|
||||
managingPermissionsFor.value = token;
|
||||
};
|
||||
|
||||
const updateApiToken = () => {
|
||||
updateApiTokenForm.put(route('api-tokens.update', managingPermissionsFor.value), {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (managingPermissionsFor.value = null),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmApiTokenDeletion = (token) => {
|
||||
apiTokenBeingDeleted.value = token;
|
||||
};
|
||||
|
||||
const deleteApiToken = () => {
|
||||
deleteApiTokenForm.delete(route('api-tokens.destroy', apiTokenBeingDeleted.value), {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (apiTokenBeingDeleted.value = null),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Generate API Token -->
|
||||
<JetFormSection @submitted="createApiToken">
|
||||
<template #title>
|
||||
Create API Token
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
API tokens allow third-party services to authenticate with our application on your behalf.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<!-- Token Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<JetLabel for="name" value="Name" />
|
||||
<JetInput
|
||||
id="name"
|
||||
v-model="createApiTokenForm.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autofocus
|
||||
/>
|
||||
<JetInputError :message="createApiTokenForm.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Token Permissions -->
|
||||
<div v-if="availablePermissions.length > 0" class="col-span-6">
|
||||
<JetLabel for="permissions" value="Permissions" />
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="permission in availablePermissions" :key="permission">
|
||||
<label class="flex items-center">
|
||||
<JetCheckbox v-model:checked="createApiTokenForm.permissions" :value="permission" />
|
||||
<span class="ml-2 text-sm text-gray-600">{{ permission }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<JetActionMessage :on="createApiTokenForm.recentlySuccessful" class="mr-3">
|
||||
Created.
|
||||
</JetActionMessage>
|
||||
|
||||
<JetButton :class="{ 'opacity-25': createApiTokenForm.processing }" :disabled="createApiTokenForm.processing">
|
||||
Create
|
||||
</JetButton>
|
||||
</template>
|
||||
</JetFormSection>
|
||||
|
||||
<div v-if="tokens.length > 0">
|
||||
<JetSectionBorder />
|
||||
|
||||
<!-- Manage API Tokens -->
|
||||
<div class="mt-10 sm:mt-0">
|
||||
<JetActionSection>
|
||||
<template #title>
|
||||
Manage API Tokens
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
You may delete any of your existing tokens if they are no longer needed.
|
||||
</template>
|
||||
|
||||
<!-- API Token List -->
|
||||
<template #content>
|
||||
<div class="space-y-6">
|
||||
<div v-for="token in tokens" :key="token.id" class="flex items-center justify-between">
|
||||
<div>
|
||||
{{ token.name }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div v-if="token.last_used_ago" class="text-sm text-gray-400">
|
||||
Last used {{ token.last_used_ago }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="availablePermissions.length > 0"
|
||||
class="cursor-pointer ml-6 text-sm text-gray-400 underline"
|
||||
@click="manageApiTokenPermissions(token)"
|
||||
>
|
||||
Permissions
|
||||
</button>
|
||||
|
||||
<button class="cursor-pointer ml-6 text-sm text-red-500" @click="confirmApiTokenDeletion(token)">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</JetActionSection>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Value Modal -->
|
||||
<JetDialogModal :show="displayingToken" @close="displayingToken = false">
|
||||
<template #title>
|
||||
API Token
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div>
|
||||
Please copy your new API token. For your security, it won't be shown again.
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.flash.token" class="mt-4 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500">
|
||||
{{ $page.props.jetstream.flash.token }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<JetSecondaryButton @click="displayingToken = false">
|
||||
Close
|
||||
</JetSecondaryButton>
|
||||
</template>
|
||||
</JetDialogModal>
|
||||
|
||||
<!-- API Token Permissions Modal -->
|
||||
<JetDialogModal :show="managingPermissionsFor != null" @close="managingPermissionsFor = null">
|
||||
<template #title>
|
||||
API Token Permissions
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="permission in availablePermissions" :key="permission">
|
||||
<label class="flex items-center">
|
||||
<JetCheckbox v-model:checked="updateApiTokenForm.permissions" :value="permission" />
|
||||
<span class="ml-2 text-sm text-gray-600">{{ permission }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<JetSecondaryButton @click="managingPermissionsFor = null">
|
||||
Cancel
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': updateApiTokenForm.processing }"
|
||||
:disabled="updateApiTokenForm.processing"
|
||||
@click="updateApiToken"
|
||||
>
|
||||
Save
|
||||
</JetButton>
|
||||
</template>
|
||||
</JetDialogModal>
|
||||
|
||||
<!-- Delete Token Confirmation Modal -->
|
||||
<JetConfirmationModal :show="apiTokenBeingDeleted != null" @close="apiTokenBeingDeleted = null">
|
||||
<template #title>
|
||||
Delete API Token
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to delete this API token?
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<JetSecondaryButton @click="apiTokenBeingDeleted = null">
|
||||
Cancel
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetDangerButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': deleteApiTokenForm.processing }"
|
||||
:disabled="deleteApiTokenForm.processing"
|
||||
@click="deleteApiToken"
|
||||
>
|
||||
Delete
|
||||
</JetDangerButton>
|
||||
</template>
|
||||
</JetConfirmationModal>
|
||||
</div>
|
||||
</template>
|
||||
64
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
64
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Head, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const passwordInput = ref(null);
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.confirm'), {
|
||||
onFinish: () => {
|
||||
form.reset();
|
||||
|
||||
passwordInput.value.focus();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Secure Area" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
This is a secure area of the application. Please confirm your password before continuing.
|
||||
</div>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<JetLabel for="password" value="Password" />
|
||||
<JetInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<JetButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Confirm
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
61
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
61
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { Head, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
defineProps({
|
||||
status: String,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.email'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Forgot Password" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.
|
||||
</div>
|
||||
|
||||
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<JetLabel for="email" value="Email" />
|
||||
<JetInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<JetButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Email Password Reset Link
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
89
resources/js/Pages/Auth/Login.vue
Normal file
89
resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetCheckbox from '@/Jetstream/Checkbox.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
defineProps({
|
||||
canResetPassword: Boolean,
|
||||
status: String,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.transform(data => ({
|
||||
...data,
|
||||
remember: form.remember ? 'on' : '',
|
||||
})).post(route('login'), {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Log in" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<JetLabel for="email" value="Email" />
|
||||
<JetInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="password" value="Password" />
|
||||
<JetInput
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="block mt-4">
|
||||
<label class="flex items-center">
|
||||
<JetCheckbox v-model:checked="form.remember" name="remember" />
|
||||
<span class="ml-2 text-sm text-gray-600">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 hover:text-gray-900">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
|
||||
<JetButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Log in
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
108
resources/js/Pages/Auth/Register.vue
Normal file
108
resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetCheckbox from '@/Jetstream/Checkbox.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
terms: false,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('register'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Register" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<JetLabel for="name" value="Name" />
|
||||
<JetInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="email" value="Email" />
|
||||
<JetInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="password" value="Password" />
|
||||
<JetInput
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="password_confirmation" value="Confirm Password" />
|
||||
<JetInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.hasTermsAndPrivacyPolicyFeature" class="mt-4">
|
||||
<JetLabel for="terms">
|
||||
<div class="flex items-center">
|
||||
<JetCheckbox id="terms" v-model:checked="form.terms" name="terms" />
|
||||
|
||||
<div class="ml-2">
|
||||
I agree to the <a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Terms of Service</a> and <a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
</JetLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<Link :href="route('login')" class="underline text-sm text-gray-600 hover:text-gray-900">
|
||||
Already registered?
|
||||
</Link>
|
||||
|
||||
<JetButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Register
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
83
resources/js/Pages/Auth/ResetPassword.vue
Normal file
83
resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import { Head, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
const props = defineProps({
|
||||
email: String,
|
||||
token: String,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.update'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Reset Password" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<JetLabel for="email" value="Email" />
|
||||
<JetInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="password" value="Password" />
|
||||
<JetInput
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<JetLabel for="password_confirmation" value="Confirm Password" />
|
||||
<JetInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<JetButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Reset Password
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
104
resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
104
resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { Head, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue';
|
||||
|
||||
const recovery = ref(false);
|
||||
|
||||
const form = useForm({
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
});
|
||||
|
||||
const recoveryCodeInput = ref(null);
|
||||
const codeInput = ref(null);
|
||||
|
||||
const toggleRecovery = async () => {
|
||||
recovery.value ^= true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (recovery.value) {
|
||||
recoveryCodeInput.value.focus();
|
||||
form.code = '';
|
||||
} else {
|
||||
codeInput.value.focus();
|
||||
form.recovery_code = '';
|
||||
}
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('two-factor.login'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Two-factor Confirmation" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
<template v-if="! recovery">
|
||||
Please confirm access to your account by entering the authentication code provided by your authenticator application.
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Please confirm access to your account by entering one of your emergency recovery codes.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<JetValidationErrors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div v-if="! recovery">
|
||||
<JetLabel for="code" value="Code" />
|
||||
<JetInput
|
||||
id="code"
|
||||
ref="codeInput"
|
||||
v-model="form.code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="mt-1 block w-full"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<JetLabel for="recovery_code" value="Recovery Code" />
|
||||
<JetInput
|
||||
id="recovery_code"
|
||||
ref="recoveryCodeInput"
|
||||
v-model="form.recovery_code"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<button type="button" class="text-sm text-gray-600 hover:text-gray-900 underline cursor-pointer" @click.prevent="toggleRecovery">
|
||||
<template v-if="! recovery">
|
||||
Use a recovery code
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Use an authentication code
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<JetButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Log in
|
||||
</JetButton>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
62
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
62
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
status: String,
|
||||
});
|
||||
|
||||
const form = useForm();
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('verification.send'));
|
||||
};
|
||||
|
||||
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Email Verification" />
|
||||
|
||||
<JetAuthenticationCard>
|
||||
<template #logo>
|
||||
<JetAuthenticationCardLogo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.
|
||||
</div>
|
||||
|
||||
<div v-if="verificationLinkSent" class="mb-4 font-medium text-sm text-green-600">
|
||||
A new verification link has been sent to the email address you provided in your profile settings.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<JetButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Resend Verification Email
|
||||
</JetButton>
|
||||
|
||||
<div>
|
||||
<Link
|
||||
:href="route('profile.show')"
|
||||
class="underline text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Edit Profile</Link>
|
||||
|
||||
<Link
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="underline text-sm text-gray-600 hover:text-gray-900 ml-2"
|
||||
>
|
||||
Log Out
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</JetAuthenticationCard>
|
||||
</template>
|
||||
22
resources/js/Pages/Dashboard.vue
Normal file
22
resources/js/Pages/Dashboard.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import Welcome from '@/Jetstream/Welcome.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Dashboard">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Dashboard
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<Welcome />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
24
resources/js/Pages/PrivacyPolicy.vue
Normal file
24
resources/js/Pages/PrivacyPolicy.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
|
||||
defineProps({
|
||||
policy: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Privacy Policy" />
|
||||
|
||||
<div class="font-sans text-gray-900 antialiased">
|
||||
<div class="pt-4 bg-gray-100">
|
||||
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
|
||||
<div>
|
||||
<JetAuthenticationCardLogo />
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose" v-html="policy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
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 JetActionSection from '@/Jetstream/ActionSection.vue';
|
||||
import JetDialogModal from '@/Jetstream/DialogModal.vue';
|
||||
import JetDangerButton from '@/Jetstream/DangerButton.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.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>
|
||||
<JetActionSection>
|
||||
<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">
|
||||
<JetDangerButton @click="confirmUserDeletion">
|
||||
Delete Account
|
||||
</JetDangerButton>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account Confirmation Modal -->
|
||||
<JetDialogModal :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">
|
||||
<JetInput
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
|
||||
<JetInputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<JetSecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetDangerButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Delete Account
|
||||
</JetDangerButton>
|
||||
</template>
|
||||
</JetDialogModal>
|
||||
</template>
|
||||
</JetActionSection>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue';
|
||||
import JetActionSection from '@/Jetstream/ActionSection.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetDialogModal from '@/Jetstream/DialogModal.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.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>
|
||||
<JetActionSection>
|
||||
<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">
|
||||
<JetButton @click="confirmLogout">
|
||||
Log Out Other Browser Sessions
|
||||
</JetButton>
|
||||
|
||||
<JetActionMessage :on="form.recentlySuccessful" class="ml-3">
|
||||
Done.
|
||||
</JetActionMessage>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<JetDialogModal :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">
|
||||
<JetInput
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="logoutOtherBrowserSessions"
|
||||
/>
|
||||
|
||||
<JetInputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<JetSecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="logoutOtherBrowserSessions"
|
||||
>
|
||||
Log Out Other Browser Sessions
|
||||
</JetButton>
|
||||
</template>
|
||||
</JetDialogModal>
|
||||
</template>
|
||||
</JetActionSection>
|
||||
</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 JetActionSection from '@/Jetstream/ActionSection.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetConfirmsPassword from '@/Jetstream/ConfirmsPassword.vue';
|
||||
import JetDangerButton from '@/Jetstream/DangerButton.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.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>
|
||||
<JetActionSection>
|
||||
<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">
|
||||
<JetLabel for="code" value="Code" />
|
||||
|
||||
<JetInput
|
||||
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"
|
||||
/>
|
||||
|
||||
<JetInputError :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">
|
||||
<JetConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||
<JetButton type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
|
||||
Enable
|
||||
</JetButton>
|
||||
</JetConfirmsPassword>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<JetConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||
<JetButton
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
:class="{ 'opacity-25': enabling }"
|
||||
:disabled="enabling"
|
||||
>
|
||||
Confirm
|
||||
</JetButton>
|
||||
</JetConfirmsPassword>
|
||||
|
||||
<JetConfirmsPassword @confirmed="regenerateRecoveryCodes">
|
||||
<JetSecondaryButton
|
||||
v-if="recoveryCodes.length > 0 && ! confirming"
|
||||
class="mr-3"
|
||||
>
|
||||
Regenerate Recovery Codes
|
||||
</JetSecondaryButton>
|
||||
</JetConfirmsPassword>
|
||||
|
||||
<JetConfirmsPassword @confirmed="showRecoveryCodes">
|
||||
<JetSecondaryButton
|
||||
v-if="recoveryCodes.length === 0 && ! confirming"
|
||||
class="mr-3"
|
||||
>
|
||||
Show Recovery Codes
|
||||
</JetSecondaryButton>
|
||||
</JetConfirmsPassword>
|
||||
|
||||
<JetConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<JetSecondaryButton
|
||||
v-if="confirming"
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Cancel
|
||||
</JetSecondaryButton>
|
||||
</JetConfirmsPassword>
|
||||
|
||||
<JetConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<JetDangerButton
|
||||
v-if="! confirming"
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Disable
|
||||
</JetDangerButton>
|
||||
</JetConfirmsPassword>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</JetActionSection>
|
||||
</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 JetActionMessage from '@/Jetstream/ActionMessage.vue';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetFormSection from '@/Jetstream/FormSection.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetLabel from '@/Jetstream/Label.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>
|
||||
<JetFormSection @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">
|
||||
<JetLabel for="current_password" value="Current Password" />
|
||||
<JetInput
|
||||
id="current_password"
|
||||
ref="currentPasswordInput"
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<JetInputError :message="form.errors.current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<JetLabel for="password" value="New Password" />
|
||||
<JetInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<JetInputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<JetLabel for="password_confirmation" value="Confirm Password" />
|
||||
<JetInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<JetInputError :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<JetActionMessage :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</JetActionMessage>
|
||||
|
||||
<JetButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</JetButton>
|
||||
</template>
|
||||
</JetFormSection>
|
||||
</template>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
import { Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import JetButton from '@/Jetstream/Button.vue';
|
||||
import JetFormSection from '@/Jetstream/FormSection.vue';
|
||||
import JetInput from '@/Jetstream/Input.vue';
|
||||
import JetInputError from '@/Jetstream/InputError.vue';
|
||||
import JetLabel from '@/Jetstream/Label.vue';
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue';
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.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>
|
||||
<JetFormSection @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"
|
||||
>
|
||||
|
||||
<JetLabel 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>
|
||||
|
||||
<JetSecondaryButton class="mt-2 mr-2" type="button" @click.prevent="selectNewPhoto">
|
||||
Select A New Photo
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetSecondaryButton
|
||||
v-if="user.profile_photo_path"
|
||||
type="button"
|
||||
class="mt-2"
|
||||
@click.prevent="deletePhoto"
|
||||
>
|
||||
Remove Photo
|
||||
</JetSecondaryButton>
|
||||
|
||||
<JetInputError :message="form.errors.photo" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<JetLabel for="name" value="Name" />
|
||||
<JetInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="name"
|
||||
/>
|
||||
<JetInputError :message="form.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<JetLabel for="email" value="Email" />
|
||||
<JetInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
/>
|
||||
<JetInputError :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>
|
||||
<JetActionMessage :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</JetActionMessage>
|
||||
|
||||
<JetButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</JetButton>
|
||||
</template>
|
||||
</JetFormSection>
|
||||
</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 JetSectionBorder from '@/Jetstream/SectionBorder.vue';
|
||||
import LogoutOtherBrowserSessionsForm from '@/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.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" />
|
||||
|
||||
<JetSectionBorder />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canUpdatePassword">
|
||||
<UpdatePasswordForm class="mt-10 sm:mt-0" />
|
||||
|
||||
<JetSectionBorder />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
|
||||
<TwoFactorAuthenticationForm
|
||||
:requires-confirmation="confirmsTwoFactorAuthentication"
|
||||
class="mt-10 sm:mt-0"
|
||||
/>
|
||||
|
||||
<JetSectionBorder />
|
||||
</div>
|
||||
|
||||
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
|
||||
|
||||
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
|
||||
<JetSectionBorder />
|
||||
|
||||
<DeleteUserForm class="mt-10 sm:mt-0" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
24
resources/js/Pages/TermsOfService.vue
Normal file
24
resources/js/Pages/TermsOfService.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue';
|
||||
|
||||
defineProps({
|
||||
terms: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Terms of Service" />
|
||||
|
||||
<div class="font-sans text-gray-900 antialiased">
|
||||
<div class="pt-4 bg-gray-100">
|
||||
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
|
||||
<div>
|
||||
<JetAuthenticationCardLogo />
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose" v-html="terms" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
249
resources/js/Pages/Welcome.vue
Normal file
249
resources/js/Pages/Welcome.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup>
|
||||
import { Head, Link } from '@inertiajs/inertia-vue3';
|
||||
|
||||
defineProps({
|
||||
canLogin: Boolean,
|
||||
canRegister: Boolean,
|
||||
laravelVersion: String,
|
||||
phpVersion: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Welcome" />
|
||||
|
||||
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0">
|
||||
<div v-if="canLogin" class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
|
||||
<Link v-if="$page.props.user" :href="route('dashboard')" class="text-sm text-gray-700 underline">
|
||||
Dashboard
|
||||
</Link>
|
||||
|
||||
<template v-else>
|
||||
<Link :href="route('login')" class="text-sm text-gray-700 underline">
|
||||
Log in
|
||||
</Link>
|
||||
|
||||
<Link v-if="canRegister" :href="route('register')" class="ml-4 text-sm text-gray-700 underline">
|
||||
Register
|
||||
</Link>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex justify-center pt-8 sm:justify-start sm:pt-0">
|
||||
<svg
|
||||
viewBox="0 0 651 192"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-auto text-gray-700 sm:h-20"
|
||||
>
|
||||
<g clip-path="url(#clip0)" fill="#EF3B2D">
|
||||
<path d="M248.032 44.676h-16.466v100.23h47.394v-14.748h-30.928V44.676zM337.091 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.431 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162-.001 2.863-.479 5.584-1.432 8.161zM463.954 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.432 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162 0 2.863-.479 5.584-1.432 8.161zM650.772 44.676h-15.606v100.23h15.606V44.676zM365.013 144.906h15.607V93.538h26.776V78.182h-42.383v66.724zM542.133 78.182l-19.616 51.096-19.616-51.096h-15.808l25.617 66.724h19.614l25.617-66.724h-15.808zM591.98 76.466c-19.112 0-34.239 15.706-34.239 35.079 0 21.416 14.641 35.079 36.239 35.079 12.088 0 19.806-4.622 29.234-14.688l-10.544-8.158c-.006.008-7.958 10.449-19.832 10.449-13.802 0-19.612-11.127-19.612-16.884h51.777c2.72-22.043-11.772-40.877-33.023-40.877zm-18.713 29.28c.12-1.284 1.917-16.884 18.589-16.884 16.671 0 18.697 15.598 18.813 16.884h-37.402zM184.068 43.892c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002-35.648-20.524a2.971 2.971 0 00-2.964 0l-35.647 20.522-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v38.979l-29.706 17.103V24.493a3 3 0 00-.103-.776c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002L40.098 1.396a2.971 2.971 0 00-2.964 0L1.487 21.919l-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v122.09c0 1.063.568 2.044 1.489 2.575l71.293 41.045c.156.089.324.143.49.202.078.028.15.074.23.095a2.98 2.98 0 001.524 0c.069-.018.132-.059.2-.083.176-.061.354-.119.519-.214l71.293-41.045a2.971 2.971 0 001.489-2.575v-38.979l34.158-19.666a2.971 2.971 0 001.489-2.575V44.666a3.075 3.075 0 00-.106-.774zM74.255 143.167l-29.648-16.779 31.136-17.926.001-.001 34.164-19.669 29.674 17.084-21.772 12.428-43.555 24.863zm68.329-76.259v33.841l-12.475-7.182-17.231-9.92V49.806l12.475 7.182 17.231 9.92zm2.97-39.335l29.693 17.095-29.693 17.095-29.693-17.095 29.693-17.095zM54.06 114.089l-12.475 7.182V46.733l17.231-9.92 12.475-7.182v74.537l-17.231 9.921zM38.614 7.398l29.693 17.095-29.693 17.095L8.921 24.493 38.614 7.398zM5.938 29.632l12.475 7.182 17.231 9.92v79.676l.001.005-.001.006c0 .114.032.221.045.333.017.146.021.294.059.434l.002.007c.032.117.094.222.14.334.051.124.088.255.156.371a.036.036 0 00.004.009c.061.105.149.191.222.288.081.105.149.22.244.314l.008.01c.084.083.19.142.284.215.106.083.202.178.32.247l.013.005.011.008 34.139 19.321v34.175L5.939 144.867V29.632h-.001zm136.646 115.235l-65.352 37.625V148.31l48.399-27.628 16.953-9.677v33.862zm35.646-61.22l-29.706 17.102V66.908l17.231-9.92 12.475-7.182v33.841z" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-white dark:bg-gray-800 overflow-hidden shadow sm:rounded-lg">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
|
||||
<div class="ml-4 text-lg leading-7 font-semibold">
|
||||
<a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-12">
|
||||
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
|
||||
Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-t-0 md:border-l">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
><path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
<div class="ml-4 text-lg leading-7 font-semibold">
|
||||
<a href="https://laracasts.com" class="underline text-gray-900 dark:text-white">Laracasts</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-12">
|
||||
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
|
||||
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
><path d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" /></svg>
|
||||
<div class="ml-4 text-lg leading-7 font-semibold">
|
||||
<a href="https://laravel-news.com/" class="underline text-gray-900 dark:text-white">Laravel News</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-12">
|
||||
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
|
||||
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-l">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-8 h-8 text-gray-500"
|
||||
><path d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<div class="ml-4 text-lg leading-7 font-semibold text-gray-900 dark:text-white">
|
||||
Vibrant Ecosystem
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-12">
|
||||
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
|
||||
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline">Forge</a>, <a href="https://vapor.laravel.com" class="underline">Vapor</a>, <a href="https://nova.laravel.com" class="underline">Nova</a>, and <a href="https://envoyer.io" class="underline">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="underline">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline">Telescope</a>, and more.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-4 sm:items-center sm:justify-between">
|
||||
<div class="text-center text-sm text-gray-500 sm:text-left">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="-mt-px w-5 h-5 text-gray-400"
|
||||
>
|
||||
<path d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
|
||||
<a href="https://laravel.bigcartel.com" class="ml-1 underline">
|
||||
Shop
|
||||
</a>
|
||||
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
class="ml-4 -mt-px w-5 h-5 text-gray-400"
|
||||
>
|
||||
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
|
||||
<a href="https://github.com/sponsors/taylorotwell" class="ml-1 underline">
|
||||
Sponsor
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 text-center text-sm text-gray-500 sm:text-right sm:ml-0">
|
||||
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bg-gray-100 {
|
||||
background-color: #f7fafc;
|
||||
background-color: rgba(247, 250, 252, var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: #edf2f7;
|
||||
border-color: rgba(237, 242, 247, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #cbd5e0;
|
||||
color: rgba(203, 213, 224, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #a0aec0;
|
||||
color: rgba(160, 174, 192, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #718096;
|
||||
color: rgba(113, 128, 150, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #4a5568;
|
||||
color: rgba(74, 85, 104, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
color: #1a202c;
|
||||
color: rgba(26, 32, 44, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-gray-800 {
|
||||
background-color: #2d3748;
|
||||
background-color: rgba(45, 55, 72, var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900 {
|
||||
background-color: #1a202c;
|
||||
background-color: rgba(26, 32, 44, var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-700 {
|
||||
border-color: #4a5568;
|
||||
border-color: rgba(74, 85, 104, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-400 {
|
||||
color: #cbd5e0;
|
||||
color: rgba(203, 213, 224, var(--tw-text-opacity));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user