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:
Frank John Begornia
2025-11-16 01:19:35 +08:00
parent 0ff41822af
commit bf701f8342
19 changed files with 1807 additions and 223 deletions

View File

@@ -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,
};
};