feat: Update authentication and profile components
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user