You've already forked php-auth
generated from siteworxpro/Php-Template
Basics of auth
This commit is contained in:
56
front-end/src/pages/error.vue
Normal file
56
front-end/src/pages/error.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center md:p-10">
|
||||
<Card class="p-10 w-full md:w-2/3 lg:w-1/2 xl:w-5/12">
|
||||
<template #header>
|
||||
<h1 class="text-3xl font-bold text-center mb-4">
|
||||
Oh No, An Error!
|
||||
</h1>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-6xl text-yellow-500 mb-8"></i>
|
||||
<p class="mb-8">Error: {{ getErrorMessage() }}</p>
|
||||
<p>
|
||||
Please check your configuration or contact support if the issue persists.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<p class="text-xs text-center text-gray-500 mt-10">
|
||||
Authentication Portal ::
|
||||
<i class="text-xs fa-sharp fa-thin fa-copyright"></i> {{ date() }} :: {{ version() }}
|
||||
</p>
|
||||
<p class="text-center text-gray-500 mt-10">
|
||||
<i class="fa-regular text-2xl fa-shield-keyhole"></i>
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {defineAsyncComponent} from "vue";
|
||||
|
||||
const Card = defineAsyncComponent(() => import("primevue/card"));
|
||||
|
||||
function date() {
|
||||
return new Date().getFullYear()
|
||||
}
|
||||
|
||||
function version (): string {
|
||||
return import.meta.env.VITE_VERSION || 'dev-master'
|
||||
}
|
||||
|
||||
function getErrorMessage(): string {
|
||||
const status = new URLSearchParams(window.location.search).get('e') || '';
|
||||
|
||||
switch (status) {
|
||||
case 'invalid_client':
|
||||
return 'Unknown client. Please check the client ID and try again.';
|
||||
case 'unsupported_grant_type':
|
||||
return 'The authentication method is not supported. Please contact support.';
|
||||
default:
|
||||
return 'Sorry, something went wrong on our end. Please try again later.';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
290
front-end/src/pages/login.vue
Normal file
290
front-end/src/pages/login.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center md:p-10">
|
||||
<Card class="p-10 w-full md:w-2/3 lg:w-1/2 xl:w-5/12">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-2xl mt-5">
|
||||
<span v-if="capabilities.client_name !== ''">{{ capabilities.client_name }}</span>
|
||||
<span v-else>Login Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="capabilities.userPass" @keydown.enter="login">
|
||||
<div>
|
||||
<InputText
|
||||
v-model="form.email"
|
||||
:invalid="v$.$dirty && v$.form.email.$invalid"
|
||||
class="w-full"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<label v-if="v$.$dirty && v$.form.email.required.$invalid" class="text-xs text-red-600">Please enter an
|
||||
email address</label>
|
||||
<label v-if="v$.$dirty && v$.form.email.email.$invalid" class="text-xs text-red-600">Email address is
|
||||
invalid</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!magicLink" class="mt-5">
|
||||
<InputText
|
||||
v-model="form.password"
|
||||
:invalid="v$.$dirty && v$.form.password.$invalid"
|
||||
class="w-full"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
<label v-if="v$.$dirty && v$.form.password.required.$invalid" class="text-xs text-red-600">Please enter a
|
||||
password</label>
|
||||
</div>
|
||||
<div v-if="capabilities.magicLogin" class="mt-5">
|
||||
<div class="flex items-center">
|
||||
<Checkbox id="magic-link" v-model="magicLink" binary class="mr-3" />
|
||||
<label for="magic-link">
|
||||
Use Magic Login (Password-less)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!magicLink && capabilities.userPass">
|
||||
<Button
|
||||
class="w-full mt-5"
|
||||
:loading="loading"
|
||||
raised
|
||||
@click="login"
|
||||
icon="fa-regular fa-sharp fa-right-to-bracket"
|
||||
label="Login"
|
||||
/>
|
||||
|
||||
<p class="text-center mt-5">
|
||||
<router-link
|
||||
to="/password-reset"
|
||||
class="p-button p-button-raised p-button-secondary w-full"
|
||||
>
|
||||
<i class="fa-light fa-sharp fa-lock"></i>
|
||||
Reset Password
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="magicLink">
|
||||
<Button
|
||||
class="w-full mt-5"
|
||||
:loading="loading"
|
||||
raised
|
||||
label="Send Magic Link"
|
||||
@click="sendMagicLink"
|
||||
icon="fa-light fa-wand-magic-sparkles"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="capabilities.socials && Object.keys(capabilities.socials).length > 0" class="mt-5">
|
||||
<div class="text-center mt-5 mb-5">
|
||||
<div class="mb-5 w-1/4 ml-auto mr-auto" style="border-bottom: 1px solid rgba(156,134,134,0.27)" />
|
||||
Social Logins
|
||||
</div>
|
||||
<div class="flex justify-around mt-5">
|
||||
<Button style="display: none" />
|
||||
<a v-if="capabilities.socials.google" :href="capabilities.socials.google.redirectUrl" class="p-button"
|
||||
style="background-color: #de5246">
|
||||
<i class="fa-brands fa-google mr-2"></i> Google
|
||||
</a>
|
||||
<a v-if="capabilities.socials.linkedIn" :href="capabilities.socials.linkedIn.redirectUrl" class="p-button"
|
||||
style="background-color: #55ACEE">
|
||||
<i class="fa-brands fa-linkedin mr-2"></i> LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<p class="text-xs text-center text-gray-500 mt-10">
|
||||
Career Uprising, Inc :: Authentication Portal ::
|
||||
<i class="text-xs fa-sharp fa-thin fa-copyright"></i> {{ date() }} :: {{ version() }}
|
||||
</p>
|
||||
<p class="text-center text-gray-500 mt-10">
|
||||
<i class="fa-regular text-2xl fa-shield-keyhole"></i>
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ToastMessageOptions } from 'primevue/toast'
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { email, required } from '@vuelidate/validators'
|
||||
|
||||
interface Data {
|
||||
form: {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
magicLink: boolean
|
||||
loading: boolean
|
||||
capabilities: {
|
||||
client_name: string
|
||||
userPass: boolean
|
||||
magicLogin: boolean
|
||||
socials: {
|
||||
linkedIn?: socialProvider
|
||||
google?: socialProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface socialProvider {
|
||||
provider: string
|
||||
clientId: string
|
||||
redirectUrl: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
Checkbox,
|
||||
},
|
||||
setup () {
|
||||
return { v$: useVuelidate() }
|
||||
},
|
||||
data: (): Data => ({
|
||||
form: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
magicLink: false,
|
||||
loading: false,
|
||||
capabilities: {
|
||||
client_name: '',
|
||||
userPass: false,
|
||||
magicLogin: false,
|
||||
socials: {},
|
||||
},
|
||||
}),
|
||||
validations: {
|
||||
form: {
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
required: function (value: string) {
|
||||
// @ts-ignore
|
||||
if (!this.magicLink) {
|
||||
return value !== ''
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.getCapabilities()
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const error = urlParams.get('error')
|
||||
|
||||
// response type === nil means no auth needed. send the user
|
||||
// back through the flow
|
||||
const responseType = urlParams.get('response_type')
|
||||
const redirectUri = urlParams.get('redirect_url')
|
||||
if (responseType === 'nil') {
|
||||
window.location.href = redirectUri || '/'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed',
|
||||
detail: error,
|
||||
life: 5000,
|
||||
closable: false,
|
||||
} as ToastMessageOptions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
date() {
|
||||
return new Date().getFullYear()
|
||||
},
|
||||
version (): string {
|
||||
return import.meta.env.VITE_VERSION || 'dev-master'
|
||||
},
|
||||
getCapabilities () {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
axios.get(`/client/capabilities?client_id=${urlParams.get('client_id')}`).then((response) => {
|
||||
this.capabilities = response.data
|
||||
})
|
||||
},
|
||||
sendMagicLink () {
|
||||
this.v$.$touch()
|
||||
|
||||
if (this.v$.$error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
axios.post('/magic-link', {
|
||||
email: this.form.email,
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.v$.$reset()
|
||||
}).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Magic Link Sent',
|
||||
detail: 'A Link sent to your email if it exists.',
|
||||
life: 3000,
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 3000,
|
||||
})
|
||||
})
|
||||
},
|
||||
login () {
|
||||
|
||||
if (this.magicLink) {
|
||||
return this.sendMagicLink()
|
||||
}
|
||||
|
||||
this.v$.$touch()
|
||||
|
||||
if (this.v$.$error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
axios.post('/login', this.form).then(r => {
|
||||
window.location.href = r.data.location
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed',
|
||||
detail: 'The Login Has Failed',
|
||||
life: 5000,
|
||||
closable: false,
|
||||
} as ToastMessageOptions)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.v$.$reset()
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
101
front-end/src/pages/password-reset.vue
Normal file
101
front-end/src/pages/password-reset.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center p-10">
|
||||
<Card class="p-10 w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-1xl mt-5">
|
||||
Password Reset
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<InputText class="w-full" placeholder="Email" v-model="form.email" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<Button raised :loading="loading" class="w-full" @click="sendReset">
|
||||
<template #default>
|
||||
<span class="p-button-label">
|
||||
<i class="fa-regular fa-paper-plane"></i>
|
||||
Send Reset
|
||||
</span>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<router-link class="w-full p-button p-button-raised p-component p-button-secondary mt-5" to="/">
|
||||
<span class="p-button-label">
|
||||
<i class="fa-sharp fa-regular fa-hand-point-left"></i>
|
||||
Back
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import axios from 'axios'
|
||||
|
||||
interface Data {
|
||||
loading: boolean
|
||||
form: {
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
},
|
||||
data: (): Data => ({
|
||||
loading: false,
|
||||
form: {
|
||||
email: '',
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
sendReset () {
|
||||
if (this.form.email === '') {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Please complete all fields.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
axios.post('/password-reset', this.form).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Password Reset',
|
||||
detail: 'A link was sent to your email if it exists.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
148
front-end/src/pages/update-password.vue
Normal file
148
front-end/src/pages/update-password.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center p-10">
|
||||
<Card class="p-10 w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-1xl mt-5">
|
||||
Password Reset
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-full">
|
||||
<Password
|
||||
toggleMask
|
||||
required
|
||||
inputClass="w-full"
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
class="w-full"
|
||||
ref="passwordField"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<InputText class="w-full" type="password" v-model="passwordAgain" placeholder="Password Again"/>
|
||||
</div>
|
||||
<div v-if="toStrong" class="mt-3">
|
||||
<Message :value="true" severity="error">Your Password is To Strong</Message>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
class="w-full"
|
||||
label="Update Password"
|
||||
@click="updatePassword"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import Password from 'primevue/password'
|
||||
import Message from 'primevue/message'
|
||||
import axios from "axios";
|
||||
|
||||
interface Data {
|
||||
password: string
|
||||
passwordAgain: string
|
||||
loading: boolean
|
||||
toStrong: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
Password,
|
||||
Message
|
||||
},
|
||||
data: (): Data => ({
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
loading: false,
|
||||
toStrong: false,
|
||||
}),
|
||||
created() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
axios.get('/password-reset?k=' + urlParams.get('k'))
|
||||
.catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Key is no longer valid. Please make your request again.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
formValid(): boolean {
|
||||
this.toStrong = false
|
||||
|
||||
if (this.password === '' || this.passwordAgain === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordAgain) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password.toLowerCase.toString() === 'chucknorris' || this.password.toLowerCase.toString()=== 'chuck norris') {
|
||||
this.toStrong = true
|
||||
return false
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return this.$refs.passwordField.$data.meter?.strength !== null && this.$refs.passwordField.$data.meter?.strength !== 'weak'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePassword() {
|
||||
this.loading = true
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
axios.put('/password-reset', {
|
||||
password: this.password,
|
||||
k: urlParams.get('k')
|
||||
}).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Password Reset',
|
||||
detail: 'Your password has been updated.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
})
|
||||
.catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user