Skip to content

Commit

Permalink
Fix New User Google Login/Register Issue (#234)
Browse files Browse the repository at this point in the history
* Fixed local machine dependency issues

* Fixed the issue of registering new users on simulator

* Added documentation for login related pages

* Fixed the wrong uri call for checking user existence

* Removeed loggings that are used for debugging

* Made check user existence logic concise
  • Loading branch information
Shengle-Dai authored Nov 20, 2024
1 parent 5b0b6d1 commit 0d9e19c
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 37 deletions.
2 changes: 1 addition & 1 deletion game/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Firebase
import GoogleMaps
import flutter_config

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
33 changes: 27 additions & 6 deletions game/lib/api/game_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand All @@ -243,4 +244,24 @@ class ApiClient extends ChangeNotifier {
authenticated = false;
notifyListeners();
}

Future<bool> 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;
}
}
}
4 changes: 3 additions & 1 deletion game/lib/api/geopoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down
2 changes: 2 additions & 0 deletions game/lib/interests/interests_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions game/lib/register_page/register_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 20 additions & 4 deletions game/lib/splash_page/splash_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
44 changes: 22 additions & 22 deletions game/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions game/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 35 additions & 1 deletion server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<IntermediatePayload | null> {
try {
const payload = await appleSignin.verifyIdToken(
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down
30 changes: 28 additions & 2 deletions server/src/auth/google/google.controller.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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 };
}
}
7 changes: 7 additions & 0 deletions server/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ export class UserService {
});
}

async checkIfUserExists(
authType: AuthType,
id: string,
): Promise<User | null> {
return this.byAuth(authType, id);
}

async dtoForUserData(user: User, partial: boolean): Promise<UserDto> {
const joinedUser = await this.prisma.user.findUniqueOrThrow({
where: { id: user.id },
Expand Down

0 comments on commit 0d9e19c

Please sign in to comment.