Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix New User Google Login/Register Issue #234

Merged
merged 7 commits into from
Nov 20, 2024
Merged
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
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
Loading