Skip to content

Commit 52939d4

Browse files
added oauth in frontend
1 parent 00f301a commit 52939d4

7 files changed

Lines changed: 446 additions & 7 deletions

File tree

backend/main.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from fastapi import FastAPI, HTTPException
2+
from fastapi.middleware.cors import CORSMiddleware
3+
from pydantic import BaseModel
4+
from google.oauth2 import id_token
5+
from google.auth.transport import requests
6+
7+
app = FastAPI(title="FocusForge API")
8+
9+
app.add_middleware(
10+
CORSMiddleware,
11+
allow_origins=["*"],
12+
allow_credentials=True,
13+
allow_methods=["*"],
14+
allow_headers=["*"],
15+
)
16+
17+
GOOGLE_CLIENT_ID = "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com" # TODO: replace
18+
19+
20+
class GoogleTokenRequest(BaseModel):
21+
id_token: str
22+
23+
24+
@app.post("/auth/google")
25+
async def google_auth(payload: GoogleTokenRequest):
26+
"""Verify Google ID token and return user info."""
27+
try:
28+
idinfo = id_token.verify_oauth2_token(
29+
payload.id_token,
30+
requests.Request(),
31+
GOOGLE_CLIENT_ID,
32+
)
33+
34+
# Token is valid — extract user info
35+
return {
36+
"email": idinfo.get("email"),
37+
"name": idinfo.get("name"),
38+
"photo": idinfo.get("picture"),
39+
"google_id": idinfo.get("sub"),
40+
"verified": idinfo.get("email_verified", False),
41+
}
42+
except ValueError as e:
43+
raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")
44+
45+
46+
@app.get("/health")
47+
async def health():
48+
return {"status": "ok"}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:flutter/material.dart';
2+
import 'services/auth_service.dart';
3+
4+
class Dashboard extends StatelessWidget {
5+
const Dashboard({super.key});
6+
7+
@override
8+
Widget build(BuildContext context) {
9+
final user = AuthService.currentUser;
10+
11+
return Scaffold(
12+
appBar: AppBar(
13+
title: const Text('FocusForge'),
14+
actions: [
15+
if (user?.photoUrl != null)
16+
Padding(
17+
padding: const EdgeInsets.only(right: 8.0),
18+
child: CircleAvatar(
19+
backgroundImage: NetworkImage(user!.photoUrl!),
20+
radius: 16,
21+
),
22+
),
23+
IconButton(
24+
icon: const Icon(Icons.logout),
25+
tooltip: 'Sign out',
26+
onPressed: () async {
27+
await AuthService.signOut();
28+
if (context.mounted) {
29+
Navigator.of(context).pushReplacementNamed('/login');
30+
}
31+
},
32+
),
33+
],
34+
),
35+
body: Center(
36+
child: Column(
37+
mainAxisAlignment: MainAxisAlignment.center,
38+
children: [
39+
Text(
40+
'Welcome, ${user?.displayName ?? 'User'}!',
41+
style: Theme.of(context).textTheme.headlineMedium,
42+
),
43+
const SizedBox(height: 8),
44+
Text(
45+
user?.email ?? '',
46+
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
47+
color: Colors.grey,
48+
),
49+
),
50+
],
51+
),
52+
),
53+
);
54+
}
55+
}

flutter-frontend/Focusforge/lib/main.dart

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import 'package:flutter/material.dart';
2+
import 'screens/login_screen.dart';
3+
import 'Dashboard.dart';
4+
import 'services/auth_service.dart';
25

36
void main() {
47
runApp(const FocusFlowApp());
@@ -10,14 +13,19 @@ class FocusFlowApp extends StatelessWidget {
1013
@override
1114
Widget build(BuildContext context) {
1215
return MaterialApp(
13-
title: 'FocusFlow',
16+
title: 'FocusForge',
17+
debugShowCheckedModeBanner: false,
1418
theme: ThemeData(
1519
colorScheme: ColorScheme.fromSeed(
16-
seedColor: const Color(0xFF2c3e50), // Deep blue
20+
seedColor: const Color(0xFF2c3e50),
1721
brightness: Brightness.light,
1822
),
1923
useMaterial3: true,
2024
),
25+
routes: {
26+
'/login': (_) => const LoginScreen(),
27+
'/dashboard': (_) => const Dashboard(),
28+
},
2129
home: const SplashScreenPage(),
2230
);
2331
}
@@ -54,11 +62,14 @@ class _SplashScreenPageState extends State<SplashScreenPage>
5462

5563
_animationController.forward();
5664

57-
// Simulate app initialization, then navigate
58-
Future.delayed(const Duration(seconds: 3), () {
65+
// Try silent sign-in, then navigate
66+
Future.delayed(const Duration(seconds: 2), () async {
67+
if (!mounted) return;
68+
final account = await AuthService.silentSignIn();
5969
if (mounted) {
60-
// Replace with your home screen route
61-
Navigator.of(context).pushReplacementNamed('/home');
70+
Navigator.of(context).pushReplacementNamed(
71+
account != null ? '/dashboard' : '/login',
72+
);
6273
}
6374
});
6475
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import 'package:flutter/material.dart';
2+
import '../services/auth_service.dart';
3+
4+
class LoginScreen extends StatefulWidget {
5+
const LoginScreen({super.key});
6+
7+
@override
8+
State<LoginScreen> createState() => _LoginScreenState();
9+
}
10+
11+
class _LoginScreenState extends State<LoginScreen> {
12+
bool _isLoading = false;
13+
14+
Future<void> _handleGoogleSignIn() async {
15+
setState(() => _isLoading = true);
16+
try {
17+
final result = await AuthService.signInWithGoogle();
18+
if (result != null && mounted) {
19+
Navigator.of(context).pushReplacementNamed('/dashboard');
20+
}
21+
} catch (e) {
22+
if (mounted) {
23+
ScaffoldMessenger.of(context).showSnackBar(
24+
SnackBar(
25+
content: Text('Sign in failed: ${e.toString()}'),
26+
backgroundColor: Colors.red.shade700,
27+
),
28+
);
29+
}
30+
} finally {
31+
if (mounted) setState(() => _isLoading = false);
32+
}
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
return Scaffold(
38+
body: Container(
39+
decoration: const BoxDecoration(
40+
gradient: LinearGradient(
41+
begin: Alignment.topCenter,
42+
end: Alignment.bottomCenter,
43+
colors: [Color(0xFF2c3e50), Color(0xFF34495e)],
44+
),
45+
),
46+
child: SafeArea(
47+
child: Center(
48+
child: Padding(
49+
padding: const EdgeInsets.symmetric(horizontal: 32.0),
50+
child: Column(
51+
mainAxisAlignment: MainAxisAlignment.center,
52+
children: [
53+
// Logo
54+
Container(
55+
width: 100,
56+
height: 100,
57+
decoration: BoxDecoration(
58+
color: Colors.white.withOpacity(0.1),
59+
shape: BoxShape.circle,
60+
border: Border.all(
61+
color: const Color(0xFF3498db),
62+
width: 2,
63+
),
64+
),
65+
child: const Center(
66+
child: Icon(
67+
Icons.local_fire_department_rounded,
68+
size: 50,
69+
color: Color(0xFF3498db),
70+
),
71+
),
72+
),
73+
const SizedBox(height: 32),
74+
75+
// Title
76+
const Text(
77+
'FocusForge',
78+
style: TextStyle(
79+
fontSize: 32,
80+
fontWeight: FontWeight.w700,
81+
color: Colors.white,
82+
letterSpacing: 1.2,
83+
),
84+
),
85+
const SizedBox(height: 8),
86+
87+
// Tagline
88+
Text(
89+
'Focus intentionally. Achieve more.',
90+
style: TextStyle(
91+
fontSize: 16,
92+
color: Colors.white.withOpacity(0.7),
93+
letterSpacing: 0.5,
94+
),
95+
),
96+
const SizedBox(height: 64),
97+
98+
// Google Sign-In Button
99+
SizedBox(
100+
width: double.infinity,
101+
height: 52,
102+
child: ElevatedButton(
103+
onPressed: _isLoading ? null : _handleGoogleSignIn,
104+
style: ElevatedButton.styleFrom(
105+
backgroundColor: Colors.white,
106+
foregroundColor: const Color(0xFF2c3e50),
107+
shape: RoundedRectangleBorder(
108+
borderRadius: BorderRadius.circular(12),
109+
),
110+
elevation: 2,
111+
),
112+
child: _isLoading
113+
? const SizedBox(
114+
width: 24,
115+
height: 24,
116+
child: CircularProgressIndicator(
117+
strokeWidth: 2.5,
118+
color: Color(0xFF2c3e50),
119+
),
120+
)
121+
: Row(
122+
mainAxisAlignment: MainAxisAlignment.center,
123+
children: [
124+
Image.network(
125+
'https://www.google.com/favicon.ico',
126+
width: 24,
127+
height: 24,
128+
errorBuilder: (_, __, ___) => const Icon(
129+
Icons.g_mobiledata,
130+
size: 28,
131+
color: Color(0xFF4285F4),
132+
),
133+
),
134+
const SizedBox(width: 12),
135+
const Text(
136+
'Continue with Google',
137+
style: TextStyle(
138+
fontSize: 16,
139+
fontWeight: FontWeight.w600,
140+
),
141+
),
142+
],
143+
),
144+
),
145+
),
146+
],
147+
),
148+
),
149+
),
150+
),
151+
),
152+
);
153+
}
154+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'dart:convert';
2+
import 'package:google_sign_in/google_sign_in.dart';
3+
import 'package:http/http.dart' as http;
4+
5+
class AuthService {
6+
static const String _backendUrl = 'http://10.0.2.2:8000'; // Android emulator → localhost
7+
// For physical device, use your machine's LAN IP e.g. 'http://192.168.x.x:8000'
8+
9+
static final GoogleSignIn _googleSignIn = GoogleSignIn(
10+
scopes: ['email', 'profile'],
11+
);
12+
13+
static GoogleSignInAccount? currentUser;
14+
15+
/// Sign in with Google and send ID token to FastAPI backend
16+
static Future<Map<String, dynamic>?> signInWithGoogle() async {
17+
try {
18+
final GoogleSignInAccount? account = await _googleSignIn.signIn();
19+
if (account == null) return null; // User cancelled
20+
21+
final GoogleSignInAuthentication auth = await account.authentication;
22+
final String? idToken = auth.idToken;
23+
24+
if (idToken == null) {
25+
throw Exception('Failed to get ID token');
26+
}
27+
28+
// Send token to FastAPI backend for verification
29+
final response = await http.post(
30+
Uri.parse('$_backendUrl/auth/google'),
31+
headers: {'Content-Type': 'application/json'},
32+
body: jsonEncode({'id_token': idToken}),
33+
);
34+
35+
if (response.statusCode == 200) {
36+
currentUser = account;
37+
return jsonDecode(response.body);
38+
} else {
39+
throw Exception('Backend auth failed: ${response.body}');
40+
}
41+
} catch (e) {
42+
// If backend is not available, still allow sign-in for development
43+
final GoogleSignInAccount? account = _googleSignIn.currentUser;
44+
if (account != null) {
45+
currentUser = account;
46+
return {
47+
'email': account.email,
48+
'name': account.displayName,
49+
'photo': account.photoUrl,
50+
'offline': true,
51+
};
52+
}
53+
rethrow;
54+
}
55+
}
56+
57+
/// Sign out
58+
static Future<void> signOut() async {
59+
await _googleSignIn.signOut();
60+
currentUser = null;
61+
}
62+
63+
/// Check if already signed in
64+
static Future<GoogleSignInAccount?> silentSignIn() async {
65+
try {
66+
final account = await _googleSignIn.signInSilently();
67+
currentUser = account;
68+
return account;
69+
} catch (_) {
70+
return null;
71+
}
72+
}
73+
74+
/// Whether user is signed in
75+
static bool get isSignedIn => currentUser != null;
76+
}

0 commit comments

Comments
 (0)