You've already forked php-auth
generated from siteworxpro/Php-Template
Basics of auth
This commit is contained in:
1
front-end/.env.development
Normal file
1
front-end/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.dev.int
|
||||
1
front-end/.env.production
Normal file
1
front-end/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.careeruprising.com
|
||||
1
front-end/.env.sandbox
Normal file
1
front-end/.env.sandbox
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.sandbox.careeruprising.com
|
||||
26
front-end/.gitignore
vendored
Normal file
26
front-end/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
1
front-end/.nvmrc
Normal file
1
front-end/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v22.14.0
|
||||
BIN
front-end/assets/favicon.ico
Executable file
BIN
front-end/assets/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
15
front-end/index.html
Normal file
15
front-end/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="./assets/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Career UpRising - Login</title>
|
||||
<script src="https://kit.fontawesome.com/a0fd516b66.js" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/src/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2990
front-end/package-lock.json
generated
Normal file
2990
front-end/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
front-end/package.json
Normal file
32
front-end/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "front-end",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -Rf node_modules && rm -Rf ../public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.13.2",
|
||||
"lodash": "^4.17.21",
|
||||
"primevue": "^4.4.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"sass": "^1.94.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
}
|
||||
14
front-end/src/App.vue
Normal file
14
front-end/src/App.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Toast/>
|
||||
<router-view />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
|
||||
export default defineComponent({})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
29
front-end/src/main.ts
Normal file
29
front-end/src/main.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import theme from '@primevue/themes/nora'
|
||||
import router from './router'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: theme,
|
||||
options: {
|
||||
prefix: 'p-',
|
||||
darkModeSelector: 'off',
|
||||
cssLayer: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(ToastService);
|
||||
app.use(router)
|
||||
app.component('InputText', InputText)
|
||||
app.component('Button', Button)
|
||||
app.component('Toast', Toast)
|
||||
|
||||
app.mount('#app')
|
||||
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>
|
||||
15
front-end/src/router/index.ts
Normal file
15
front-end/src/router/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createWebHashHistory, createRouter } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => import('../pages/login.vue') },
|
||||
{ path: '/error', component: () => import('../pages/error.vue') },
|
||||
{ path: '/password-reset', component: () => import('../pages/password-reset.vue') },
|
||||
{ path: '/update-password', component: () => import('../pages/update-password.vue') },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
64
front-end/src/style.css
Normal file
64
front-end/src/style.css
Normal file
@@ -0,0 +1,64 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
:root {
|
||||
color: #444;
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 13px;
|
||||
|
||||
--color-primary: #304d63;
|
||||
--color-primary-500: #426f8a;
|
||||
--color-secondary: #b3e7e8;
|
||||
--color-success: #8fb9aa;
|
||||
--color-success-900: #479881;
|
||||
--color-error: #dc5e5e;
|
||||
--color-warning: #f2d196;
|
||||
|
||||
--p--inputtext-border-radius: 0 !important;
|
||||
--p--card-border-radius: 0 !important;
|
||||
--p--select-border-radius: 0 !important;
|
||||
|
||||
--p--button-primary-background: var(--color-primary) !important;
|
||||
--p--button-primary-hover-background: var(--color-primary-500) !important;
|
||||
--p--button-primary-active-background: var(--color-primary) !important;
|
||||
|
||||
--p--button-secondary-background: var(--color-secondary) !important;
|
||||
|
||||
--p--button-info-background: #ed8975 !important;
|
||||
--p--button-info-hover-background: #ea9b8c !important;
|
||||
--p--button-info-border-color: #ed8975 !important;
|
||||
--p--button-info-hover-border-color: #ed8975 !important;
|
||||
|
||||
--p--button-danger-background: var(--color-error) !important;
|
||||
--p--button-danger-border-color: #ffffff !important;
|
||||
|
||||
--p--button-label-font-weight: 100 !important;
|
||||
|
||||
--p--message-error-background: var(--color-error) !important;
|
||||
--p--message-error-border-color: #ffffff !important;
|
||||
|
||||
--p--button-help-background: #4d95df !important;
|
||||
--p--button-help-hover-background: #2880d6 !important;
|
||||
--p--button-help-border-color: #84b0d8 !important;
|
||||
--p--button-help-hover-border-color: #4f799f !important;
|
||||
|
||||
|
||||
--p--toast-success-background: var(--color-success-900) !important;
|
||||
|
||||
--p--toast-info-background: var(--color-secondary) !important;
|
||||
--p--toast-info-color: #4a2525 !important;
|
||||
--p--toast-info-detail-color: #4a3131 !important;
|
||||
--p--toast-info-border-color: #3d5875 !important;
|
||||
--p--toast-info-close-button-hover-background: #ed8975 !important;
|
||||
|
||||
--p--toast-warn-background: var(--color-warning) !important;
|
||||
--p--toast-warn-color: #712828 !important;
|
||||
--p--toast-warn-detail-color: #4a3131 !important;
|
||||
|
||||
--p--toast-error-background: var(--color-error) !important;
|
||||
}
|
||||
1
front-end/src/vite-env.d.ts
vendored
Normal file
1
front-end/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
12
front-end/tailwind.config.js
Normal file
12
front-end/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
25
front-end/tsconfig.json
Normal file
25
front-end/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
front-end/tsconfig.node.json
Normal file
10
front-end/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
front-end/vite.config.ts
Normal file
10
front-end/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
build: {
|
||||
outDir: '../public'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user