feat: Update authentication and profile components

This commit is contained in:
2025-04-22 15:39:53 -04:00
parent 3bb52e7555
commit 22867b5765
2 changed files with 176 additions and 116 deletions

View File

@@ -6,6 +6,7 @@ import {useSessionStorage} from "@vueuse/core";
import {jwtDecode} from "jwt-decode"; import {jwtDecode} from "jwt-decode";
function getClaimsFromToken(token) { function getClaimsFromToken(token) {
if (!token) return null;
try { try {
return jwtDecode(token); return jwtDecode(token);
} catch (error) { } catch (error) {
@@ -17,12 +18,15 @@ function getClaimsFromToken(token) {
function isTokenExpiringSoon(token) { function isTokenExpiringSoon(token) {
if (!token) return true; if (!token) return true;
const claims = getClaimsFromToken(token); const claims = getClaimsFromToken(token);
if (!claims) return true; if (!claims || !claims.exp) return true;
// Check if token will expire in the next 5 minutes // Check if token will expire in the next 5 minutes
const expirationTime = claims.exp * 1000; // Convert to milliseconds const expirationTime = claims.exp * 1000; // Convert to milliseconds
const currentTime = Date.now(); const currentTime = Date.now();
return currentTime >= expirationTime - 5 * 60 * 1000; // 5 minutes before expiration const fiveMinutesInMs = 5 * 60 * 1000;
// Return true if token is expired or will expire in the next 5 minutes
return currentTime >= expirationTime - fiveMinutesInMs;
} }
export const useAuthStore = defineStore( export const useAuthStore = defineStore(
@@ -38,77 +42,121 @@ export const useAuthStore = defineStore(
const accessToken = useSessionStorage('auth-accessToken', undefined) const accessToken = useSessionStorage('auth-accessToken', undefined)
const refreshToken = useSessionStorage('auth-refreshToken', undefined) const refreshToken = useSessionStorage('auth-refreshToken', undefined)
// Cache for decoded claims using session storage with proper serialization
const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
serializer: {
read: (v) => v ? JSON.parse(v) : null,
write: (v) => v ? JSON.stringify(v) : null
}
})
const isAuthenticated = computed(() => !!accessToken.value) const isAuthenticated = computed(() => !!accessToken.value)
const userId = computed(() => { const userId = computed(() => tokenClaims.value?.sub)
const claims = getClaimsFromToken(accessToken.value)
return claims?.sub;
})
function updateTokens(data) { function updateTokens(data) {
accessToken.value = data.accessToken if (!data?.accessToken || !data?.refreshToken) {
refreshToken.value = data.refreshToken throw new Error('Invalid token data');
}
accessToken.value = data.accessToken;
refreshToken.value = data.refreshToken;
// Update claims cache when we get new tokens
const claims = getClaimsFromToken(data.accessToken);
tokenClaims.value = claims;
} }
function cleanTokens() { function cleanTokens() {
updateTokens({ accessToken.value = undefined;
accessToken: undefined, refreshToken.value = undefined;
refreshToken: undefined, tokenClaims.value = null;
}) // Clear any other auth-related data if needed
} }
async function logout() { async function logout() {
cleanTokens() try {
await router.push('/') // Optionally call logout endpoint if you have one
// await clientApi.post('api/users/logout');
} catch (error) {
console.error('Logout failed:', error);
} finally {
cleanTokens();
await router.push('/');
}
} }
async function login(email, password) { async function login(email, password) {
if (!email || !password) {
throw new Error('Email and password are required');
}
try { try {
const response = await clientApi.post( const response = await clientApi.post(
'api/users/login', 'api/users/login',
{ {
email: email, email: email.trim(),
password: password password: password
}) });
updateTokens(response.data)
return true if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid login response');
}
updateTokens(response.data);
return true;
} catch (error) { } catch (error) {
console.error(error) console.error('Login failed:', error);
cleanTokens() cleanTokens();
return false throw error;
} }
} }
async function loginWithGoogle(accessToken) { async function loginWithGoogle(accessToken) {
if (!accessToken) {
throw new Error('Google access token is required');
}
try { try {
const response = await clientApi.post( const response = await clientApi.post(
'api/users/login-with-google', 'api/users/login-with-google',
{ {
token: accessToken token: accessToken
}) });
updateTokens(response.data)
return true if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Google login response');
}
updateTokens(response.data);
return true;
} catch (error) { } catch (error) {
console.error(error) console.error('Google login failed:', error);
cleanTokens() cleanTokens();
return false throw error;
} }
} }
async function loginWithFacebook(authResponse) { async function loginWithFacebook(authResponse) {
if (!authResponse?.accessToken) {
throw new Error('Facebook access token is required');
}
try { try {
const response = await clientApi.post( const response = await clientApi.post(
'api/users/login-with-facebook', 'api/users/login-with-facebook',
{ {
token: authResponse.accessToken token: authResponse.accessToken
}) });
updateTokens(response.data)
return true if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Facebook login response');
}
updateTokens(response.data);
return true;
} catch (error) { } catch (error) {
console.error(error) console.error('Facebook login failed:', error);
cleanTokens() cleanTokens();
return false throw error;
} }
} }
@@ -133,6 +181,10 @@ export const useAuthStore = defineStore(
refreshToken: refreshToken.value refreshToken: refreshToken.value
}); });
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid refresh response');
}
updateTokens({ updateTokens({
accessToken: response.data.accessToken, accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken refreshToken: response.data.refreshToken

View File

@@ -18,9 +18,9 @@ import Linkedin from "@/views/svg/Linkedin.vue";
import Tiktok from "@/views/svg/Tiktok.vue"; import Tiktok from "@/views/svg/Tiktok.vue";
import Instagram from "@/views/svg/Instagram.vue"; import Instagram from "@/views/svg/Instagram.vue";
import Facebook from "@/views/svg/Facebook.vue"; import Facebook from "@/views/svg/Facebook.vue";
import { useI18n } from 'vue-i18n'; import {useI18n} from 'vue-i18n';
const { t } = useI18n(); const {t} = useI18n();
const userProfileStore = useUserProfileStore() const userProfileStore = useUserProfileStore()
// ### Fullname // ### Fullname
@@ -116,63 +116,6 @@ function handleDelete() {
<template> <template>
<div class="min-h-screen w-full"> <div class="min-h-screen w-full">
<v-dialog v-model="dialogEditFullnameShown">
<fullname-dialog
:firstname="userProfileStore.user.firstname"
:lastname="userProfileStore.user.lastname"
@close="handleCloseEditFullname"
@save="handleSaveEditFullname"
></fullname-dialog>
</v-dialog>
<v-dialog v-model="dialogEditAliasShown">
<alias-dialog
:alias="userProfileStore.user.alias"
@close="handleCloseEditAlias"
@save="handleSaveEditAlias"
></alias-dialog>
</v-dialog>
<v-dialog v-model="dialogShown">
<component
:is="currentComponent"
:creator="creatorProfileStore.creator"
:email="userProfileStore.user.email"
@closeRequested="closeDialog"
></component>
</v-dialog>
<v-dialog v-model="restoreDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('restoreCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('restoreWarning') }}</p>
<div class="card-actions">
<button class="secondary" @click="restoreDialogShown = false">
{{ t('cancel') }}
</button>
<button class="primary" @click="handleRestore">
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
<v-dialog v-model="deleteDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('deleteCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('deleteWarning') }}</p>
<div class="card-actions">
<button class="secondary" @click="deleteDialogShown = false">
{{ t('cancel') }}
</button>
<button class="primary danger-action" @click="handleDelete">
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
<div class="flex flex-col items-center gap-4 m-4"> <div class="flex flex-col items-center gap-4 m-4">
<div class="card"> <div class="card">
@@ -231,12 +174,12 @@ function handleDelete() {
</button> </button>
<!-- NAME --> <!-- NAME -->
<button class="action" @click="openDialog('ChangeNameDialog')"> <button class="action" @click="openDialog('ChangeNameDialog')">
<span class="label">{{ t('name') }}</span> <span class="label">{{ t('name') }}</span>
<span class="value">{{ creatorProfileStore.creator.name }}</span> <span class="value">{{ creatorProfileStore.creator.name }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span> <span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
</button> </button>
<!-- TITLE --> <!-- TITLE -->
<button class="action" @click="openDialog('ChangeTitleDialog')"> <button class="action" @click="openDialog('ChangeTitleDialog')">
<span class="label">{{ t('title') }}</span> <span class="label">{{ t('title') }}</span>
@@ -258,8 +201,7 @@ function handleDelete() {
<div class="card-title"> <div class="card-title">
{{ t('socialNetworks') }} {{ t('socialNetworks') }}
</div> </div>
<div class="content">
<div>
<button class="action" @click="openDialog('SocialsDialog')"> <button class="action" @click="openDialog('SocialsDialog')">
<span class="label"> <span class="label">
@@ -292,7 +234,7 @@ function handleDelete() {
<span class="value">{{ creatorProfileStore.creator.socials?.linkedInUrl }}</span> <span class="value">{{ creatorProfileStore.creator.socials?.linkedInUrl }}</span>
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span> <span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
</button> </button>
<button class="action" @click="openDialog('SocialsDialog')"> <button class="action" @click="openDialog('SocialsDialog')">
<span class="label"> <span class="label">
<tiktok class="social-icon"></tiktok> <tiktok class="social-icon"></tiktok>
@@ -329,36 +271,97 @@ function handleDelete() {
</div> </div>
<div class="card"> <div class="card">
<div class="card-title">{{ t('dangerZone') }}</div> <div class="card-title">
{{ t('dangerZone') }}
</div>
<div class="content"> <div class="content">
<p> <span class="p-2">
{{ t('dangerZoneWarning') }} {{ t('dangerZoneWarning') }}
</p> </span>
<button v-if="!creatorProfileStore.creator.isDeleted" <div class="p-2">
class="primary danger-action" <button v-if="!creatorProfileStore.creator.isDeleted"
@click="deleteDialogShown = true"> class="primary danger-action"
{{ t('deleteCreatorPage') }} @click="deleteDialogShown = true">
</button> {{ t('deleteCreatorPage') }}
<button v-else </button>
class="primary safe-action" <button v-else
@click="restoreDialogShown = true"> class="primary safe-action"
{{ t('restoreCreatorPage') }} @click="restoreDialogShown = true">
</button> {{ t('restoreCreatorPage') }}
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
<v-dialog v-model="dialogEditFullnameShown">
<FullnameDialog
:firstname="userProfileStore.user.firstname"
:lastname="userProfileStore.user.lastname"
@close="handleCloseEditFullname"
@save="handleSaveEditFullname"
/>
</v-dialog>
<v-dialog v-model="dialogEditAliasShown">
<alias-dialog
:alias="userProfileStore.user.alias"
@close="handleCloseEditAlias"
@save="handleSaveEditAlias"
></alias-dialog>
</v-dialog>
<v-dialog v-model="dialogShown">
<component
:is="currentComponent"
:creator="creatorProfileStore.creator"
:email="userProfileStore.user.email"
@closeRequested="closeDialog"
></component>
</v-dialog>
<v-dialog v-model="restoreDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('restoreCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('restoreWarning') }}</p>
<div class="card-actions">
<button class="secondary" @click="restoreDialogShown = false">
{{ t('cancel') }}
</button>
<button class="primary" @click="handleRestore">
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
<v-dialog v-model="deleteDialogShown">
<div class="card dialog">
<div class="card-title">{{ t('deleteCreatorPage') }}</div>
<div class="card-content">
<p>{{ t('deleteWarning') }}</p>
<div class="card-actions">
<button class="secondary" @click="deleteDialogShown = false">
{{ t('cancel') }}
</button>
<button class="primary danger-action" @click="handleDelete">
{{ t('accept') }}
</button>
</div>
</div>
</div>
</v-dialog>
</template> </template>
<style scoped> <style scoped>
.card { .card {
@apply bg-hBackground rounded-lg p-4 w-full max-w-2xl; @apply rounded-lg p-4 w-full max-w-2xl;
} }
.card-title { .card-title {
@apply text-hOnBackground text-lg font-bold mb-4; @apply text-hOnBackground text-lg font-bold;
} }
.content { .content {
@@ -366,25 +369,30 @@ function handleDelete() {
} }
.action { .action {
@apply flex flex-row items-center justify-between w-full p-2 rounded-lg; @apply flex flex-row items-center w-full p-3 rounded-lg;
@apply hover:bg-hSurface; @apply hover:bg-hSurface;
@apply transition-colors duration-500;
} }
.label { .label {
@apply text-hOnBackground; @apply text-hOnBackground w-[200px] text-left;
@apply flex items-center justify-start;
} }
.value { .value {
@apply text-hOnBackground opacity-70; @apply text-hOnBackground flex-1 text-center;
@apply flex items-center justify-center;
} }
.chevron { .chevron {
@apply text-hOnBackground opacity-70; @apply text-hOnBackground w-[40px] text-right;
@apply flex items-center justify-end;
} }
.social-icon { .social-icon {
@apply fill-current w-6 h-6; @apply fill-current w-6 h-6;
@apply text-hOnBackground; @apply text-hOnBackground;
@apply mr-2;
} }
.danger-action { .danger-action {