- 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
291 lines
7.2 KiB
TypeScript
291 lines
7.2 KiB
TypeScript
import {
|
|
signInWithEmailAndPassword,
|
|
signInWithPopup,
|
|
GoogleAuthProvider,
|
|
signOut as firebaseSignOut,
|
|
onAuthStateChanged,
|
|
getAuth,
|
|
createUserWithEmailAndPassword,
|
|
} from "firebase/auth";
|
|
import { getApp, getApps, initializeApp } from "firebase/app";
|
|
import type { User } from "firebase/auth";
|
|
|
|
export const useAuth = () => {
|
|
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
|
|
);
|
|
|
|
const config = useRuntimeConfig();
|
|
|
|
const ensureFirebaseApp = () => {
|
|
if (!process.client) {
|
|
return null;
|
|
}
|
|
|
|
if (!firebaseReady.value) {
|
|
const firebaseConfig = {
|
|
apiKey: config.public.firebaseApiKey,
|
|
authDomain: config.public.firebaseAuthDomain,
|
|
projectId: config.public.firebaseProjectId,
|
|
storageBucket: config.public.firebaseStorageBucket,
|
|
messagingSenderId: config.public.firebaseMessagingSenderId,
|
|
appId: config.public.firebaseAppId,
|
|
...(config.public.firebaseMeasurementId
|
|
? { measurementId: config.public.firebaseMeasurementId }
|
|
: {}),
|
|
};
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
const getAuthInstance = () => {
|
|
const app = ensureFirebaseApp();
|
|
return app ? getAuth(app) : null;
|
|
};
|
|
|
|
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();
|
|
if (!auth) {
|
|
throw new Error("Firebase not initialized");
|
|
}
|
|
|
|
const userCredential = await signInWithEmailAndPassword(
|
|
auth,
|
|
email,
|
|
password
|
|
);
|
|
const idToken = await userCredential.user.getIdToken();
|
|
|
|
try {
|
|
await authenticateWithBackend(idToken);
|
|
} catch (backendErr) {
|
|
console.warn(
|
|
"[useAuth] Backend authentication failed after email login:",
|
|
backendErr
|
|
);
|
|
}
|
|
|
|
user.value = userCredential.user;
|
|
registerListener();
|
|
|
|
return userCredential.user;
|
|
} catch (err: any) {
|
|
error.value = err.message;
|
|
throw err;
|
|
} finally {
|
|
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
|
|
);
|
|
}
|
|
|
|
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();
|
|
if (!auth) {
|
|
throw new Error("Firebase not initialized");
|
|
}
|
|
|
|
await firebaseSignOut(auth);
|
|
user.value = null;
|
|
backendUser.value = null;
|
|
} catch (err: any) {
|
|
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();
|
|
};
|
|
|
|
return {
|
|
user: readonly(user),
|
|
isLoading: readonly(isLoading),
|
|
error: readonly(error),
|
|
backendUser: readonly(backendUser),
|
|
initAuth,
|
|
signInWithEmail,
|
|
signInWithGoogle,
|
|
registerWithEmail,
|
|
signOut,
|
|
getIdToken,
|
|
};
|
|
};
|