feat: integrate reCAPTCHA for contact form validation and submission WIP #8
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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.OpenIdConnect" Version="8.0.18" NoWarn="NU1605" />
|
||||
<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,90 +11,98 @@
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<form>
|
||||
<div class="container">
|
||||
<!-- Map stays inside, but will visually float out -->
|
||||
<div class="map-container overflow-hidden rounded">
|
||||
<iframe class="overflow-hidden rounded"
|
||||
src="https://maps.google.com/maps?q=48.72388977142655,21.247448657208842&z=15&output=embed"
|
||||
width="100%" height="400" style="border: 0" allowfullscreen="" loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</div>
|
||||
<div class="spacing-top-bottom">
|
||||
<div class="row">
|
||||
<div class="col-md-8 m-b-30">
|
||||
<div class="row ">
|
||||
<div class="col-md-6">
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="text" placeholder="Meno" />
|
||||
</mat-form-field>
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="tel" placeholder="xxxx xxx xxxx" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="text" placeholder="Priezvisko" />
|
||||
</mat-form-field>
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="mail" placeholder="Emailová adresa" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="container">
|
||||
<!-- Map stays inside, but will visually float out -->
|
||||
<div class="map-container overflow-hidden rounded">
|
||||
<iframe class="overflow-hidden rounded"
|
||||
src="https://maps.google.com/maps?q=48.72388977142655,21.247448657208842&z=15&output=embed" width="100%"
|
||||
height="400" style="border: 0" allowfullscreen="" loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</div>
|
||||
<div class="spacing-top-bottom">
|
||||
<div class="row">
|
||||
<div class="col-md-8 m-b-30">
|
||||
<div class="row ">
|
||||
<div class="col-md-6">
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="text" placeholder="Meno" [(ngModel)]="model.name" />
|
||||
</mat-form-field>
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="tel" placeholder="xxxx xxx xxxx" [(ngModel)]="model.phone" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="text" placeholder="Priezvisko" [(ngModel)]="model.surname" />
|
||||
</mat-form-field>
|
||||
<!-- input -->
|
||||
<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">
|
||||
<input matInput type="mail" placeholder="Emailová adresa" [(ngModel)]="model.email" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Dopyt týkajúci sa<span>*</span>
|
||||
</mat-label>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select value="General Enquiry">
|
||||
<mat-option value="General Enquiry">Všeobecný dopyt</mat-option>
|
||||
<mat-option value="Registration Enquiry">Žiadosť o registráciu</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<mat-label class="f-s-14 f-w-600 m-b-8 d-block">Dopyt týkajúci sa<span>*</span>
|
||||
</mat-label>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select value="General Enquiry" [(ngModel)]="model.subject">
|
||||
<mat-option value="General Enquiry">Všeobecný dopyt</mat-option>
|
||||
<mat-option value="Registration Enquiry">Žiadosť o registráciu</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<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">
|
||||
<textarea matInput rows="5" placeholder="Napíšte svoju správu sem"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<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">
|
||||
<textarea matInput rows="5" placeholder="Napíšte svoju správu sem" [(ngModel)]="model.message"
|
||||
name="message"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button mat-flat-button>Odoslať</button>
|
||||
</div>
|
||||
<div class="col-md-4 right-side-content">
|
||||
<mat-card class="shadow-none bg-primary rounded-8 p-8">
|
||||
<mat-card-content>
|
||||
<div>
|
||||
<h6 class="f-s-20 m-b-16 text-white">Kontaktujte nás dnes</h6>
|
||||
<p class="f-s-14 m-0 text-white">
|
||||
Máte otázky alebo potrebujete pomoc? Napíšte nám správu.
|
||||
</p>
|
||||
</div>
|
||||
<mat-divider class="m-y-30"></mat-divider>
|
||||
<div>
|
||||
<h6 class="f-s-20 m-b-16 text-white">Naša lokalita</h6>
|
||||
<p class="f-s-14 m-0 text-white">
|
||||
Navštívte nás osobne alebo nájdite naše kontaktné údaje, aby ste sa s nami mohli priamo spojiť.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- 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 class="col-md-4 right-side-content">
|
||||
<mat-card class="shadow-none bg-primary rounded-8 p-8">
|
||||
<mat-card-content>
|
||||
<div>
|
||||
<h6 class="f-s-20 m-b-16 text-white">Kontaktujte nás dnes</h6>
|
||||
<p class="f-s-14 m-0 text-white">
|
||||
Máte otázky alebo potrebujete pomoc? Napíšte nám správu.
|
||||
</p>
|
||||
</div>
|
||||
<mat-divider class="m-y-30"></mat-divider>
|
||||
<div>
|
||||
<h6 class="f-s-20 m-b-16 text-white">Naša lokalita</h6>
|
||||
<p class="f-s-14 m-0 text-white">
|
||||
Navštívte nás osobne alebo nájdite naše kontaktné údaje, aby ste sa s nami mohli priamo spojiť.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 { MaterialModule } from '../../../material.module';
|
||||
import { FooterComponent } from '../footer/footer.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
imports: [MaterialModule,IconModule,FooterComponent],
|
||||
imports: [MaterialModule, IconModule, FooterComponent, FormsModule],
|
||||
templateUrl: './contact.component.html',
|
||||
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"
|
||||
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>
|
||||
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user