Replace Firebase Storage with MinIO and add user account features
- Storage Integration: * Remove Firebase Storage dependency and useFirebaseStorage composable * Implement direct MinIO uploads via POST /storage/upload with multipart/form-data * Upload canvas JSON, preview PNG, and production PNG as separate objects * Store public URLs and metadata in design records - Authentication & Registration: * Add email/password registration page with validation * Integrate backend user session via /auth/login endpoint * Store backendUser.id as ownerId in design records * Auto-sync backend session on Firebase auth state changes - User Account Pages: * Create profile page showing user details and backend session info * Create orders page with transaction history filtered by customerEmail * Add server proxy /api/orders to forward GET /transactions queries - Navigation Improvements: * Replace inline auth buttons with avatar dropdown menu * Add Profile, Orders, and Logout options to dropdown * Implement outside-click and route-change handlers for dropdown * Display user initials in avatar badge - API Updates: * Update transactions endpoint to accept amount as string * Format amount with .toFixed(2) in checkout success flow * Query orders by customerEmail instead of ownerId for consistency
This commit is contained in:
@@ -1,25 +1,37 @@
|
||||
import {
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
import {
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
signOut as firebaseSignOut,
|
||||
onAuthStateChanged,
|
||||
getAuth
|
||||
} from 'firebase/auth'
|
||||
import { getApps, initializeApp } from 'firebase/app'
|
||||
import type { User } from 'firebase/auth'
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
} from "firebase/auth";
|
||||
import { getApp, getApps, initializeApp } from "firebase/app";
|
||||
import type { User } from "firebase/auth";
|
||||
|
||||
export const useAuth = () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const user = useState<User | null>("auth-user", () => null);
|
||||
const isLoading = useState<boolean>("auth-loading", () => true);
|
||||
const error = useState<string | null>("auth-error", () => null);
|
||||
const firebaseReady = useState<boolean>("firebase-ready", () => false);
|
||||
const listenerRegistered = useState<boolean>(
|
||||
"auth-listener-registered",
|
||||
() => false
|
||||
);
|
||||
const backendUser = useState<Record<string, any> | null>(
|
||||
"auth-backend-user",
|
||||
() => null
|
||||
);
|
||||
|
||||
// Initialize Firebase if not already initialized
|
||||
const initializeFirebase = async () => {
|
||||
if (process.client && getApps().length === 0) {
|
||||
console.log('Initializing Firebase with config:')
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const ensureFirebaseApp = () => {
|
||||
if (!process.client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!firebaseReady.value) {
|
||||
const firebaseConfig = {
|
||||
apiKey: config.public.firebaseApiKey,
|
||||
authDomain: config.public.firebaseAuthDomain,
|
||||
@@ -29,149 +41,250 @@ export const useAuth = () => {
|
||||
appId: config.public.firebaseAppId,
|
||||
...(config.public.firebaseMeasurementId
|
||||
? { measurementId: config.public.firebaseMeasurementId }
|
||||
: {})
|
||||
}
|
||||
await initializeApp(firebaseConfig)
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (getApps().length === 0) {
|
||||
initializeApp(firebaseConfig);
|
||||
}
|
||||
|
||||
firebaseReady.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
return getApp();
|
||||
} catch (err) {
|
||||
console.error("[useAuth] Failed to get Firebase app instance:", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get auth instance directly
|
||||
const getAuthInstance = () => {
|
||||
if (process.client) {
|
||||
initializeFirebase()
|
||||
return getAuth()
|
||||
}
|
||||
return null
|
||||
}
|
||||
const app = ensureFirebaseApp();
|
||||
return app ? getAuth(app) : null;
|
||||
};
|
||||
|
||||
// Initialize auth state listener
|
||||
const initAuth = () => {
|
||||
if (process.client) {
|
||||
try {
|
||||
const auth = getAuthInstance()
|
||||
if (auth) {
|
||||
onAuthStateChanged(auth, (firebaseUser) => {
|
||||
user.value = firebaseUser
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize auth:', err)
|
||||
isLoading.value = false
|
||||
}
|
||||
const authenticateWithBackend = async (idToken: string) => {
|
||||
try {
|
||||
const response = await $fetch<{
|
||||
token?: string;
|
||||
user?: Record<string, any> | null;
|
||||
}>("/auth/login", {
|
||||
baseURL: config.public.backendUrl,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
idToken,
|
||||
},
|
||||
});
|
||||
|
||||
backendUser.value = response?.user ?? null;
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.error("Backend authentication failed:", err);
|
||||
backendUser.value = null;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const syncBackendWithToken = async (tokenProvider: () => Promise<string>) => {
|
||||
try {
|
||||
const idToken = await tokenProvider();
|
||||
await authenticateWithBackend(idToken);
|
||||
} catch (err) {
|
||||
console.warn("[useAuth] Failed to sync backend session", err);
|
||||
}
|
||||
};
|
||||
|
||||
const registerListener = () => {
|
||||
if (!process.client || listenerRegistered.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = getAuthInstance();
|
||||
if (auth) {
|
||||
const existingUser = auth.currentUser;
|
||||
if (existingUser && !user.value) {
|
||||
user.value = existingUser;
|
||||
isLoading.value = false;
|
||||
syncBackendWithToken(() => existingUser.getIdToken());
|
||||
}
|
||||
|
||||
listenerRegistered.value = true;
|
||||
onAuthStateChanged(auth, (firebaseUser) => {
|
||||
user.value = firebaseUser;
|
||||
isLoading.value = false;
|
||||
if (firebaseUser) {
|
||||
syncBackendWithToken(() => firebaseUser.getIdToken());
|
||||
} else {
|
||||
backendUser.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[useAuth] Failed to initialize auth listener:", err);
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const initAuth = () => {
|
||||
registerListener();
|
||||
};
|
||||
|
||||
// Eagerly register listener when composable is used in client context
|
||||
if (process.client) {
|
||||
registerListener();
|
||||
}
|
||||
|
||||
// Sign in with email and password
|
||||
const signInWithEmail = async (email: string, password: string) => {
|
||||
try {
|
||||
error.value = null
|
||||
isLoading.value = true
|
||||
|
||||
const auth = getAuthInstance()
|
||||
error.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
const auth = getAuthInstance();
|
||||
if (!auth) {
|
||||
throw new Error('Firebase not initialized')
|
||||
throw new Error("Firebase not initialized");
|
||||
}
|
||||
|
||||
const userCredential = await signInWithEmailAndPassword(auth, email, password)
|
||||
const idToken = await userCredential.user.getIdToken()
|
||||
|
||||
|
||||
const userCredential = await signInWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
const idToken = await userCredential.user.getIdToken();
|
||||
|
||||
try {
|
||||
await authenticateWithBackend(idToken)
|
||||
await authenticateWithBackend(idToken);
|
||||
} catch (backendErr) {
|
||||
console.warn('[useAuth] Backend authentication failed after email login:', backendErr)
|
||||
console.warn(
|
||||
"[useAuth] Backend authentication failed after email login:",
|
||||
backendErr
|
||||
);
|
||||
}
|
||||
|
||||
return userCredential.user
|
||||
|
||||
user.value = userCredential.user;
|
||||
registerListener();
|
||||
|
||||
return userCredential.user;
|
||||
} catch (err: any) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
error.value = err.message;
|
||||
throw err;
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Sign in with Google
|
||||
const signInWithGoogle = async () => {
|
||||
try {
|
||||
error.value = null
|
||||
isLoading.value = true
|
||||
|
||||
const auth = getAuthInstance()
|
||||
if (!auth) {
|
||||
throw new Error('Firebase not initialized')
|
||||
}
|
||||
|
||||
const provider = new GoogleAuthProvider()
|
||||
const userCredential = await signInWithPopup(auth, provider)
|
||||
const idToken = await userCredential.user.getIdToken()
|
||||
|
||||
try {
|
||||
await authenticateWithBackend(idToken)
|
||||
} catch (backendErr) {
|
||||
console.warn('[useAuth] Backend authentication failed after Google login:', backendErr)
|
||||
}
|
||||
|
||||
return userCredential.user
|
||||
} catch (err: any) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
error.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
// Authenticate with backend
|
||||
const authenticateWithBackend = async (idToken: string) => {
|
||||
try {
|
||||
const response = await $fetch('/auth/login', {
|
||||
baseURL: config.public.backendUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
idToken
|
||||
}
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Backend authentication failed:', err)
|
||||
throw err
|
||||
const auth = getAuthInstance();
|
||||
if (!auth) {
|
||||
throw new Error("Firebase not initialized");
|
||||
}
|
||||
|
||||
const provider = new GoogleAuthProvider();
|
||||
const userCredential = await signInWithPopup(auth, provider);
|
||||
const idToken = await userCredential.user.getIdToken();
|
||||
|
||||
try {
|
||||
await authenticateWithBackend(idToken);
|
||||
} catch (backendErr) {
|
||||
console.warn(
|
||||
"[useAuth] Backend authentication failed after Google login:",
|
||||
backendErr
|
||||
);
|
||||
}
|
||||
|
||||
user.value = userCredential.user;
|
||||
registerListener();
|
||||
|
||||
return userCredential.user;
|
||||
} catch (err: any) {
|
||||
error.value = err.message;
|
||||
throw err;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const registerWithEmail = async (email: string, password: string) => {
|
||||
try {
|
||||
error.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
const auth = getAuthInstance();
|
||||
if (!auth) {
|
||||
throw new Error("Firebase not initialized");
|
||||
}
|
||||
|
||||
const userCredential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
const idToken = await userCredential.user.getIdToken();
|
||||
|
||||
try {
|
||||
await authenticateWithBackend(idToken);
|
||||
} catch (backendErr) {
|
||||
console.warn(
|
||||
"[useAuth] Backend authentication failed after registration:",
|
||||
backendErr
|
||||
);
|
||||
}
|
||||
|
||||
user.value = userCredential.user;
|
||||
registerListener();
|
||||
|
||||
return userCredential.user;
|
||||
} catch (err: any) {
|
||||
error.value = err.message;
|
||||
throw err;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
try {
|
||||
const auth = getAuthInstance()
|
||||
const auth = getAuthInstance();
|
||||
if (!auth) {
|
||||
throw new Error('Firebase not initialized')
|
||||
throw new Error("Firebase not initialized");
|
||||
}
|
||||
|
||||
await firebaseSignOut(auth)
|
||||
user.value = null
|
||||
|
||||
await firebaseSignOut(auth);
|
||||
user.value = null;
|
||||
backendUser.value = null;
|
||||
} catch (err: any) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
error.value = err.message;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get current user's ID token
|
||||
const getIdToken = async () => {
|
||||
if (!user.value) return null
|
||||
return await user.value.getIdToken()
|
||||
}
|
||||
if (!user.value) return null;
|
||||
return await user.value.getIdToken();
|
||||
};
|
||||
|
||||
return {
|
||||
user: readonly(user),
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
backendUser: readonly(backendUser),
|
||||
initAuth,
|
||||
signInWithEmail,
|
||||
signInWithGoogle,
|
||||
registerWithEmail,
|
||||
signOut,
|
||||
getIdToken
|
||||
}
|
||||
}
|
||||
getIdToken,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user