Skip to content

Commit

Permalink
feat: add OTP support with email code
Browse files Browse the repository at this point in the history
  • Loading branch information
cevheri committed Jan 1, 2025
1 parent 2752382 commit abefb2b
Show file tree
Hide file tree
Showing 19 changed files with 688 additions and 170 deletions.
16 changes: 15 additions & 1 deletion lib/configuration/allowed_paths.dart
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
List<String> allowedPaths = ['/authenticate', '/register', '/logout', '/account/reset-password/init', '/forgot-password'];
List<String> allowedPaths = [
'/authenticate',
'/register',
'/logout',
'/account/reset-password/init',
'/forgot-password',
'/login-otp',
'/login-otp-verify',
];

List<String> authPaths = [
'/authenticate',
'/login-otp',
'/login-otp-verify',
];
12 changes: 8 additions & 4 deletions lib/data/http_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,13 @@ class HttpUtils {
/// if isMock is true, return mock data instead of making a request
if (!ProfileConstants.isProduction) return await mockRequest('POST', endpoint);

final headers = await HttpUtils.headers();
String messageBody = "";
final requestHeaders = await HttpUtils.headers();
if (headers != null) {
requestHeaders.addAll(headers);
}

if (headers['Content-Type'] == applicationJson) {
String messageBody = "";
if (requestHeaders['Content-Type'] == applicationJson) {
messageBody = JsonMapper.serialize(body, _serOps);
} else {
messageBody = body as String;
Expand All @@ -139,7 +142,8 @@ class HttpUtils {
final http.Response response;
try {
final url = Uri.parse('${ProfileConstants.api}$endpoint');
response = await client.post(url, headers: headers, body: messageBody, encoding: _encoding).timeout(_timeout);
response = await client.post(url, headers: requestHeaders, body: messageBody, encoding: _encoding).timeout(_timeout);
//final a = response.body;
checkUnauthorizedAccess(endpoint, response);
} on SocketException catch (se) {
debugPrint("Socket Exception: $se");
Expand Down
44 changes: 44 additions & 0 deletions lib/data/repository/login_repository.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:io';

import 'package:dart_json_mapper/dart_json_mapper.dart';
import 'package:flutter_bloc_advance/configuration/app_logger.dart';
import 'package:flutter_bloc_advance/configuration/local_storage.dart';
import 'package:flutter_bloc_advance/data/app_api_exception.dart';
Expand All @@ -6,6 +9,29 @@ import '../http_utils.dart';
import '../models/jwt_token.dart';
import '../models/user_jwt.dart';

@JsonSerializable()
class SendOtpRequest {
final String email;

SendOtpRequest({required this.email});

Map<String, dynamic> toJson() => {"email": email};

static SendOtpRequest? fromJson(Map<String, dynamic> json) => JsonMapper.fromMap<SendOtpRequest>(json);
}

@JsonSerializable()
class VerifyOtpRequest {
final String email;
final String otp;

VerifyOtpRequest({required this.email, required this.otp});

Map<String, dynamic> toJson() => {"email": email, "otp": otp};

static VerifyOtpRequest? fromJson(Map<String, dynamic> json) => JsonMapper.fromMap<VerifyOtpRequest>(json);
}

class LoginRepository {
static final _log = AppLogger.getLogger("LoginRepository");

Expand Down Expand Up @@ -42,4 +68,22 @@ class LoginRepository {
await AppLocalStorage().clear();
_log.debug("END:logout successful");
}

Future<void> sendOtp(SendOtpRequest request) async {
_log.debug("BEGIN:sendOtp repository start email: {}", [request.email]);
final headers = {"Content-Type": "application/json"};
final response = await HttpUtils.postRequest<SendOtpRequest>("/authenticate/send-otp", request, headers: headers);
if(response.statusCode >= HttpStatus.badRequest){
throw BadRequestException(response.body);
}
_log.debug("successful response: {}", [response.body]);
_log.debug("END:sendOtp successful");
}

Future<JWTToken?> verifyOtp(VerifyOtpRequest request) async {
_log.debug("BEGIN:verifyOtp repository start email: {}", [request.email]);
final headers = {"Content-Type": "application/json"};
final response = await HttpUtils.postRequest<VerifyOtpRequest>("/authenticate/verify-otp", request, headers: headers);
return JWTToken.fromJsonString(response.body);
}
}
15 changes: 15 additions & 0 deletions lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class MessageLookup extends MessageLookupByLibrary {
"filter": MessageLookupByLibrary.simpleMessage("Filter"),
"first_name": MessageLookupByLibrary.simpleMessage("First Name"),
"guest": MessageLookupByLibrary.simpleMessage("Guest"),
"invalid_email":
MessageLookupByLibrary.simpleMessage("Invalid email address"),
"language_select":
MessageLookupByLibrary.simpleMessage("Select Language"),
"last_name": MessageLookupByLibrary.simpleMessage("Last Name"),
Expand All @@ -68,6 +70,8 @@ class MessageLookup extends MessageLookupByLibrary {
"login_button": MessageLookupByLibrary.simpleMessage("Login"),
"login_password": MessageLookupByLibrary.simpleMessage("Password"),
"login_user_name": MessageLookupByLibrary.simpleMessage("Username"),
"login_with_email":
MessageLookupByLibrary.simpleMessage("Login with Email"),
"logout": MessageLookupByLibrary.simpleMessage("Logout"),
"logout_sure": MessageLookupByLibrary.simpleMessage(
"Are you sure you want to logout?"),
Expand Down Expand Up @@ -102,6 +106,12 @@ class MessageLookup extends MessageLookupByLibrary {
"no_changes_made":
MessageLookupByLibrary.simpleMessage("No changes made"),
"no_data": MessageLookupByLibrary.simpleMessage("No Data"),
"only_numbers":
MessageLookupByLibrary.simpleMessage("Only numbers are allowed"),
"otp_code": MessageLookupByLibrary.simpleMessage("OTP Code"),
"otp_length": MessageLookupByLibrary.simpleMessage(
"OTP must be 6 characters long"),
"otp_sent_to": MessageLookupByLibrary.simpleMessage("OTP sent to"),
"password_forgot":
MessageLookupByLibrary.simpleMessage("Forgot Password"),
"password_max_length": MessageLookupByLibrary.simpleMessage(
Expand All @@ -114,16 +124,21 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Required Field"),
"required_range":
MessageLookupByLibrary.simpleMessage("Range is required"),
"resend_otp_code":
MessageLookupByLibrary.simpleMessage("Resend OTP Code"),
"role": MessageLookupByLibrary.simpleMessage("Role"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"screen_size_error":
MessageLookupByLibrary.simpleMessage("Screen size is too small."),
"send_otp_code": MessageLookupByLibrary.simpleMessage("Send OTP Code"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
"success": MessageLookupByLibrary.simpleMessage("Success"),
"translate_menu_title": m0,
"turkish": MessageLookupByLibrary.simpleMessage("Turkish"),
"unsaved_changes": MessageLookupByLibrary.simpleMessage(
"You have unsaved changes. Are you sure you want to leave?"),
"verify_otp_code":
MessageLookupByLibrary.simpleMessage("Verify OTP Code"),
"view_user": MessageLookupByLibrary.simpleMessage("View User"),
"warning": MessageLookupByLibrary.simpleMessage("Warning"),
"yes": MessageLookupByLibrary.simpleMessage("Yes")
Expand Down
90 changes: 90 additions & 0 deletions lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,14 @@
"login_button": "Login",
"loading": "Loading...",
"email_send": "Send Email",
"translate_menu_title": "{translate, select, account{Account} userManagement{User Management} settings{Settings} logout{Logout} info{Info} language{Language} theme{Theme} new_user{New} list_user{List} other{Other}}"
}
"translate_menu_title": "{translate, select, account{Account} userManagement{User Management} settings{Settings} logout{Logout} info{Info} language{Language} theme{Theme} new_user{New} list_user{List} other{Other}}",
"login_with_email": "Login with Email",
"send_otp_code": "Send OTP Code",
"invalid_email": "Invalid email address",
"resend_otp_code": "Resend OTP Code",
"verify_otp_code": "Verify OTP Code",
"only_numbers": "Only numbers are allowed",
"otp_length": "OTP must be 6 characters long",
"otp_code": "OTP Code",
"otp_sent_to": "OTP sent to"
}
159 changes: 84 additions & 75 deletions lib/main/main_local.mapper.g.dart

Large diffs are not rendered by default.

159 changes: 84 additions & 75 deletions lib/main/main_prod.mapper.g.dart

Large diffs are not rendered by default.

47 changes: 46 additions & 1 deletion lib/presentation/screen/login/bloc/login_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
super(const LoginState()) {
on<LoginFormSubmitted>(_onSubmit);
on<TogglePasswordVisibility>((event, emit) => emit(state.copyWith(passwordVisible: !state.passwordVisible)));
on<ChangeLoginMethod>(_onChangeLoginMethod);
on<SendOtpRequested>(_onSendOtpRequested);
on<VerifyOtpSubmitted>(_onVerifyOtpSubmitted);
}

@override
Expand Down Expand Up @@ -51,7 +54,6 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
await AppLocalStorage().save(StorageKeys.roles.name, user.authorities);
_log.debug("onSubmit save storage roles: {}", [user.authorities]);


emit(LoginLoadedState(username: event.username, password: event.password));

_log.debug("END:onSubmit LoginFormSubmitted event success: {}", [token.toString()]);
Expand All @@ -63,4 +65,47 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
_log.error("END:onSubmit LoginFormSubmitted event error: {}", [e.toString()]);
}
}

void _onChangeLoginMethod(ChangeLoginMethod event, Emitter<LoginState> emit) {
emit(state.copyWith(
loginMethod: event.method,
status: LoginStatus.initial,
isOtpSent: false,
));
}

Future<void> _onSendOtpRequested(SendOtpRequested event, Emitter<LoginState> emit) async {
_log.debug("BEGIN: onSendOtpRequested SendOtpRequested event: {}", [event.email]);
emit(LoginLoadingState(username: event.email));
try {
await _repository.sendOtp(SendOtpRequest(email: event.email));
emit(LoginOtpSentState(email: event.email));
_log.debug("END: onSendOtpRequested SendOtpRequested event success: {}", [event.email]);
} catch (e) {
emit(LoginErrorState(message: "OTP gönderme hatası: ${e.toString()}"));
_log.error("END: onSendOtpRequested SendOtpRequested event error: {}", [e.toString()]);
}
}

Future<void> _onVerifyOtpSubmitted(VerifyOtpSubmitted event, Emitter<LoginState> emit) async {
_log.debug("BEGIN: onVerifyOtpSubmitted VerifyOtpSubmitted event: {}", [event.email]);
emit(state.copyWith(status: LoginStatus.loading));
try {
final token = await _repository.verifyOtp(VerifyOtpRequest(email: event.email, otp: event.otpCode));
_log.debug("onVerifyOtpSubmitted token: {}", [token.toString()]);
if (token != null && token.idToken != null) {
await AppLocalStorage().save(StorageKeys.jwtToken.name, token.idToken);
await AppLocalStorage().save(StorageKeys.username.name, event.email);

final user = await AccountRepository().getAccount();
await AppLocalStorage().save(StorageKeys.roles.name, user.authorities);

emit(LoginLoadedState(username: event.email, password: event.otpCode));
} else {
throw BadRequestException("Geçersiz OTP Token");
}
} catch (e) {
emit(LoginErrorState(message: "OTP doğrulama hatası: ${e.toString()}"));
}
}
}
32 changes: 32 additions & 0 deletions lib/presentation/screen/login/bloc/login_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,35 @@ class LoginFormSubmitted extends LoginEvent {
@override
List<Object?> get props => [username, password];
}

enum LoginMethod {otp, password}

class ChangeLoginMethod extends LoginEvent {
final LoginMethod method;

const ChangeLoginMethod({required this.method});

@override
List<Object?> get props => [method];
}

class SendOtpRequested extends LoginEvent {
final String email;

const SendOtpRequested({required this.email});

@override
List<Object?> get props => [email];
}

class VerifyOtpSubmitted extends LoginEvent {
final String email;
final String otpCode;

const VerifyOtpSubmitted({required this.email, required this.otpCode});

@override
List<Object?> get props => [email, otpCode];
}


Loading

0 comments on commit abefb2b

Please sign in to comment.