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 @@
VITE_AUTH_URL=https://auth.dev.int

View File

@@ -0,0 +1 @@
VITE_AUTH_URL=https://auth.careeruprising.com

1
front-end/.env.sandbox Normal file
View File

@@ -0,0 +1 @@
VITE_AUTH_URL=https://auth.sandbox.careeruprising.com

26
front-end/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
v22.14.0

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

File diff suppressed because it is too large Load Diff

32
front-end/package.json Normal file
View 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
View 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
View 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')

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>

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

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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
View 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" }]
}

View 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
View 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'
}
})