Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ firebase-debug.log
.firebase/
serviceAccountKey.json
firebase-admin-sdk.json
firebase-service-account.json

# Jupyter Notebooks
.ipynb_checkpoints/
Expand Down
145 changes: 142 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Dict
from typing import Dict, Optional
import firebase_admin
from firebase_admin import credentials, auth as firebase_auth
from sqlalchemy import create_engine, Column, String, DateTime, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import sessionmaker, declarative_base
from datetime import datetime
import uuid
import os

# Initialize Firebase Admin SDK
try:
firebase_admin.get_app()
except ValueError:
# Initialize with service account credentials
cred = credentials.Certificate('firebase-service-account.json')
firebase_admin.initialize_app(cred)

# Database setup
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://app_user:app_password@postgres:5432/app_db")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# User model
class User(Base):
__tablename__ = "user"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
firebase_uid = Column(String(128), unique=True, nullable=False)
email = Column(String(254), nullable=True)
display_name = Column(String(100), nullable=True)
created_at = Column(DateTime, server_default=text('CURRENT_TIMESTAMP'))

app = FastAPI(title="Backend API", version="1.0.0")

Expand All @@ -18,6 +50,16 @@ class HealthResponse(BaseModel):
message: str
service: str

class UserInitRequest(BaseModel):
id_token: str

class UserResponse(BaseModel):
id: str
firebase_uid: str
email: Optional[str]
display_name: Optional[str]
created_at: str

@app.get("/")
async def root() -> Dict[str, str]:
return {"message": "Backend is running!"}
Expand All @@ -28,4 +70,101 @@ async def health_check() -> HealthResponse:
status="connected",
message="Backend is operational",
service="FastAPI Backend"
)
)

@app.get("/users/me", response_model=UserResponse)
async def get_current_user(authorization: Optional[str] = Header(None)):
"""
Get the current user's data from the database using their Firebase ID token.
Expects: Authorization: Bearer <firebase_id_token>
"""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")

id_token = authorization.split("Bearer ")[1]
db = SessionLocal()

try:
# Verify the Firebase ID token
try:
decoded_token = firebase_auth.verify_id_token(id_token)
firebase_uid = decoded_token['uid']
except Exception as e:
raise HTTPException(status_code=401, detail=f"Invalid Firebase token: {str(e)}")

# Fetch user from database
user = db.query(User).filter(User.firebase_uid == firebase_uid).first()

if not user:
raise HTTPException(status_code=404, detail="User not found in database")

return UserResponse(
id=str(user.id),
firebase_uid=user.firebase_uid,
email=user.email,
display_name=user.display_name,
created_at=user.created_at.isoformat() if user.created_at else ""
)

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to fetch user: {str(e)}")
finally:
db.close()

@app.post("/users/init", response_model=UserResponse)
async def initialize_user(request: UserInitRequest):
"""
Initialize a new user in the database after Firebase signup.
Verifies the Firebase ID token and creates a user record in Postgres.
"""
db = SessionLocal()
try:
# Verify the Firebase ID token
try:
decoded_token = firebase_auth.verify_id_token(request.id_token)
firebase_uid = decoded_token['uid']
email = decoded_token.get('email')
except Exception as e:
raise HTTPException(status_code=401, detail=f"Invalid Firebase token: {str(e)}")

# Check if user already exists
existing_user = db.query(User).filter(User.firebase_uid == firebase_uid).first()
if existing_user:
# User already exists, return existing user data
return UserResponse(
id=str(existing_user.id),
firebase_uid=existing_user.firebase_uid,
email=existing_user.email,
display_name=existing_user.display_name,
created_at=existing_user.created_at.isoformat() if existing_user.created_at else ""
)

# Create new user in database
new_user = User(
firebase_uid=firebase_uid,
email=email,
display_name=email.split('@')[0] if email else None # Use email prefix as default display name
)

db.add(new_user)
db.commit()
db.refresh(new_user)

return UserResponse(
id=str(new_user.id),
firebase_uid=new_user.firebase_uid,
email=new_user.email,
display_name=new_user.display_name,
created_at=new_user.created_at.isoformat() if new_user.created_at else ""
)

except HTTPException:
db.rollback()
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Failed to create user: {str(e)}")
finally:
db.close()
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ services:
postgres:
image: postgres:17-alpine
ports:
- "5432:5432"
- "5433:5432"
environment:
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=app_password
Expand Down
158 changes: 111 additions & 47 deletions frontend/App.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,126 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import LoginScreen from './src/components/LoginScreen';
import SignupScreen from './src/components/SignupScreen';
import DashboardScreen from './src/components/DashboardScreen';
import { signUpUser, signInUser, resetPassword, logoutUser } from './src/services/authService';

const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000';
export type RootStackParamList = {
Login: undefined;
Signup: undefined;
Dashboard: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
const [connectionStatus, setConnectionStatus] = useState<string>('');
const [isConnected, setIsConnected] = useState<boolean>(false);
const handleLogin = async (email: string, password: string) => {
try {
// Sign in user with Firebase and fetch from database
const userData = await signInUser(email, password);
console.log('User logged in successfully:', userData);

// Navigation will happen automatically after successful login
} catch (error: any) {
// Error is already formatted by authService
throw error;
}
};

useEffect(() => {
checkBackendConnection();
}, []);
const handleSignup = async (email: string, password: string) => {
try {
// Sign up user with Firebase and initialize in database
const userData = await signUpUser(email, password);
console.log('User signed up successfully:', userData);

// Navigation will happen automatically after successful signup
// The user is already auto-logged in by Firebase's createUserWithEmailAndPassword
} catch (error: any) {
// Error is already formatted by authService
throw error;
}
};

const checkBackendConnection = async () => {
const handlePasswordReset = async (email: string) => {
try {
const response = await axios.get(`${API_URL}/api/health`);
if (response.data.status === 'connected') {
setConnectionStatus('You are connected to the backend!');
setIsConnected(true);
await resetPassword(email);
} catch (error: any) {
// Error is already formatted by authService
throw error;
}
};

const handleLogout = async () => {
try {
await logoutUser();
console.log('User logged out successfully');
// Navigate to login after logout
if (navigationRef.current) {
navigationRef.current.reset({
index: 0,
routes: [{ name: 'Login' }],
});
}
} catch (error) {
setConnectionStatus('Unable to connect to backend');
setIsConnected(false);
} catch (error: any) {
console.error('Logout failed:', error);
throw error;
}
};

const navigationRef = React.useRef<any>(null);

return (
<View style={styles.container}>
<Text style={styles.title}>Hello World</Text>
<Text style={[styles.status, isConnected ? styles.connected : styles.disconnected]}>
{connectionStatus}
</Text>
<NavigationContainer ref={navigationRef}>
<Stack.Navigator
initialRouteName="Login"
screenOptions={{
headerStyle: {
backgroundColor: '#007AFF',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen
name="Login"
options={{ title: 'Sign In' }}
>
{(props) => (
<LoginScreen
{...props}
onLogin={handleLogin}
onPasswordReset={handlePasswordReset}
/>
)}
</Stack.Screen>

<Stack.Screen
name="Signup"
options={{ title: 'Sign Up' }}
>
{(props) => <SignupScreen {...props} onSignup={handleSignup} />}
</Stack.Screen>

<Stack.Screen
name="Dashboard"
options={{
title: 'Dashboard',
headerBackVisible: false,
headerRight: () => (
<TouchableOpacity onPress={handleLogout} style={{ marginRight: 10 }}>
<Text style={{ color: '#fff', fontSize: 16 }}>Log Out</Text>
</TouchableOpacity>
),
}}
component={DashboardScreen}
/>
</Stack.Navigator>
<StatusBar style="auto" />
</View>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 20,
},
status: {
fontSize: 18,
marginTop: 10,
},
connected: {
color: 'green',
},
disconnected: {
color: 'red',
},
});
Loading