diff --git a/game/ios/Runner/AppDelegate.swift b/game/ios/Runner/AppDelegate.swift index 1d7f1447..1807eea4 100644 --- a/game/ios/Runner/AppDelegate.swift +++ b/game/ios/Runner/AppDelegate.swift @@ -4,7 +4,7 @@ import Firebase import GoogleMaps import flutter_config -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/game/lib/api/game_api.dart b/game/lib/api/game_api.dart index 89171828..1c0c846c 100644 --- a/game/lib/api/game_api.dart +++ b/game/lib/api/game_api.dart @@ -211,13 +211,14 @@ class ApiClient extends ChangeNotifier { _createSocket(false); return loginResponse; } else { - print(loginResponse.body); - } - authenticated = false; - notifyListeners(); + print("LoginResponse:" + loginResponse.body); - print("Failed to connect to server!"); - return null; + authenticated = false; + notifyListeners(); + + print("Failed to connect to server!"); + return null; + } /* } print("Failed to get location data!"); @@ -243,4 +244,24 @@ class ApiClient extends ChangeNotifier { authenticated = false; notifyListeners(); } + + Future checkUserExists(String idToken) async { + try { + final uri = _googleLoginUrl.replace( + path: '${_googleLoginUrl.path}/check-user', + queryParameters: {'idToken': idToken}, + ); + final response = await http.get(uri); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + return responseData['exists']; + } + print('Failed to check user. Status code: ${response.statusCode}'); + return false; + } catch (e) { + print('Error occurred while checking user: $e'); + return false; + } + } } diff --git a/game/lib/api/geopoint.dart b/game/lib/api/geopoint.dart index c5eafb62..43f1dadc 100644 --- a/game/lib/api/geopoint.dart +++ b/game/lib/api/geopoint.dart @@ -48,14 +48,16 @@ class GeoPoint { return Future.error( 'Location permissions are permanently denied, we cannot request permissions.'); } + print("Getting location"); final pos = await Geolocator.getCurrentPosition( // Ideally we would use best accuracy, but it doesn't work for some reason // desiredAccuracy: LocationAccuracy.best desiredAccuracy: LocationAccuracy.medium); + print("Got location: ${pos.latitude}, ${pos.longitude}"); return GeoPoint(pos.latitude, pos.longitude, pos.heading); } catch (e) { print(e); - return Future.error(e.toString()); + return Future.error("Error:" + e.toString()); } } diff --git a/game/lib/interests/interests_page.dart b/game/lib/interests/interests_page.dart index f555c205..026f8323 100644 --- a/game/lib/interests/interests_page.dart +++ b/game/lib/interests/interests_page.dart @@ -5,6 +5,8 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:game/utils/utility_functions.dart'; import 'package:flutter_svg/flutter_svg.dart'; +/// This page allows the user to select their interests. Connection to backend +/// (creating the user) happens here. class InterestsPageWidget extends StatefulWidget { InterestsPageWidget( {Key? key, diff --git a/game/lib/register_page/register_page.dart b/game/lib/register_page/register_page.dart index 94ce5fa6..308bd209 100644 --- a/game/lib/register_page/register_page.dart +++ b/game/lib/register_page/register_page.dart @@ -4,6 +4,10 @@ import 'package:game/details_page/details_page.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter_svg/flutter_svg.dart'; +/// If the google-login user is not registered, this page will be displayed. +/// The user will be asked to select their enrollment type. The user is created +/// only when they fill all required information (at registerpage -> details_page +/// -> interests_page). class RegisterPageWidget extends StatefulWidget { final GoogleSignInAccount? user; final String? idToken; diff --git a/game/lib/splash_page/splash_page.dart b/game/lib/splash_page/splash_page.dart index 10758960..eaca812b 100644 --- a/game/lib/splash_page/splash_page.dart +++ b/game/lib/splash_page/splash_page.dart @@ -79,11 +79,27 @@ class SplashPageWidget extends StatelessWidget { return; } - final gRelogResult = - await apiClient.connectGoogleNoRegister(account); + final auth = await account.authentication; + final idToken = auth.idToken; - if (gRelogResult != null) { - return; + bool userExists = + await apiClient.checkUserExists(idToken ?? ""); + if (userExists) { + // User exists, proceed with the login process + final gRelogResult = + await client.connectGoogleNoRegister(account); + + if (gRelogResult != null) { + return; + } + } else { + // User does not exist, navigate to registration page + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => RegisterPageWidget( + user: account, idToken: idToken), + ), + ); } Navigator.of(context).push( diff --git a/game/pubspec.lock b/game/pubspec.lock index be39b8c3..7995c094 100644 --- a/game/pubspec.lock +++ b/game/pubspec.lock @@ -553,10 +553,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" js: dependency: transitive description: @@ -593,26 +593,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" linkify: dependency: transitive description: @@ -681,18 +681,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mgrs_dart: dependency: transitive description: @@ -1078,10 +1078,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" tuple: dependency: "direct main" description: @@ -1214,18 +1214,18 @@ packages: dependency: "direct main" description: name: velocity_x - sha256: "38585b8ed87c17ccb42a5c13d55bdafdc65e7cd3f41dceb61c38714c758fa228" + sha256: "99b910c80cc2010b184ef921f0af6894a8d632e13169cf77e6f6cb5a7f310698" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.2.1" vm_service: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" vxstate: dependency: transitive description: @@ -1243,13 +1243,13 @@ packages: source: hosted version: "0.5.1" win32: - dependency: transitive + dependency: "direct main" description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.8.0" win32_registry: dependency: transitive description: @@ -1291,5 +1291,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.19.0" diff --git a/game/pubspec.yaml b/game/pubspec.yaml index 8ceac575..b215f36c 100644 --- a/game/pubspec.yaml +++ b/game/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: carousel_slider: ^5.0.0 tuple: ^2.0.2 sticky_headers: ^0.3.0+2 + win32: ^5.6.1 dev_dependencies: flutter_launcher_icons: diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index a3088fe7..dc88dfef 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -14,6 +14,12 @@ interface IntermediatePayload { email: string; } +/** + * The `AuthService` class provides authentication-related functionality for the application, + * including login, token management, and user verification. + * + * Now supports Google and Apple (OAuth), and device login. Only Cornell emails are allowed. + */ @Injectable() export class AuthService { constructor( @@ -37,6 +43,13 @@ export class AuthService { secret: process.env.JWT_ACCESS_SECRET, }; + /** + * Verifies an Apple ID token and extracts the user's ID and email. + * + * @param idToken - The ID token from Apple Sign-In. + * @returns An object containing the user’s `id` and `email` if verification is successful; + * otherwise, `null` if verification fails. + */ async payloadFromApple(idToken: string): Promise { try { const payload = await appleSignin.verifyIdToken( @@ -54,6 +67,14 @@ export class AuthService { } } + /** + * Verifies a Google ID token based on the audience (platform) and extracts the user's ID and email. + * + * @param idToken - The ID token received from Google. + * @param aud - The platform that issued the token: 'android', 'ios', or 'web'. + * @returns An object containing the user's `id` and `email` if verification is successful; + * otherwise, `null` if verification fails. + */ async payloadFromGoogle( idToken: string, aud: 'android' | 'ios' | 'web', @@ -89,6 +110,14 @@ export class AuthService { } } + /** + * Authenticates a user based on the user credentials, issues access and refresh tokens. + * + * @param authType - The type of authentication used. + * @param req - The login DTO containing user credentials. + * @returns A tuple `[accessToken, refreshToken]` if login is successful; + * otherwise, `null` if authentication fails. + */ async login( authType: AuthType, req: LoginDto, @@ -141,10 +170,15 @@ export class AuthService { idToken.id, req.enrollmentType, ); + + if (!user) { + console.log('Unable to register user'); + return null; + } } + // if user is null here, then it means we're not registering this user now (req.noRegister === true) if (!user) { - console.log('Unable to register user'); return null; } diff --git a/server/src/auth/google/google.controller.ts b/server/src/auth/google/google.controller.ts index aa9b2755..a6abd055 100644 --- a/server/src/auth/google/google.controller.ts +++ b/server/src/auth/google/google.controller.ts @@ -1,11 +1,15 @@ -import { Controller, Post, Body } from '@nestjs/common'; +import { Controller, Post, Body, Get, Query } from '@nestjs/common'; import { AuthType } from '@prisma/client'; import { AuthService } from '../auth.service'; import { LoginDto } from '../login.dto'; +import { UserService } from '../../user/user.service'; @Controller('google') export class GoogleController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) {} // login @Post() @@ -16,4 +20,26 @@ export class GoogleController { return tokens && { accessToken: tokens[0], refreshToken: tokens[1] }; } + + // check user existence + @Get('check-user') + async checkUser( + @Query('idToken') idToken: string, + ): Promise<{ exists: boolean; user?: any }> { + const idTokenPayload = await this.authService.payloadFromGoogle( + idToken, + 'web', + ); + + if (!idTokenPayload) { + console.log('Invalid token or unable to decode token'); + return { exists: false }; + } + + const user = await this.userService.byAuth( + AuthType.GOOGLE, + idTokenPayload.id, + ); + return user ? { exists: true, user } : { exists: false }; + } } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index e8f3dbb5..ace3bf41 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -219,6 +219,13 @@ export class UserService { }); } + async checkIfUserExists( + authType: AuthType, + id: string, + ): Promise { + return this.byAuth(authType, id); + } + async dtoForUserData(user: User, partial: boolean): Promise { const joinedUser = await this.prisma.user.findUniqueOrThrow({ where: { id: user.id },