Skip to content
Closed
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
4 changes: 4 additions & 0 deletions backend/api/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class Settings(BaseSettings):
APPLE_SHARED_SECRET: str = "" # App Store Connect shared secret
GOOGLE_SERVICE_ACCOUNT_JSON: str = "" # JSON string of service account key

# OAuth
GOOGLE_CLIENT_ID: str = ""
GOOGLE_CLIENT_SECRET: str = ""

# Feature flags
ENABLE_AI_INSIGHTS: bool = True

Expand Down
77 changes: 76 additions & 1 deletion backend/api/app/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from sqlalchemy import select
from ..database import get_db
from ..models import User
from ..schemas.user import UserCreate, UserLogin, UserUpdate, UserResponse, TokenResponse, ForgotPasswordRequest
from ..schemas.user import UserCreate, UserLogin, UserUpdate, UserResponse, TokenResponse, ForgotPasswordRequest, GoogleLoginRequest
from ..services.auth import hash_password, verify_password, create_access_token, get_current_user
from ..config import get_settings
import uuid
import httpx
from datetime import datetime, timedelta

router = APIRouter(prefix="/api/auth", tags=["auth"])
Expand Down Expand Up @@ -51,6 +53,79 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
)


@router.post("/google", response_model=TokenResponse)
async def google_login(login_data: GoogleLoginRequest, db: AsyncSession = Depends(get_db)):
"""Login with Google."""
settings = get_settings()

# Exchange code for tokens
async with httpx.AsyncClient() as client:
token_response = await client.post(
"https://oauth2.googleapis.com/token",
data={
"code": login_data.code,
"client_id": settings.GOOGLE_CLIENT_ID,
"client_secret": settings.GOOGLE_CLIENT_SECRET,
"redirect_uri": login_data.redirect_uri or "postmessage",
"grant_type": "authorization_code",
},
)

if token_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Google credentials",
)

token_data = token_response.json()
access_token = token_data.get("access_token")

# Get user info
user_info_response = await client.get(
"https://www.googleapis.com/oauth2/v2/userinfo",
headers={"Authorization": f"Bearer {access_token}"},
)

if user_info_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Failed to get user info from Google",
)

user_info = user_info_response.json()
email = user_info.get("email")

if not email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Google account must have an email",
)

# Find or create user
result = await db.execute(select(User).where(User.email == email))
user = result.scalar_one_or_none()

if not user:
user = User(
email=email,
full_name=user_info.get("name"),
platform="web",
is_active=True,
is_verified=True,
)
db.add(user)
await db.commit()
await db.refresh(user)

# Create access token
access_token = create_access_token(data={"sub": str(user.id)})

return TokenResponse(
access_token=access_token,
user=UserResponse.model_validate(user)
)


@router.post("/forgot-password", status_code=status.HTTP_200_OK)
async def forgot_password(
request: ForgotPasswordRequest,
Expand Down
8 changes: 7 additions & 1 deletion backend/api/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class UserCreate(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8, max_length=100)
full_name: Optional[str] = Field(None, max_length=255)
platform: str = Field(..., pattern="^(ios|android)$")
platform: str = Field(..., pattern="^(ios|android|web)$")
device_token: Optional[str] = None

@field_validator("password")
Expand All @@ -44,6 +44,12 @@ class UserLogin(BaseModel):
password: str


class GoogleLoginRequest(BaseModel):
"""Schema for Google login."""
code: str
redirect_uri: Optional[str] = None


class ForgotPasswordRequest(BaseModel):
"""Schema for forgot password request."""
email: EmailStr
Expand Down
1 change: 1 addition & 0 deletions web-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ <h3>Preferences</h3>
<!-- Scripts -->
<script src="js/api.js?v=3.0"></script>
<script src="js/app.js?v=3.0"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</body>

</html>
13 changes: 12 additions & 1 deletion web-demo/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class APIClient {

async request(endpoint, options = {}) {
// Use mock data for demo mode
if (this.useMockData) {
if (this.useMockData && !options.forceReal) {
return this.getMockData(endpoint);
}

Expand Down Expand Up @@ -180,6 +180,17 @@ class APIClient {
return response;
}

async googleLogin(code) {
const response = await this.request('/api/auth/google', {
method: 'POST',
auth: false,
body: JSON.stringify({ code }),
forceReal: true
});
this.setToken(response.access_token);
return response;
}

async getCurrentUser() {
return await this.request('/users/me');
}
Expand Down
56 changes: 26 additions & 30 deletions web-demo/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,39 +180,35 @@ class ClimaAI {
}

async handleGoogleSignIn() {
if (typeof google === 'undefined' || !google.accounts) {
this.showToast('Google Sign-In script not loaded', 'error');
return;
}

try {
this.showToast('🔐 Signing in with Google...', 'info');

// In production, this would trigger Google OAuth flow:
// 1. Redirect to Google OAuth consent screen
// 2. User grants permissions
// 3. Google redirects back with authorization code
// 4. Backend exchanges code for tokens
// 5. Backend creates/updates user and returns JWT

// For demo purposes, we'll simulate successful OAuth with demo account
setTimeout(async () => {
try {
// Auto-login with demo account
const response = await api.login('demo@climaai.com', 'Test1234');
this.user = response.user;
this.showToast('✅ Welcome! Signed in with Google', 'success');
this.showScreen('homeScreen');
this.loadWeatherData();
this.checkSubscription();
} catch (error) {
this.showToast('Google Sign-In succeeded! Welcome!', 'success');
// Create a demo user object
this.user = {
email: 'google-user@gmail.com',
full_name: 'Google User',
is_premium: true
};
this.isPremium = true;
this.showScreen('homeScreen');
this.loadWeatherData();
}
}, 1500); // Simulate OAuth redirect delay
const client = google.accounts.oauth2.initCodeClient({
client_id: 'YOUR_GOOGLE_CLIENT_ID_HERE',
scope: 'email profile',
ux_mode: 'popup',
callback: async (response) => {
if (response.code) {
try {
const apiResponse = await api.googleLogin(response.code);
this.user = apiResponse.user;
this.showToast('✅ Welcome! Signed in with Google', 'success');
this.showScreen('homeScreen');
this.loadWeatherData();
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Google Sign-In failed', 'error');
}
}
},
});

client.requestCode();

} catch (error) {
this.showToast(error.message || 'Google Sign-In failed', 'error');
Expand Down