fix(album): improving the creation/update workflow
This commit is contained in:
@@ -1,102 +1,93 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {useAuthStore} from '@/stores/authStore.js';
|
import { useAuthStore } from '@/stores/authStore.js';
|
||||||
|
|
||||||
export function useClient() {
|
export function useClient() {
|
||||||
if (!import.meta.env.VITE_API_URL) {
|
if (!import.meta.env.VITE_API_URL) {
|
||||||
throw new Error('VITE_API_URL is not provided');
|
throw new Error('VITE_API_URL is not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
client.interceptors.request.use(async (config) => {
|
||||||
|
// Check if this is the refresh token endpoint
|
||||||
|
const isRefreshEndpoint = config.url?.includes('api/users/refresh');
|
||||||
|
|
||||||
|
// Check if we need to refresh the token
|
||||||
|
if (authStore.isAuthenticated && !isRefreshEndpoint) {
|
||||||
|
try {
|
||||||
|
// If token is expiring soon, start a refresh and WAIT for it to complete
|
||||||
|
if (authStore.isTokenExpiringSoon(authStore.accessToken)) {
|
||||||
|
console.log(`Token is expiring soon, waiting for refresh to complete before continuing request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
await authStore.refresh();
|
||||||
|
console.log(`Token refresh completed, proceeding with request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to refresh token for: ${config.method?.toUpperCase()} ${config.url}`, error);
|
||||||
|
throw error; // This will cancel the request
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = axios.create({
|
if (authStore.isAuthenticated && !isRefreshEndpoint) {
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
||||||
headers: {
|
}
|
||||||
'Content-Type': 'application/json'
|
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
delete config.headers['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
client.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
console.error(`Response interceptor caught an error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`, error);
|
||||||
|
|
||||||
|
// Prevent retry loops by checking if this is already a retry or a refresh request
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url.includes('api/users/refresh')) {
|
||||||
|
console.log(`Received 401 error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, attempting token refresh...`);
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use a timeout to prevent hanging indefinitely
|
||||||
|
const refreshTimeout = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Token refresh timeout')), 10000); // 10s timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Race the refresh against the timeout
|
||||||
|
await Promise.race([authStore.refresh(), refreshTimeout]);
|
||||||
|
|
||||||
|
console.log(`Token refresh successful, retrying original request: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
|
||||||
|
return client(originalRequest);
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.error(`Token refresh failed for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, logging out user:`, refreshError);
|
||||||
|
// Let the authStore handle the navigation with returnUrl
|
||||||
|
throw refreshError;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
// If it's a refresh request that failed, or a retry that still failed, give up
|
||||||
|
if (originalRequest.url.includes('api/users/refresh') || originalRequest._retry) {
|
||||||
|
console.log(`Request permanently failed: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Request interceptor
|
return Promise.reject(error);
|
||||||
client.interceptors.request.use(async (config) => {
|
}
|
||||||
console.log(`Request interceptor triggered for: ${config.method?.toUpperCase()} ${config.url}`);
|
);
|
||||||
|
|
||||||
// Check if this is the refresh token endpoint
|
return client;
|
||||||
const isRefreshEndpoint = config.url?.includes('api/users/refresh');
|
}
|
||||||
|
|
||||||
// Check if we need to refresh the token
|
|
||||||
if (authStore.isAuthenticated && !isRefreshEndpoint) {
|
|
||||||
try {
|
|
||||||
console.log(`User is authenticated, checking token for: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
|
|
||||||
// If token is expiring soon, start a refresh and WAIT for it to complete
|
|
||||||
if (authStore.isTokenExpiringSoon(authStore.accessToken)) {
|
|
||||||
console.log(`Token is expiring soon, waiting for refresh to complete before continuing request: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
await authStore.refresh();
|
|
||||||
console.log(`Token refresh completed, proceeding with request: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to refresh token for: ${config.method?.toUpperCase()} ${config.url}`, error);
|
|
||||||
throw error; // This will cancel the request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authStore.isAuthenticated && !isRefreshEndpoint) {
|
|
||||||
console.log(`Setting Authorization header for: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
|
|
||||||
} else if (isRefreshEndpoint) {
|
|
||||||
console.log(`Skipping Authorization header for refresh endpoint: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.data instanceof FormData) {
|
|
||||||
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
delete config.headers['Content-Type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Response interceptor
|
|
||||||
client.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
console.log(`Response received successfully for: ${response.config.method?.toUpperCase()} ${response.config.url}`);
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
async (error) => {
|
|
||||||
const originalRequest = error.config;
|
|
||||||
console.error(`Response interceptor caught an error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`, error);
|
|
||||||
|
|
||||||
// Prevent retry loops by checking if this is already a retry or a refresh request
|
|
||||||
if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url.includes('api/users/refresh')) {
|
|
||||||
console.log(`Received 401 error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, attempting token refresh...`);
|
|
||||||
originalRequest._retry = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use a timeout to prevent hanging indefinitely
|
|
||||||
const refreshTimeout = new Promise((_, reject) => {
|
|
||||||
setTimeout(() => reject(new Error('Token refresh timeout')), 10000); // 10s timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
// Race the refresh against the timeout
|
|
||||||
await Promise.race([authStore.refresh(), refreshTimeout]);
|
|
||||||
|
|
||||||
console.log(`Token refresh successful, retrying original request: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
|
|
||||||
return client(originalRequest);
|
|
||||||
} catch (refreshError) {
|
|
||||||
console.error(`Token refresh failed for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, logging out user:`, refreshError);
|
|
||||||
// Let the authStore handle the navigation with returnUrl
|
|
||||||
throw refreshError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a refresh request that failed, or a retry that still failed, give up
|
|
||||||
if (originalRequest.url.includes('api/users/refresh') || originalRequest._retry) {
|
|
||||||
console.log(`Request permanently failed: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
|
|
||||||
// Don't do anything here, let the refresh error handling work
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,284 +1,283 @@
|
|||||||
import {defineStore} from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import {computed, ref} from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import {useRouter} from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import {useClient} from '@/plugins/api.js';
|
import { useClient } from '@/plugins/api.js';
|
||||||
import {useSessionStorage} from '@vueuse/core';
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
import {jwtDecode} from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import {formatDuration} from "@/internal_time_ago.js";
|
import { formatDuration } from "@/internal_time_ago.js";
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const clientApi = useClient();
|
const clientApi = useClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
let refreshPromise = null;
|
let refreshPromise = null;
|
||||||
|
|
||||||
const accessToken = useSessionStorage('auth-accessToken', undefined);
|
const accessToken = useSessionStorage('auth-accessToken', undefined);
|
||||||
const refreshToken = useSessionStorage('auth-refreshToken', undefined);
|
const refreshToken = useSessionStorage('auth-refreshToken', undefined);
|
||||||
const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
|
const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
|
||||||
serializer: {
|
serializer: {
|
||||||
read: (v) => (v ? JSON.parse(v) : null),
|
read: (v) => (v ? JSON.parse(v) : null),
|
||||||
write: (v) => (v ? JSON.stringify(v) : null)
|
write: (v) => (v ? JSON.stringify(v) : null)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!accessToken.value);
|
const isAuthenticated = computed(() => !!accessToken.value);
|
||||||
const userId = computed(() => tokenClaims.value?.sub);
|
const userId = computed(() => tokenClaims.value?.sub);
|
||||||
|
|
||||||
function updateTokens(data) {
|
function updateTokens(data) {
|
||||||
console.log('updateTokens called with response data:', data);
|
if (!data?.accessToken || !data?.refreshToken) {
|
||||||
if (!data?.accessToken || !data?.refreshToken) {
|
throw new Error('Invalid token data');
|
||||||
throw new Error('Invalid token data');
|
}
|
||||||
}
|
accessToken.value = data.accessToken;
|
||||||
accessToken.value = data.accessToken;
|
refreshToken.value = data.refreshToken;
|
||||||
refreshToken.value = data.refreshToken;
|
const claims = getClaimsFromToken(data.accessToken);
|
||||||
const claims = getClaimsFromToken(data.accessToken);
|
tokenClaims.value = claims;
|
||||||
tokenClaims.value = claims;
|
console.log('Tokens updated, user ID:', claims?.sub);
|
||||||
console.log('Tokens updated, user ID:', claims?.sub);
|
}
|
||||||
|
|
||||||
|
function cleanTokens() {
|
||||||
|
console.log('cleanTokens called - clearing stored tokens');
|
||||||
|
accessToken.value = undefined;
|
||||||
|
refreshToken.value = undefined;
|
||||||
|
tokenClaims.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(redirectTo = '/landing') {
|
||||||
|
console.log('logout called, redirecting to:', redirectTo);
|
||||||
|
try {
|
||||||
|
// 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(redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(email, password) {
|
||||||
|
console.log('login called with email:', email);
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error('Email and password are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanTokens() {
|
try {
|
||||||
console.log('cleanTokens called - clearing stored tokens');
|
const response = await clientApi.post('api/users/login', {
|
||||||
accessToken.value = undefined;
|
email: email.trim(),
|
||||||
refreshToken.value = undefined;
|
password: password
|
||||||
tokenClaims.value = null;
|
});
|
||||||
|
|
||||||
|
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||||
|
throw new Error('Invalid login response');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTokens(response.data);
|
||||||
|
console.log('login successful');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
cleanTokens();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithGoogle(accessTokenParam) {
|
||||||
|
console.log('loginWithGoogle called');
|
||||||
|
if (!accessTokenParam) {
|
||||||
|
throw new Error('Google access token is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout(redirectTo = '/landing') {
|
try {
|
||||||
console.log('logout called, redirecting to:', redirectTo);
|
const response = await clientApi.post('api/users/login-with-google', {
|
||||||
|
token: accessTokenParam
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||||
|
throw new Error('Invalid Google login response');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTokens(response.data);
|
||||||
|
console.log('Google login successful');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Google login failed:', error);
|
||||||
|
cleanTokens();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithFacebook(authResponse) {
|
||||||
|
console.log('loginWithFacebook called');
|
||||||
|
if (!authResponse?.accessToken) {
|
||||||
|
throw new Error('Facebook access token is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await clientApi.post('api/users/login-with-facebook', {
|
||||||
|
token: authResponse.accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||||
|
throw new Error('Invalid Facebook login response');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTokens(response.data);
|
||||||
|
console.log('Facebook login successful');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Facebook login failed:', error);
|
||||||
|
cleanTokens();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
console.log('refresh called');
|
||||||
|
|
||||||
|
if (!refreshToken.value) {
|
||||||
|
cleanTokens(); // Clear tokens first
|
||||||
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshing.value && refreshPromise) {
|
||||||
|
console.log('Already refreshing, returning existing refreshPromise');
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
refreshPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
// Optionally call logout endpoint if you have one
|
console.log('Sending refresh request...');
|
||||||
// await clientApi.post('api/users/logout');
|
|
||||||
|
const response = await clientApi.post('api/users/refresh', {
|
||||||
|
refreshToken: refreshToken.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||||
|
throw new Error('Invalid refresh response');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTokens({
|
||||||
|
accessToken: response.data.accessToken,
|
||||||
|
refreshToken: response.data.refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Token refresh successful');
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout failed:', error);
|
console.error('Token refresh failed:', error);
|
||||||
} finally {
|
cleanTokens();
|
||||||
cleanTokens();
|
|
||||||
await router.push(redirectTo);
|
const currentRoute = router.currentRoute.value;
|
||||||
|
const returnUrl = currentRoute.fullPath;
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
router.push({
|
||||||
|
name: 'login',
|
||||||
|
query: { returnUrl }
|
||||||
|
}).catch(navError => {
|
||||||
|
console.error('Navigation error after token refresh failure:', navError);
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error; // Re-throw to notify callers
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return await refreshPromise;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Ensure these are always reset, even if an error is thrown
|
||||||
|
isRefreshing.value = false;
|
||||||
|
refreshPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClaimsFromToken(token) {
|
||||||
|
if (!token) return null;
|
||||||
|
try {
|
||||||
|
return jwtDecode(token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decode token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTokenExpiringSoon(token) {
|
||||||
|
if (!token) {
|
||||||
|
console.log('No token provided, considered expiring soon');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(email, password) {
|
const claims = getClaimsFromToken(token);
|
||||||
console.log('login called with email:', email);
|
if (!claims || !claims.exp) {
|
||||||
if (!email || !password) {
|
console.log('No valid claims found, considered expiring soon');
|
||||||
throw new Error('Email and password are required');
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await clientApi.post('api/users/login', {
|
|
||||||
email: email.trim(),
|
|
||||||
password: password
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
|
||||||
throw new Error('Invalid login response');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTokens(response.data);
|
|
||||||
console.log('login successful');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error);
|
|
||||||
cleanTokens();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginWithGoogle(accessTokenParam) {
|
const expirationTime = claims.exp * 1000; // Convert to milliseconds
|
||||||
console.log('loginWithGoogle called');
|
const currentTime = Date.now();
|
||||||
if (!accessTokenParam) {
|
const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
|
||||||
throw new Error('Google access token is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Calculate time remaining (can be negative if already expired)
|
||||||
const response = await clientApi.post('api/users/login-with-google', {
|
const timeRemainingMs = expirationTime - currentTime;
|
||||||
token: accessTokenParam
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
// Token is expiring soon if less than 2 minutes remaining or already expired
|
||||||
throw new Error('Invalid Google login response');
|
const isExpiring = timeRemainingMs < fiveMinutesInMs;
|
||||||
}
|
|
||||||
|
|
||||||
updateTokens(response.data);
|
// Determine the sign for display purposes
|
||||||
console.log('Google login successful');
|
const formattedTimeRemaining = timeRemainingMs < 0
|
||||||
return true;
|
? `-${formatDuration(Math.abs(timeRemainingMs))}`
|
||||||
} catch (error) {
|
: formatDuration(timeRemainingMs);
|
||||||
console.error('Google login failed:', error);
|
|
||||||
cleanTokens();
|
if (isExpiring) {
|
||||||
throw error;
|
console.log(`Token expiration check; is token expired: ${isExpiring}`, {
|
||||||
}
|
expirationTime: new Date(expirationTime).toLocaleString(),
|
||||||
|
currentTime: new Date(currentTime).toLocaleString(),
|
||||||
|
timeRemaining: formattedTimeRemaining
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginWithFacebook(authResponse) {
|
return isExpiring;
|
||||||
console.log('loginWithFacebook called');
|
}
|
||||||
if (!authResponse?.accessToken) {
|
|
||||||
throw new Error('Facebook access token is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
async function changePassword(newPassword) {
|
||||||
const response = await clientApi.post('api/users/login-with-facebook', {
|
console.log('changePassword called');
|
||||||
token: authResponse.accessToken
|
if (!isAuthenticated.value) {
|
||||||
});
|
throw new Error('User must be authenticated to change password');
|
||||||
|
|
||||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
|
||||||
throw new Error('Invalid Facebook login response');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTokens(response.data);
|
|
||||||
console.log('Facebook login successful');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Facebook login failed:', error);
|
|
||||||
cleanTokens();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
if (!newPassword) {
|
||||||
console.log('refresh called');
|
throw new Error('New password is required');
|
||||||
|
|
||||||
if (!refreshToken.value) {
|
|
||||||
cleanTokens(); // Clear tokens first
|
|
||||||
throw new Error('No refresh token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRefreshing.value && refreshPromise) {
|
|
||||||
console.log('Already refreshing, returning existing refreshPromise');
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isRefreshing.value = true;
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
console.log('Sending refresh request...');
|
|
||||||
|
|
||||||
const response = await clientApi.post('api/users/refresh', {
|
|
||||||
refreshToken: refreshToken.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
|
||||||
throw new Error('Invalid refresh response');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTokens({
|
|
||||||
accessToken: response.data.accessToken,
|
|
||||||
refreshToken: response.data.refreshToken
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Token refresh successful');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Token refresh failed:', error);
|
|
||||||
cleanTokens();
|
|
||||||
|
|
||||||
const currentRoute = router.currentRoute.value;
|
|
||||||
const returnUrl = currentRoute.fullPath;
|
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
router.push({
|
|
||||||
name: 'login',
|
|
||||||
query: {returnUrl}
|
|
||||||
}).catch(navError => {
|
|
||||||
console.error('Navigation error after token refresh failure:', navError);
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error; // Re-throw to notify callers
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return await refreshPromise;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// Ensure these are always reset, even if an error is thrown
|
|
||||||
isRefreshing.value = false;
|
|
||||||
refreshPromise = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClaimsFromToken(token) {
|
try {
|
||||||
if (!token) return null;
|
const response = await clientApi.post('api/users/set-password', {
|
||||||
try {
|
newPassword
|
||||||
return jwtDecode(token);
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to decode token:', error);
|
console.log('Password changed successfully');
|
||||||
return null;
|
return true;
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('Password change failed:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isTokenExpiringSoon(token) {
|
return {
|
||||||
console.log('isTokenExpiringSoon called');
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
if (!token) {
|
isAuthenticated,
|
||||||
console.log('No token provided, considered expiring soon');
|
userId,
|
||||||
return true;
|
isRefreshing,
|
||||||
}
|
login,
|
||||||
|
loginWithGoogle,
|
||||||
const claims = getClaimsFromToken(token);
|
loginWithFacebook,
|
||||||
if (!claims || !claims.exp) {
|
logout,
|
||||||
console.log('No valid claims found, considered expiring soon');
|
refresh,
|
||||||
return true;
|
isTokenExpiringSoon,
|
||||||
}
|
changePassword
|
||||||
|
};
|
||||||
const expirationTime = claims.exp * 1000; // Convert to milliseconds
|
});
|
||||||
const currentTime = Date.now();
|
|
||||||
const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
|
|
||||||
|
|
||||||
// Calculate time remaining (can be negative if already expired)
|
|
||||||
const timeRemainingMs = expirationTime - currentTime;
|
|
||||||
|
|
||||||
// Token is expiring soon if less than 2 minutes remaining or already expired
|
|
||||||
const isExpiring = timeRemainingMs < fiveMinutesInMs;
|
|
||||||
|
|
||||||
// Determine the sign for display purposes
|
|
||||||
const formattedTimeRemaining = timeRemainingMs < 0
|
|
||||||
? `-${formatDuration(Math.abs(timeRemainingMs))}`
|
|
||||||
: formatDuration(timeRemainingMs);
|
|
||||||
|
|
||||||
console.log(`Token expiration check; is token expired: ${isExpiring}`, {
|
|
||||||
expirationTime: new Date(expirationTime).toLocaleString(),
|
|
||||||
currentTime: new Date(currentTime).toLocaleString(),
|
|
||||||
timeRemaining: formattedTimeRemaining
|
|
||||||
});
|
|
||||||
|
|
||||||
return isExpiring;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function changePassword(newPassword) {
|
|
||||||
console.log('changePassword called');
|
|
||||||
if (!isAuthenticated.value) {
|
|
||||||
throw new Error('User must be authenticated to change password');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newPassword) {
|
|
||||||
throw new Error('New password is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await clientApi.post('api/users/set-password', {
|
|
||||||
newPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Password changed successfully');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Password change failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
isAuthenticated,
|
|
||||||
userId,
|
|
||||||
isRefreshing,
|
|
||||||
login,
|
|
||||||
loginWithGoogle,
|
|
||||||
loginWithFacebook,
|
|
||||||
logout,
|
|
||||||
refresh,
|
|
||||||
isTokenExpiringSoon,
|
|
||||||
changePassword
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -214,11 +214,11 @@ async function fetchAlbumData() {
|
|||||||
console.log('in fetchAlbumData()');
|
console.log('in fetchAlbumData()');
|
||||||
if (!brandingStore.value?.id) return;
|
if (!brandingStore.value?.id) return;
|
||||||
|
|
||||||
albumId.value = brandingStore.value.id;
|
const creatorId = brandingStore.value.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get the album
|
// Try to get the album
|
||||||
const response = await client.get(`/api/albums/${albumId.value}`);
|
const response = await client.get(`/api/albums/${creatorId}`);
|
||||||
|
|
||||||
if (response.data && response.data.photos) {
|
if (response.data && response.data.photos) {
|
||||||
// Store original photos for comparison
|
// Store original photos for comparison
|
||||||
@@ -230,15 +230,15 @@ async function fetchAlbumData() {
|
|||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
}));
|
}));
|
||||||
|
albumId.value = creatorId;
|
||||||
} else {
|
} else {
|
||||||
// Initialize with empty array instead of empty slots
|
// Initialize with empty array instead of empty slots
|
||||||
|
console.log('WOW! You found how to get here! Take a look at the stack!');
|
||||||
photos.value = [];
|
photos.value = [];
|
||||||
originalPhotos.value = [];
|
originalPhotos.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
// Album might not exist yet, which is fine
|
catch (error) {
|
||||||
console.log("Album might not exist yet:", error);
|
|
||||||
// Initialize with empty array instead of empty slots
|
|
||||||
photos.value = [];
|
photos.value = [];
|
||||||
originalPhotos.value = [];
|
originalPhotos.value = [];
|
||||||
}
|
}
|
||||||
@@ -302,46 +302,48 @@ async function saveChanges() {
|
|||||||
videoUrl.value = extractVideoId(editableVideoUrl.value) || "";
|
videoUrl.value = extractVideoId(editableVideoUrl.value) || "";
|
||||||
|
|
||||||
// Check for deleted photos
|
// Check for deleted photos
|
||||||
console.log('originalPhotos', originalPhotos.value);
|
|
||||||
console.log('photos', photos.value);
|
|
||||||
const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl);
|
const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl);
|
||||||
console.log('photosThumbnailUrls', photosOriginalUrls);
|
|
||||||
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
|
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
|
||||||
// If the photo URL is not in the current images array, it was deleted
|
// If the photo URL is not in the current images array, it was deleted
|
||||||
return !photosOriginalUrls.includes(originalPhoto.originalUrl);
|
return !photosOriginalUrls.includes(originalPhoto.originalUrl);
|
||||||
});
|
});
|
||||||
|
const newImages = photos.value.filter(photo => photo && photo.image && photo.image.originalUrl.startsWith('data:'));
|
||||||
|
|
||||||
|
console.log('originalPhotos', originalPhotos.value);
|
||||||
|
console.log('photos', photos.value);
|
||||||
console.log('deletedPhotos', deletedPhotos);
|
console.log('deletedPhotos', deletedPhotos);
|
||||||
|
console.log('newImages', newImages);
|
||||||
|
|
||||||
// Save album photos if they've changed
|
// Save album photos if they've changed
|
||||||
if (photos.value.length > 0 || deletedPhotos.length > 0) {
|
if (photos.value.length > 0 || deletedPhotos.length > 0) {
|
||||||
// Create or update the album
|
console.log('We got pending changes');
|
||||||
const albumId = brandingStore.value.id;
|
|
||||||
|
|
||||||
try {
|
// Create the Album if we do not have one yet
|
||||||
// Try to create the album first (it will fail if it already exists)
|
if (albumId.value == null) {
|
||||||
await client.post('/api/albums', {
|
console.log('We do not have an album yet')
|
||||||
albumId: albumId,
|
try {
|
||||||
title: `${brandingStore.value.name}'s Album`,
|
await client.post('/api/albums', {
|
||||||
description: "Photo album for the creator"
|
albumId: brandingStore.value.id,
|
||||||
});
|
title: `${brandingStore.value.name}'s Album`,
|
||||||
} catch (error) {
|
description: "Photo album for the creator"
|
||||||
// Album might already exist, which is fine
|
});
|
||||||
console.log("Album might already exist:", error);
|
albumId.value = brandingStore.value.id;
|
||||||
|
} catch (error) {
|
||||||
|
// Album might already exist, which is fine
|
||||||
|
console.log("Couldn't create an Album", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removed photos
|
// Delete removed photos
|
||||||
for (const photo of deletedPhotos) {
|
for (const photo of deletedPhotos) {
|
||||||
try {
|
try {
|
||||||
await client.delete(`/api/albums/${albumId}/photos/${photo.id}`);
|
await client.delete(`/api/albums/${albumId.value}/photos/${photo.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting photo:", error);
|
console.error("Error deleting photo:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now add or update photos
|
// Now add or update photos
|
||||||
const newImages = photos.value.filter(photo => photo && photo.image && photo.image.originalUrl.startsWith('data:'));
|
|
||||||
console.log('newImages', newImages);
|
|
||||||
for (let i = 0; i < newImages.length; i++) {
|
for (let i = 0; i < newImages.length; i++) {
|
||||||
const imageData = newImages[i];
|
const imageData = newImages[i];
|
||||||
console.log('Image Data to be uploaded:', imageData);
|
console.log('Image Data to be uploaded:', imageData);
|
||||||
@@ -356,7 +358,7 @@ async function saveChanges() {
|
|||||||
|
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
await client.post(`/api/albums/${albumId}/photos`, formData, {
|
await client.post(`/api/albums/${albumId.value}/photos`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="hasImages || isEditMode"
|
|
||||||
class="creator-album"
|
|
||||||
@click="handleAlbumClick">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from "vue";
|
|
||||||
import AlbumView from './AlbumView.vue';
|
|
||||||
import AlbumEditor from './AlbumEditor.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
isEditMode: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:images']);
|
|
||||||
|
|
||||||
// Computed property to check if there are images
|
|
||||||
const hasImages = computed(() => {
|
|
||||||
return props.images.some(url => url);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle album click to enter edit mode
|
|
||||||
function handleAlbumClick() {
|
|
||||||
if (!props.isEditMode) {
|
|
||||||
emit('update:isEditMode', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update images from AlbumEditor component
|
|
||||||
function updateImages(newImages) {
|
|
||||||
emit('update:images', newImages);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.creator-album {
|
|
||||||
@apply w-full;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
|
||||||
{
|
|
||||||
"en": {
|
|
||||||
"common": {
|
|
||||||
"delete": "Delete"
|
|
||||||
},
|
|
||||||
"creator": {
|
|
||||||
"sections": {
|
|
||||||
"album": {
|
|
||||||
"title": "Photo Album",
|
|
||||||
"image": "Album image"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"common": {
|
|
||||||
"delete": "Supprimer"
|
|
||||||
},
|
|
||||||
"creator": {
|
|
||||||
"sections": {
|
|
||||||
"album": {
|
|
||||||
"title": "Album photo",
|
|
||||||
"image": "Image de l'album"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"common": {
|
|
||||||
"delete": "Eliminar"
|
|
||||||
},
|
|
||||||
"creator": {
|
|
||||||
"sections": {
|
|
||||||
"album": {
|
|
||||||
"title": "Álbum de fotos",
|
|
||||||
"image": "Imagen del álbum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</i18n>
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { v7 } from 'uuid';
|
import { v7 } from 'uuid';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
@@ -81,10 +81,6 @@ onMounted(() => {
|
|||||||
localImages.value = props.images;
|
localImages.value = props.images;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(localImages, (newVal) => {
|
|
||||||
console.log('localImages changed:', newVal);
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
function handleFiles(files) {
|
function handleFiles(files) {
|
||||||
console.log('handleFiles:', files)
|
console.log('handleFiles:', files)
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|||||||
Reference in New Issue
Block a user