Implement Firebase Authentication and Cloud Functions deployment
- Replace custom JWT auth with Firebase Auth SDK - Add Firebase web app configuration - Implement user registration and login with Firebase - Update backend to use Firebase Admin SDK for token verification - Remove custom auth routes and controllers - Add Firebase Cloud Functions deployment configuration - Update frontend to use Firebase Auth state management - Add registration mode toggle to login form - Configure CORS and deployment for Firebase hosting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
7
frontend/.env.production
Normal file
7
frontend/.env.production
Normal file
@@ -0,0 +1,7 @@
|
||||
VITE_API_BASE_URL=https://api-y56ccs6wva-uc.a.run.app/api
|
||||
VITE_FIREBASE_API_KEY=AIzaSyBoV04YHkbCSUIU6sXki57um4xNsvLV_jY
|
||||
VITE_FIREBASE_AUTH_DOMAIN=cim-summarizer.firebaseapp.com
|
||||
VITE_FIREBASE_PROJECT_ID=cim-summarizer
|
||||
VITE_FIREBASE_STORAGE_BUCKET=cim-summarizer.firebasestorage.app
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=245796323861
|
||||
VITE_FIREBASE_APP_ID=1:245796323861:web:39c1c86e0e4b405510041c
|
||||
5
frontend/.firebaserc
Normal file
5
frontend/.firebaserc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "cim-summarizer"
|
||||
}
|
||||
}
|
||||
69
frontend/.gitignore
vendored
Normal file
69
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
firebase-debug.log*
|
||||
firebase-debug.*.log*
|
||||
|
||||
# Firebase cache
|
||||
.firebase/
|
||||
|
||||
# Firebase config
|
||||
|
||||
# Uncomment this if you'd like others to create their own Firebase project.
|
||||
# For a team working on the same Firebase project(s), it is recommended to leave
|
||||
# it commented so all members can deploy to the same project(s) in .firebaserc.
|
||||
# .firebaserc
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# dataconnect generated files
|
||||
.dataconnect
|
||||
16
frontend/firebase.json
Normal file
16
frontend/firebase.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1051
frontend/package-lock.json
generated
1051
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"firebase": "^12.0.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -116,6 +116,7 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
|
||||
reviewers: '',
|
||||
cimPageCount: '',
|
||||
statedReasonForSale: '',
|
||||
employeeCount: '',
|
||||
},
|
||||
|
||||
// Business Description
|
||||
@@ -200,9 +201,7 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
|
||||
}
|
||||
}, [cimReviewData]);
|
||||
|
||||
const updateData = (field: keyof CIMReviewData, value: any) => {
|
||||
setData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
|
||||
const updateFinancials = (period: keyof CIMReviewData['financialSummary']['financials'], field: string, value: string) => {
|
||||
setData(prev => ({
|
||||
@@ -251,7 +250,7 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
|
||||
const updateNestedField = (newValue: string) => {
|
||||
setData(prev => {
|
||||
const newData = { ...prev };
|
||||
let current = newData;
|
||||
let current: any = newData;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
if (!current[path[i]]) {
|
||||
current[path[i]] = {};
|
||||
@@ -299,20 +298,7 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to safely get field values with proper nested structure handling
|
||||
const getFieldValue = (obj: any, field: keyof CIMReviewData): string => {
|
||||
const value = obj[field];
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// For nested objects, we need to handle them specifically
|
||||
// This function should only be called for top-level fields that are strings
|
||||
// For nested objects, we should use specific accessors
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
// Helper function to get nested field values
|
||||
const getNestedFieldValue = (obj: any, path: string[]): string => {
|
||||
|
||||
@@ -9,13 +9,14 @@ interface LoginFormProps {
|
||||
}
|
||||
|
||||
export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess }) => {
|
||||
const { login, isLoading, error } = useAuth();
|
||||
const { login, register, isLoading, error } = useAuth();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState<{ email?: string; password?: string }>({});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isRegisterMode, setIsRegisterMode] = useState(false);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -47,11 +48,15 @@ export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
if (isRegisterMode) {
|
||||
await register(formData);
|
||||
} else {
|
||||
await login(formData);
|
||||
}
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
// Error is handled by the auth context
|
||||
console.error('Login failed:', error);
|
||||
console.error(isRegisterMode ? 'Registration failed:' : 'Login failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,15 +155,29 @@ export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess }) => {
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Signing in...
|
||||
{isRegisterMode ? 'Creating Account...' : 'Signing in...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="h-4 w-4 mr-2" />
|
||||
Sign In
|
||||
{isRegisterMode ? 'Create Account' : 'Sign In'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Toggle between login and register */}
|
||||
<div className="text-center mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsRegisterMode(!isRegisterMode)}
|
||||
className="text-sm text-primary-600 hover:text-primary-500 focus:outline-none focus:underline"
|
||||
>
|
||||
{isRegisterMode
|
||||
? 'Already have an account? Sign in'
|
||||
: "Don't have an account? Create one"
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
18
frontend/src/config/firebase.ts
Normal file
18
frontend/src/config/firebase.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: import.meta.env.VITE_FIREBASE_APP_ID
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
|
||||
// Initialize Firebase Authentication and get a reference to the service
|
||||
export const auth = getAuth(app);
|
||||
export default app;
|
||||
@@ -16,36 +16,33 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize auth state from localStorage and validate token
|
||||
const initializeAuth = async () => {
|
||||
try {
|
||||
const storedToken = authService.getToken();
|
||||
const storedUser = authService.getCurrentUser();
|
||||
setIsLoading(true);
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
// Validate token with backend
|
||||
const validatedUser = await authService.validateToken();
|
||||
if (validatedUser) {
|
||||
setUser(validatedUser);
|
||||
setToken(storedToken);
|
||||
// Listen for Firebase auth state changes
|
||||
const unsubscribe = authService.onAuthStateChanged(async (firebaseUser) => {
|
||||
try {
|
||||
if (firebaseUser) {
|
||||
const user = authService.getCurrentUser();
|
||||
const token = await authService.getToken();
|
||||
setUser(user);
|
||||
setToken(token);
|
||||
} else {
|
||||
// Token is invalid, clear everything
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error);
|
||||
setError('Failed to initialize authentication');
|
||||
console.error('Auth state change error:', error);
|
||||
setError('Authentication error occurred');
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
initializeAuth();
|
||||
// Cleanup subscription on unmount
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const login = async (credentials: LoginCredentials): Promise<void> => {
|
||||
@@ -65,6 +62,23 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (credentials: LoginCredentials): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const authResult = await authService.register(credentials);
|
||||
setUser(authResult.user);
|
||||
setToken(authResult.token);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Registration failed';
|
||||
setError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
@@ -85,6 +99,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
@@ -1,115 +1,164 @@
|
||||
import {
|
||||
signInWithEmailAndPassword,
|
||||
createUserWithEmailAndPassword,
|
||||
signOut,
|
||||
onAuthStateChanged,
|
||||
User as FirebaseUser,
|
||||
getIdToken
|
||||
} from 'firebase/auth';
|
||||
import axios from 'axios';
|
||||
import { config } from '../config/env';
|
||||
import { auth } from '../config/firebase';
|
||||
import { LoginCredentials, AuthResult, User } from '../types/auth';
|
||||
|
||||
const API_BASE_URL = config.apiBaseUrl;
|
||||
|
||||
class AuthService {
|
||||
private token: string | null = null;
|
||||
private currentUser: FirebaseUser | null = null;
|
||||
private authStateListeners: Array<(user: FirebaseUser | null) => void> = [];
|
||||
|
||||
constructor() {
|
||||
// Initialize token from localStorage
|
||||
this.token = localStorage.getItem('auth_token');
|
||||
if (this.token) {
|
||||
this.setAuthHeader(this.token);
|
||||
}
|
||||
// Listen for auth state changes
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
this.currentUser = user;
|
||||
this.updateAxiosHeaders(user);
|
||||
// Notify all listeners
|
||||
this.authStateListeners.forEach(listener => listener(user));
|
||||
});
|
||||
}
|
||||
|
||||
private setAuthHeader(token: string) {
|
||||
private async updateAxiosHeaders(user: FirebaseUser | null) {
|
||||
if (user) {
|
||||
try {
|
||||
const token = await getIdToken(user);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to get ID token:', error);
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
}
|
||||
} else {
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
}
|
||||
}
|
||||
|
||||
private removeAuthHeader() {
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
onAuthStateChanged(callback: (user: FirebaseUser | null) => void) {
|
||||
this.authStateListeners.push(callback);
|
||||
// Immediately call with current state
|
||||
callback(this.currentUser);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
this.authStateListeners = this.authStateListeners.filter(listener => listener !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
async login(credentials: LoginCredentials): Promise<AuthResult> {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials);
|
||||
const authResult = response.data;
|
||||
const userCredential = await signInWithEmailAndPassword(
|
||||
auth,
|
||||
credentials.email,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
if (!authResult.success) {
|
||||
throw new Error(authResult.message || 'Login failed');
|
||||
}
|
||||
|
||||
// Extract data from the response structure
|
||||
const { user, tokens } = authResult.data;
|
||||
const accessToken = tokens.accessToken;
|
||||
const refreshToken = tokens.refreshToken;
|
||||
|
||||
// Store token and set auth header
|
||||
this.token = accessToken;
|
||||
localStorage.setItem('auth_token', accessToken);
|
||||
localStorage.setItem('refresh_token', refreshToken);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
|
||||
this.setAuthHeader(accessToken);
|
||||
const user = userCredential.user;
|
||||
const token = await getIdToken(user);
|
||||
|
||||
return {
|
||||
user,
|
||||
token: accessToken,
|
||||
refreshToken,
|
||||
expiresIn: tokens.expiresIn
|
||||
user: {
|
||||
id: user.uid,
|
||||
email: user.email!,
|
||||
name: user.displayName || user.email!.split('@')[0]
|
||||
},
|
||||
token,
|
||||
refreshToken: user.refreshToken,
|
||||
expiresIn: 3600 // Firebase tokens typically expire in 1 hour
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(error.response?.data?.message || 'Login failed');
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message || 'Login failed');
|
||||
}
|
||||
throw new Error('An unexpected error occurred');
|
||||
}
|
||||
|
||||
async register(credentials: LoginCredentials & { name?: string }): Promise<AuthResult> {
|
||||
try {
|
||||
const userCredential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
credentials.email,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
const user = userCredential.user;
|
||||
const token = await getIdToken(user);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.uid,
|
||||
email: user.email!,
|
||||
name: credentials.name || user.email!.split('@')[0]
|
||||
},
|
||||
token,
|
||||
refreshToken: user.refreshToken,
|
||||
expiresIn: 3600
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message || 'Registration failed');
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
if (this.token) {
|
||||
await axios.post(`${API_BASE_URL}/auth/logout`);
|
||||
}
|
||||
await signOut(auth);
|
||||
} catch (error) {
|
||||
// Continue with logout even if API call fails
|
||||
console.error('Logout API call failed:', error);
|
||||
} finally {
|
||||
// Clear local storage and auth header
|
||||
this.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user');
|
||||
this.removeAuthHeader();
|
||||
console.error('Logout failed:', error);
|
||||
throw new Error('Logout failed');
|
||||
}
|
||||
}
|
||||
|
||||
async validateToken(): Promise<User | null> {
|
||||
if (!this.token) {
|
||||
if (!this.currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/auth/profile`);
|
||||
return response.data.success ? response.data.data.user : null;
|
||||
// Firebase handles token validation automatically
|
||||
// Just return the current user info
|
||||
return {
|
||||
id: this.currentUser.uid,
|
||||
email: this.currentUser.email!,
|
||||
name: this.currentUser.displayName || this.currentUser.email!.split('@')[0]
|
||||
};
|
||||
} catch (error) {
|
||||
// Token is invalid, clear it
|
||||
this.logout();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!this.currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return this.token;
|
||||
return {
|
||||
id: this.currentUser.uid,
|
||||
email: this.currentUser.email!,
|
||||
name: this.currentUser.displayName || this.currentUser.email!.split('@')[0]
|
||||
};
|
||||
}
|
||||
|
||||
async getToken(): Promise<string | null> {
|
||||
if (!this.currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await getIdToken(this.currentUser);
|
||||
} catch (error) {
|
||||
console.error('Failed to get ID token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.token;
|
||||
return !!this.currentUser;
|
||||
}
|
||||
|
||||
getFirebaseUser(): FirebaseUser | null {
|
||||
return this.currentUser;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'user' | 'admin';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
role?: 'user' | 'admin';
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface LoginCredentials {
|
||||
@@ -16,13 +16,14 @@ export interface AuthResult {
|
||||
user: User;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
expiresIn?: string;
|
||||
expiresIn?: number;
|
||||
}
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (credentials: LoginCredentials) => Promise<void>;
|
||||
register: (credentials: LoginCredentials) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
Reference in New Issue
Block a user