feat: integrate reCAPTCHA for contact form validation and submission WIP #8
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Cloud.RecaptchaEnterprise.V1" Version="2.18.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" NoWarn="NU1605" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" NoWarn="NU1605" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.18" NoWarn="NU1605" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.18" NoWarn="NU1605" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.18">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.18">
|
||||||
|
|||||||
62
Api/Helpers/ReCapthchaAssessment.cs
Normal file
62
Api/Helpers/ReCapthchaAssessment.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Google.Api.Gax.ResourceNames;
|
||||||
|
using Google.Cloud.RecaptchaEnterprise.V1;
|
||||||
|
|
||||||
|
namespace Api.Helpers
|
||||||
|
{
|
||||||
|
public static class ReCaptchaAssessment
|
||||||
|
{
|
||||||
|
// Checks the supplied token with Recaptcha Enterprise and returns true if valid.
|
||||||
|
// Outputs a short reason when false (invalid reason or exception message).
|
||||||
|
public static bool CheckToken(string token, out string reason, string projectId = "webserverfarm", string recaptchaKey = "6Lf6df0rAAAAAMXcAx1umneXl1QJo9rTrflpWCvB")
|
||||||
|
{
|
||||||
|
reason = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
reason = "empty-token";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = RecaptchaEnterpriseServiceClient.Create();
|
||||||
|
|
||||||
|
var request = new CreateAssessmentRequest
|
||||||
|
{
|
||||||
|
Parent = $"projects/{projectId}",
|
||||||
|
Assessment = new Assessment
|
||||||
|
{
|
||||||
|
Event = new Event
|
||||||
|
{
|
||||||
|
SiteKey = recaptchaKey,
|
||||||
|
Token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = client.CreateAssessment(request);
|
||||||
|
|
||||||
|
if (response?.TokenProperties == null)
|
||||||
|
{
|
||||||
|
reason = "missing-token-properties";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.TokenProperties.Valid)
|
||||||
|
{
|
||||||
|
reason = response.TokenProperties.InvalidReason.ToString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reason = "valid";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
reason = ex.Message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
<form>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Map stays inside, but will visually float out -->
|
<!-- Map stays inside, but will visually float out -->
|
||||||
<div class="map-container overflow-hidden rounded">
|
<div class="map-container overflow-hidden rounded">
|
||||||
<iframe class="overflow-hidden rounded"
|
<iframe class="overflow-hidden rounded"
|
||||||
src="https://maps.google.com/maps?q=48.72388977142655,21.247448657208842&z=15&output=embed"
|
src="https://maps.google.com/maps?q=48.72388977142655,21.247448657208842&z=15&output=embed" width="100%"
|
||||||
width="100%" height="400" style="border: 0" allowfullscreen="" loading="lazy"
|
height="400" style="border: 0" allowfullscreen="" loading="lazy"
|
||||||
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacing-top-bottom">
|
<div class="spacing-top-bottom">
|
||||||
@@ -28,24 +27,24 @@
|
|||||||
<!-- input -->
|
<!-- input -->
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Meno<span>*</span></mat-label>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Meno<span>*</span></mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
<mat-form-field appearance="outline" class="w-100" color="primary">
|
||||||
<input matInput type="text" placeholder="Meno" />
|
<input matInput type="text" placeholder="Meno" [(ngModel)]="model.name" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Telefónne číslo<span>*</span></mat-label>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Telefónne číslo<span>*</span></mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
<mat-form-field appearance="outline" class="w-100" color="primary">
|
||||||
<input matInput type="tel" placeholder="xxxx xxx xxxx" />
|
<input matInput type="tel" placeholder="xxxx xxx xxxx" [(ngModel)]="model.phone" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Priezvisko<span>*</span></mat-label>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Priezvisko<span>*</span></mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
<mat-form-field appearance="outline" class="w-100" color="primary">
|
||||||
<input matInput type="text" placeholder="Priezvisko" />
|
<input matInput type="text" placeholder="Priezvisko" [(ngModel)]="model.surname" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Email<span>*</span></mat-label>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Email<span>*</span></mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
<mat-form-field appearance="outline" class="w-100" color="primary">
|
||||||
<input matInput type="mail" placeholder="Emailová adresa" />
|
<input matInput type="mail" placeholder="Emailová adresa" [(ngModel)]="model.email" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Dopyt týkajúci sa<span>*</span>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Dopyt týkajúci sa<span>*</span>
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-select value="General Enquiry">
|
<mat-select value="General Enquiry" [(ngModel)]="model.subject">
|
||||||
<mat-option value="General Enquiry">Všeobecný dopyt</mat-option>
|
<mat-option value="General Enquiry">Všeobecný dopyt</mat-option>
|
||||||
<mat-option value="Registration Enquiry">Žiadosť o registráciu</mat-option>
|
<mat-option value="Registration Enquiry">Žiadosť o registráciu</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
@@ -66,11 +65,21 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Správa</mat-label>
|
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Správa</mat-label>
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
<mat-form-field appearance="outline" class="w-100" color="primary">
|
||||||
<textarea matInput rows="5" placeholder="Napíšte svoju správu sem"></textarea>
|
<textarea matInput rows="5" placeholder="Napíšte svoju správu sem" [(ngModel)]="model.message"
|
||||||
|
name="message"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button mat-flat-button>Odoslať</button>
|
|
||||||
|
<!-- reCAPTCHA widget: uses siteKey from component -->
|
||||||
|
<div class="row m-t-16">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<!-- anchor for programmatic render -->
|
||||||
|
<div #recaptcha class="g-recaptcha"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button mat-flat-button type="button" (click)="onSubmit()">Odoslať</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 right-side-content">
|
<div class="col-md-4 right-side-content">
|
||||||
<mat-card class="shadow-none bg-primary rounded-8 p-8">
|
<mat-card class="shadow-none bg-primary rounded-8 p-8">
|
||||||
@@ -94,7 +103,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|||||||
@@ -1,14 +1,91 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, AfterViewInit, ViewChild, ElementRef, NgZone } from '@angular/core';
|
||||||
import { IconModule } from '../../../icon/icon.module';
|
import { IconModule } from '../../../icon/icon.module';
|
||||||
import { MaterialModule } from '../../../material.module';
|
import { MaterialModule } from '../../../material.module';
|
||||||
import { FooterComponent } from '../footer/footer.component';
|
import { FooterComponent } from '../footer/footer.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact',
|
selector: 'app-contact',
|
||||||
imports: [MaterialModule,IconModule,FooterComponent],
|
imports: [MaterialModule, IconModule, FooterComponent, FormsModule],
|
||||||
templateUrl: './contact.component.html',
|
templateUrl: './contact.component.html',
|
||||||
styleUrl: './contact.component.scss'
|
styleUrl: './contact.component.scss'
|
||||||
})
|
})
|
||||||
export class ContactComponent {
|
export class ContactComponent implements AfterViewInit {
|
||||||
|
model: any = {
|
||||||
|
name: '',
|
||||||
|
surname: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
subject: '',
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
siteKey: string = '6Lf6df0rAAAAAMXcAx1umneXl1QJo9rTrflpWCvB';
|
||||||
|
@ViewChild('recaptcha', { static: false }) recaptchaElem!: ElementRef;
|
||||||
|
widgetId: number | null = null;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
const win: any = window as any;
|
||||||
|
const renderWidget = () => {
|
||||||
|
const grecaptcha = win.grecaptcha;
|
||||||
|
if (grecaptcha && grecaptcha.render && this.recaptchaElem) {
|
||||||
|
// render widget and keep id
|
||||||
|
this.widgetId = grecaptcha.render(this.recaptchaElem.nativeElement, { sitekey: this.siteKey });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// if grecaptcha already loaded, render immediately
|
||||||
|
if ((win as any).grecaptcha && (win as any).grecaptcha.render) {
|
||||||
|
renderWidget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise define global callback that the index.html script will call when loaded
|
||||||
|
(win as any).onRecaptchaLoad = () => {
|
||||||
|
setTimeout(renderWidget);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
// get token from grecaptcha
|
||||||
|
const grecaptchaAny: any = (window as any).grecaptcha;
|
||||||
|
let token = '';
|
||||||
|
if (grecaptchaAny && grecaptchaAny.getResponse) {
|
||||||
|
token = this.widgetId !== null ? grecaptchaAny.getResponse(this.widgetId) : grecaptchaAny.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// optionally show UI feedback to complete captcha
|
||||||
|
alert('Prosím, potvrďte reCAPTCHA.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: this.model.name,
|
||||||
|
surname: this.model.surname,
|
||||||
|
phone: this.model.phone,
|
||||||
|
email: this.model.email,
|
||||||
|
subject: this.model.subject,
|
||||||
|
message: this.model.message,
|
||||||
|
recaptchatoken: token
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST to your backend endpoint which must verify the token with Google
|
||||||
|
this.http.post('/api/webmessages', payload).subscribe({
|
||||||
|
next: () => {
|
||||||
|
alert('Správa odoslaná');
|
||||||
|
// reset widget
|
||||||
|
if (grecaptchaAny && grecaptchaAny.reset) {
|
||||||
|
if (this.widgetId !== null) grecaptchaAny.reset(this.widgetId);
|
||||||
|
else grecaptchaAny.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert('Chyba pri odosielaní — skúste znova.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap"
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<!-- load recaptcha in explicit render mode and call onRecaptchaLoad when ready -->
|
||||||
|
<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad&render=explicit" async defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="mat-typography">
|
<body class="mat-typography">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user