Basics of auth

This commit is contained in:
2026-01-01 10:32:17 -05:00
parent 23f2b6432b
commit 9f895bbb85
66 changed files with 5967 additions and 156 deletions

View 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>

View 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>

View 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>

View 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>