Skip to content

Commit

Permalink
feature: added recaptcha to integrate with new service, and keys to use
Browse files Browse the repository at this point in the history
  • Loading branch information
felipecastrosales committed Apr 9, 2024
1 parent 0fa8ce7 commit a90d2f8
Show file tree
Hide file tree
Showing 15 changed files with 591 additions and 33 deletions.
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# You can get these keys from https://www.google.com/recaptcha/admin
RECAPTCHA_PUBLIC_KEY=your_public_key
RECAPTCHA_SECRET_KEY=your_secret_key

# Create your api key from AWS SES / API Gateway:
# - https://aws.amazon.com/ses/;
# - https://aws.amazon.com/api-gateway/
API_SEND_MAIL=your_api_send_mail
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ firebase.json
firebase-config.js
.firebase
package.json

# Environment configuration
.env
lib/infra/env/env.g.dart
26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "site",
"request": "launch",
"type": "dart",
"flutterMode": "debug"
},
{
"name": "site (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "site (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}
3 changes: 1 addition & 2 deletions l10n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ output-dir: lib/app/core/l10n/localizations
template-arb-file: app_pt.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
### Uncomment when generating.
# synthetic-package: false
synthetic-package: false
8 changes: 4 additions & 4 deletions lib/app/features/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ class _HomePageState extends State<HomePage> {
void initState() {
super.initState();
items = [
Presentation(itemScrollController),
const Projects(),
const Experience(),
const Social(),
ContactWidget(
contactController: ContactController(
contactRepository: ContactRepositoryImpl(
Expand All @@ -54,6 +50,10 @@ class _HomePageState extends State<HomePage> {
),
),
),
Presentation(itemScrollController),
const Projects(),
const Experience(),
const Social(),
const CustomFooter(),
];
}
Expand Down
59 changes: 39 additions & 20 deletions lib/app/features/home/widgets/contact/contact_widget.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:g_recaptcha_v3/g_recaptcha_v3.dart';

import 'package:site/app/core/injections/injections.dart';
import 'package:site/app/core/l10n/l10n.dart';
Expand All @@ -12,6 +13,7 @@ import 'package:site/app/features/home/widgets/contact/widgets/widgets.dart';
import 'package:site/app/widgets/snack_bars/snack_bars.dart';
import 'package:site/data/models/models.dart' as models;
import 'package:site/data/repositories/contact/contact.dart';
import 'package:site/data/services/recaptcha/recaptcha.dart';

class ContactWidget extends StatelessWidget {
ContactWidget({
Expand Down Expand Up @@ -42,27 +44,39 @@ class ContactWidget extends StatelessWidget {
emailController: emailController,
subjectController: subjectController,
messageController: messageController,
onPressed: () {
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
appShowSnackBar(
context,
text: AppTexts.get(context).emailSendedWithSuccess,
icon: Icons.check,
color: AppColors.primaryDark,
width: 300,
);
_contactController?.sendMail(
contact: models.Contact(
name: nameController.text,
email: emailController.text,
message: messageController.text,
subject: subjectController.text,
),
);
nameController.clear();
emailController.clear();
messageController.clear();
subjectController.clear();
final isNotABot = await RecaptchaService.isNotABot();

if (isNotABot) {
formKey.currentState!.save();
if (context.mounted) {
appShowSnackBar(
context,
text: AppTexts.get(context).emailSendedWithSuccess,
icon: Icons.check,
color: AppColors.primaryDark,
width: 300,
);
}

_contactController?.sendMail(
contact: models.Contact(
name: nameController.text,
email: emailController.text,
message: messageController.text,
subject: subjectController.text,
),
);
for (var controller in [
nameController,
emailController,
messageController,
subjectController,
]) {
controller.clear();
}
}
}
},
);
Expand All @@ -80,3 +94,8 @@ class ContactWidget extends StatelessWidget {
);
}
}

Future<void> generateToken() async {
final token = await GRecaptchaV3.execute('submit') ?? '';
debugPrint('Token: $token');
}
8 changes: 8 additions & 0 deletions lib/data/constants/constants_api.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import 'package:site/infra/env/env.dart';

class ConstantsAPI {
static const baseUrl = 'https://api.emailjs.com/api/v1.0/email/send';
static const headers = {
'origin': 'http://localhost',
'Content-Type': 'application/json',
};

/// Recaptcha information.
static final recaptchaPublicKey = Env.recaptchaPublicKey;
static final recaptchaSecretKey = Env.recaptchaSecretKey;
static final recaptchaUrl =
Uri.parse('https://www.google.com/recaptcha/api/siteverify');
}
2 changes: 2 additions & 0 deletions lib/data/services/recaptcha/recaptcha.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'recaptcha_model.dart';
export 'recaptcha_service.dart';
94 changes: 94 additions & 0 deletions lib/data/services/recaptcha/recaptcha_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:convert';

import 'package:flutter/foundation.dart';

class RecaptchaResponse {
RecaptchaResponse({
required this.success,
required this.challengeTimeStamp,
required this.hostName,
required this.score,
required this.action,
this.errorCodes = const [],
});

factory RecaptchaResponse.fromMap(Map<String, dynamic> json) {
return RecaptchaResponse(
success: json['success'] ?? false,
challengeTimeStamp: DateTime.parse(json['challenge_ts']),
hostName: json['hostname'] ?? '',
score: double.tryParse('${json['score']}') ?? 0.0,
action: json['action'] ?? '',
errorCodes: json['error-codes'] ?? [],
);
}

factory RecaptchaResponse.fromJson(String source) =>
RecaptchaResponse.fromMap(json.decode(source));

final bool success;
final DateTime challengeTimeStamp;
final String hostName;
final double score;
final String action;
final List<String> errorCodes;

RecaptchaResponse copyWith({
bool? success,
DateTime? challengeTimeStamp,
String? hostName,
double? score,
String? action,
List<String>? errorCodes,
}) {
return RecaptchaResponse(
success: success ?? this.success,
challengeTimeStamp: challengeTimeStamp ?? this.challengeTimeStamp,
hostName: hostName ?? this.hostName,
score: score ?? this.score,
action: action ?? this.action,
errorCodes: errorCodes ?? this.errorCodes,
);
}

Map<String, dynamic> toMap() {
return {
'success': success,
'challenge_ts': challengeTimeStamp.millisecondsSinceEpoch,
'hostname': hostName,
'score': score,
'action': action,
'error-codes': errorCodes,
};
}

String toJson() => json.encode(toMap());

@override
String toString() {
return 'RecaptchaResponse(success: $success, challengeTimeStamp: $challengeTimeStamp, hostName: $hostName, score: $score, action: $action, errorCodes: $errorCodes)';
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;

return other is RecaptchaResponse &&
other.success == success &&
other.challengeTimeStamp == challengeTimeStamp &&
other.hostName == hostName &&
other.score == score &&
other.action == action &&
listEquals(other.errorCodes, errorCodes);
}

@override
int get hashCode {
return success.hashCode ^
challengeTimeStamp.hashCode ^
hostName.hashCode ^
score.hashCode ^
action.hashCode ^
errorCodes.hashCode;
}
}
53 changes: 53 additions & 0 deletions lib/data/services/recaptcha/recaptcha_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'dart:developer';

import 'package:g_recaptcha_v3/g_recaptcha_v3.dart';
import 'package:http/http.dart' as http;
import 'package:site/data/constants/constants_api.dart';
import 'package:site/data/services/recaptcha/recaptcha.dart';

class RecaptchaService {
RecaptchaService._();

static Future<void> initiate() async =>
await GRecaptchaV3.ready(ConstantsAPI.recaptchaPublicKey);

static Future<bool> isNotABot() async {
final verificationResponse = await _getVerificationResponse();

if (verificationResponse == null) {
return false;
}

final score = verificationResponse.score;
return score >= 0.5 && score < 1;
}

static Future<RecaptchaResponse?> _getVerificationResponse() async {
try {
final token = await GRecaptchaV3.execute('submit') ?? '';

if (token.isNotEmpty) {
final response = await http.post(
ConstantsAPI.recaptchaUrl,
body: {
'secret': ConstantsAPI.recaptchaSecretKey,
'response': token,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
);
final body = response.body;
return RecaptchaResponse.fromJson(body);
} else {
log('RecaptchaService._getVerificationResponse, token is empty');
}
} catch (e, s) {
log(
'RecaptchaService._getVerificationResponse, error: $e, stackTrace: $s',
);
}

return null;
}
}
22 changes: 22 additions & 0 deletions lib/infra/env/env.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:envied/envied.dart';

part 'env.g.dart';

// Rename and / or create .env file in the root of the project
// (already has a .env.example file to help)
// - Add the following variables.
// And run the following commands:
// dart run build_runner clean
// dart run build_runner build --delete-conflicting-outputs

@Envied(path: '.env')
final class Env {
@EnviedField(varName: 'RECAPTCHA_PUBLIC_KEY', obfuscate: true)
static final String recaptchaPublicKey = _Env.recaptchaPublicKey;

@EnviedField(varName: 'RECAPTCHA_SECRET_KEY', obfuscate: true)
static final String recaptchaSecretKey = _Env.recaptchaSecretKey;

@EnviedField(varName: 'API_SEND_MAIL', obfuscate: true)
static final String apiSendMail = _Env.apiSendMail;
}
29 changes: 22 additions & 7 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import 'dart:async';
import 'dart:developer';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:url_strategy/url_strategy.dart';

import 'package:site/app/app_widget.dart';
import 'package:site/app/core/injections/injections.dart';
import 'package:site/data/services/firebase/firebase.dart';
import 'package:site/data/services/recaptcha/recaptcha.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseServiceImpl().setUpInitialization();
setPathUrlStrategy();
configureDependencies();
runApp(
AppWidget(),
);
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseServiceImpl().setUpInitialization();

if (kIsWeb) {
await RecaptchaService.initiate();
}

setPathUrlStrategy();
configureDependencies();
runApp(
AppWidget(),
);
}, (error, stackTrace) {
log('runZonedGuarded: Caught error: $error');
log('runZonedGuarded: StackTrace: $stackTrace');
});
}
Loading

0 comments on commit a90d2f8

Please sign in to comment.