Authentication flow change - Remove requirement for user to enter username - Use Passkey discoverability instead.
This commit is contained in:
parent
0d277e3035
commit
7564937faa
8 changed files with 120 additions and 75 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -16,5 +16,8 @@
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true,
|
||||||
"editor.trimAutoWhitespace": true
|
"editor.trimAutoWhitespace": true,
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "vscode.css-language-features"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -50,7 +50,10 @@ export default defineConfig((/* ctx */) =>
|
||||||
|
|
||||||
// publicPath: '/',
|
// publicPath: '/',
|
||||||
// analyze: true,
|
// analyze: true,
|
||||||
// env: {},
|
env: {
|
||||||
|
API_URL: process.env.API_URL || '/api',
|
||||||
|
PRODUCT_NAME: process.env.PRODUCT_NAME || 'StylePoint',
|
||||||
|
},
|
||||||
// rawDefine: {}
|
// rawDefine: {}
|
||||||
// ignorePublicFolder: true,
|
// ignorePublicFolder: true,
|
||||||
// minify: false,
|
// minify: false,
|
||||||
|
|
|
@ -60,7 +60,7 @@ router.post('/generate-registration-options', async(req, res) =>
|
||||||
//Check if the registrationToken matches the setting
|
//Check if the registrationToken matches the setting
|
||||||
const registrationTokenSetting = await getSetting('REGISTRATION_TOKEN');
|
const registrationTokenSetting = await getSetting('REGISTRATION_TOKEN');
|
||||||
|
|
||||||
if (registrationTokenSetting !== registrationToken)
|
if (registrationTokenSetting !== registrationToken && !req.session.loggedInUserId)
|
||||||
{
|
{
|
||||||
return res.status(403).json({ error: 'Invalid registration token' });
|
return res.status(403).json({ error: 'Invalid registration token' });
|
||||||
}
|
}
|
||||||
|
@ -200,9 +200,6 @@ router.post('/verify-registration', async(req, res) =>
|
||||||
challengeStore.delete(userId);
|
challengeStore.delete(userId);
|
||||||
delete req.session.userId;
|
delete req.session.userId;
|
||||||
|
|
||||||
// Log the user in by setting the final session userId
|
|
||||||
req.session.loggedInUserId = user.id;
|
|
||||||
|
|
||||||
res.json({ verified: true });
|
res.json({ verified: true });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -243,27 +240,29 @@ router.post('/generate-authentication-options', async(req, res) =>
|
||||||
user = await getUserById(req.session.loggedInUserId);
|
user = await getUserById(req.session.loggedInUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user)
|
// if (!user)
|
||||||
{
|
// {
|
||||||
return res.status(404).json({ error: 'User not found' });
|
// return res.status(404).json({ error: 'User not found' });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const userAuthenticators = await getUserAuthenticators(user.id);
|
// const userAuthenticators = await getUserAuthenticators(user.id);
|
||||||
|
|
||||||
const options = await generateAuthenticationOptions({
|
const options = await generateAuthenticationOptions({
|
||||||
rpID,
|
rpID,
|
||||||
// Require users to use a previously-registered authenticator
|
// Require users to use a previously-registered authenticator
|
||||||
allowCredentials: userAuthenticators.map(auth => ({
|
// allowCredentials: userAuthenticators.map(auth => ({
|
||||||
id: auth.credentialID,
|
// id: auth.credentialID,
|
||||||
type: 'public-key',
|
// type: 'public-key',
|
||||||
transports: auth.transports ? auth.transports.split(',') : undefined,
|
// transports: auth.transports ? auth.transports.split(',') : undefined,
|
||||||
})),
|
// })),
|
||||||
|
allowCredentials: [],
|
||||||
userVerification: 'preferred',
|
userVerification: 'preferred',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store the challenge associated with the user ID for verification
|
//Store challenge associated with random ID
|
||||||
challengeStore.set(user.id, options.challenge);
|
const challengeId = crypto.randomUUID();
|
||||||
req.session.challengeUserId = user.id; // Store user ID associated with this challenge
|
challengeStore.set(challengeId, options.challenge);
|
||||||
|
req.session.challengeUserId = challengeId; // Store user ID associated with this challenge
|
||||||
|
|
||||||
res.json(options);
|
res.json(options);
|
||||||
}
|
}
|
||||||
|
@ -294,11 +293,11 @@ router.post('/verify-authentication', async(req, res) =>
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const user = await getUserById(challengeUserId);
|
// const user = await getUserById(challengeUserId);
|
||||||
if (!user)
|
// if (!user)
|
||||||
{
|
// {
|
||||||
return res.status(404).json({ error: 'User associated with challenge not found' });
|
// return res.status(404).json({ error: 'User associated with challenge not found' });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
|
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
|
||||||
|
|
||||||
|
@ -307,6 +306,12 @@ router.post('/verify-authentication', async(req, res) =>
|
||||||
return res.status(404).json({ error: 'Authenticator not found' });
|
return res.status(404).json({ error: 'Authenticator not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await getUserById(authenticator.userId);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the authenticator belongs to the user attempting to log in
|
// Ensure the authenticator belongs to the user attempting to log in
|
||||||
if (authenticator.userId !== user.id)
|
if (authenticator.userId !== user.id)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,23 @@
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
font-family: 'Exo 2', sans-serif;
|
font-family: 'Exo 2', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-theme {
|
||||||
|
background: linear-gradient(to top left,
|
||||||
|
#fdc730,
|
||||||
|
#ea2963,
|
||||||
|
#bd288a 50%,
|
||||||
|
#6e43ac,
|
||||||
|
#4763bf,
|
||||||
|
#16a3e8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.bg-blurred {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section v-if="leftDrawerOpen">
|
<q-item-section v-if="leftDrawerOpen">
|
||||||
<q-item-label class="text-h4 absolute-center">
|
<q-item-label class="text-h4 absolute-center">
|
||||||
StylePoint
|
{{ productName }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
@ -195,7 +195,7 @@
|
||||||
<q-fab
|
<q-fab
|
||||||
v-model="fabOpen"
|
v-model="fabOpen"
|
||||||
icon="chat"
|
icon="chat"
|
||||||
color="accent"
|
class="bg-theme"
|
||||||
direction="up"
|
direction="up"
|
||||||
@click="toggleChat"
|
@click="toggleChat"
|
||||||
/>
|
/>
|
||||||
|
@ -265,6 +265,8 @@ import { usePreferencesStore } from 'stores/preferences'; // Import the preferen
|
||||||
import ChatInterface from 'components/ChatInterface.vue'; // Adjust path as needed
|
import ChatInterface from 'components/ChatInterface.vue'; // Adjust path as needed
|
||||||
import routes from '../router/routes'; // Import routes
|
import routes from '../router/routes'; // Import routes
|
||||||
|
|
||||||
|
const productName = process.env.PRODUCT_NAME;
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const leftDrawerOpen = ref(false);
|
const leftDrawerOpen = ref(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page class="landing-page column items-center q-pa-md wallpaper">
|
<q-page class="landing-page column items-center q-pa-md bg-theme">
|
||||||
<div class="hero text-center q-pa-xl full-width">
|
<div class="hero text-center q-pa-xl full-width">
|
||||||
<q-img
|
<img
|
||||||
src="/stylepoint.png"
|
src="/stylepoint.png"
|
||||||
alt="StylePoint Logo"
|
alt="StylePoint Logo"
|
||||||
class="logo q-mb-md"
|
class="logo q-mb-md"
|
||||||
style="max-width: 300px; width: 100%;"
|
style="max-width: 300px; width: 100%;"
|
||||||
/>
|
>
|
||||||
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
|
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
|
||||||
Welcome to StylePoint
|
Welcome to {{ productName }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-h6 text-white q-mb-lg">
|
<p class="text-h6 text-white q-mb-lg">
|
||||||
An all-in-one tool designed for StyleTech Developers.
|
An all-in-one tool designed for StyleTech Developers.
|
||||||
|
@ -47,6 +47,8 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const productName = process.env.PRODUCT_NAME;
|
||||||
|
|
||||||
const features = ref([
|
const features = ref([
|
||||||
'Automated Daily Reports',
|
'Automated Daily Reports',
|
||||||
'Deep Mantis Integration',
|
'Deep Mantis Integration',
|
||||||
|
@ -65,18 +67,6 @@ const features = ref([
|
||||||
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper {
|
|
||||||
background: linear-gradient(
|
|
||||||
to top left,
|
|
||||||
#fdc730,
|
|
||||||
#ea2963,
|
|
||||||
#bd288a 50%,
|
|
||||||
#6e43ac,
|
|
||||||
#4763bf,
|
|
||||||
#16a3e8
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
.features {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
|
@ -1,29 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page class="flex flex-center">
|
<q-page class="flex flex-center bg-theme">
|
||||||
<q-card style="width: 400px; max-width: 90vw;">
|
<img
|
||||||
|
src="/stylepoint.png"
|
||||||
|
alt="StylePoint Logo"
|
||||||
|
class="logo q-mb-md absolute"
|
||||||
|
style="max-width: 300px; width: 100%; top: 75px;"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
style="width: 400px; max-width: 90vw;"
|
||||||
|
dark
|
||||||
|
class="bg-blurred"
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">
|
<div
|
||||||
Login
|
class="
|
||||||
|
text-h4
|
||||||
|
text-center"
|
||||||
|
>
|
||||||
|
<small>Login</small>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
<q-separator />
|
||||||
<q-input
|
|
||||||
v-model="username"
|
<q-card-section class="row justify-center">
|
||||||
label="Username"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
class="q-mb-md"
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
:hint="errorMessage ? errorMessage : ''"
|
|
||||||
:rules="[val => !!val || 'Username is required']"
|
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Login with Passkey"
|
icon="key"
|
||||||
color="primary"
|
round
|
||||||
class="full-width"
|
|
||||||
@click="handleLogin"
|
@click="handleLogin"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
size="xl"
|
||||||
|
class="bg-theme shadow-5"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
|
@ -35,7 +44,8 @@
|
||||||
|
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
outline
|
||||||
|
color="secondary"
|
||||||
label="Don't have an account? Register"
|
label="Don't have an account? Register"
|
||||||
to="/register"
|
to="/register"
|
||||||
/>
|
/>
|
||||||
|
@ -51,7 +61,8 @@ import { startAuthentication } from '@simplewebauthn/browser';
|
||||||
import axios from 'boot/axios';
|
import axios from 'boot/axios';
|
||||||
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
import { useAuthStore } from 'stores/auth'; // Import the auth store
|
||||||
|
|
||||||
const username = ref('');
|
const productName = process.env.PRODUCT_NAME;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -65,9 +76,7 @@ async function handleLogin()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Get options from server
|
// 1. Get options from server
|
||||||
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {
|
const optionsRes = await axios.post('/api/auth/generate-authentication-options');
|
||||||
username: username.value || undefined, // Send username if provided
|
|
||||||
});
|
|
||||||
const options = optionsRes.data;
|
const options = optionsRes.data;
|
||||||
|
|
||||||
// 2. Start authentication ceremony in browser
|
// 2. Start authentication ceremony in browser
|
||||||
|
@ -125,4 +134,4 @@ async function handleLogin()
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,13 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page class="flex flex-center">
|
<q-page class="flex flex-center bg-theme">
|
||||||
<q-card style="width: 400px; max-width: 90vw;">
|
<img
|
||||||
|
src="/stylepoint.png"
|
||||||
|
alt="StylePoint Logo"
|
||||||
|
class="logo q-mb-md absolute"
|
||||||
|
style="max-width: 300px; width: 100%; top: 75px;"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
style="width: 400px; max-width: 90vw;"
|
||||||
|
dark
|
||||||
|
class="bg-blurred"
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<!-- Update title -->
|
<!-- Update title -->
|
||||||
<div class="text-h6">
|
<div class="text-h4 text-center">
|
||||||
Register Passkey
|
Register Account
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="username"
|
v-model="username"
|
||||||
|
@ -45,12 +59,12 @@
|
||||||
@keyup.enter="handleRegister"
|
@keyup.enter="handleRegister"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Register Passkey"
|
label="Register Account"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="handleRegister"
|
@click="handleRegister"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disable="loading || !username || !email || !fullName"
|
:disable="loading || !username || !email || !fullName || (showTokenInput && !registrationToken)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="successMessage"
|
v-if="successMessage"
|
||||||
|
@ -66,14 +80,16 @@
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="center">
|
<q-card-section>
|
||||||
<!-- Always show login link -->
|
<!-- Always show login link -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
color="secondary"
|
||||||
flat
|
flat
|
||||||
label="Already have an account? Login"
|
label="Already have an account? Login"
|
||||||
to="/login"
|
to="/login"
|
||||||
|
class="full-width"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue