Skip to content

Commit 14dc773

Browse files
guguclaude
andcommitted
Add Cloudflare Turnstile captcha to invite member dialog
Integrates Turnstile verification into the company member invitation flow for SaaS environments. The captcha widget only appears when running in SaaS mode and the token is passed to the backend which already supports verification. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent edd4708 commit 14dc773

3 files changed

Lines changed: 640 additions & 576 deletions

File tree

frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,16 @@ <h1 mat-dialog-title>Add member to <strong>{{company.name}}</strong> company</h1
7070
</mat-select>
7171
</mat-form-field>
7272
</mat-dialog-content>
73+
<app-turnstile *ngIf="isSaas"
74+
class="turnstile-widget"
75+
(tokenReceived)="onTurnstileToken($event)"
76+
(tokenError)="onTurnstileError()"
77+
(tokenExpired)="onTurnstileExpired()">
78+
</app-turnstile>
7379
<mat-dialog-actions align="end">
7480
<button type="button" mat-flat-button mat-dialog-close>Cancel</button>
7581
<button mat-flat-button color="primary"
76-
[disabled]="submitting || addUserForm.form.invalid">
82+
[disabled]="submitting || addUserForm.form.invalid || (isSaas && !turnstileToken)">
7783
Add
7884
</button>
7985
</mat-dialog-actions>
Lines changed: 101 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,117 @@
1-
import { CompanyMemberRole } from 'src/app/models/company';
2-
import { CompanyService } from 'src/app/services/company.service';
3-
import { Component, Inject } from '@angular/core';
4-
import { Angulartics2 } from 'angulartics2';
1+
import { NgForOf, NgIf } from '@angular/common';
2+
import { Component, Inject, ViewChild } from '@angular/core';
53
import { FormsModule } from '@angular/forms';
6-
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
4+
import { MatButtonModule } from '@angular/material/button';
5+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
76
import { MatFormFieldModule } from '@angular/material/form-field';
7+
import { MatIconModule } from '@angular/material/icon';
88
import { MatInputModule } from '@angular/material/input';
9-
import { MatButtonModule } from '@angular/material/button';
9+
import { MatMenuModule } from '@angular/material/menu';
1010
import { MatSelectModule } from '@angular/material/select';
11-
import { NgForOf, NgIf } from '@angular/common';
11+
import { Angulartics2 } from 'angulartics2';
1212
import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive';
13-
import { MatMenuModule } from '@angular/material/menu';
14-
import { MatIconModule } from '@angular/material/icon';
13+
import { CompanyMemberRole } from 'src/app/models/company';
14+
import { CompanyService } from 'src/app/services/company.service';
15+
import { environment } from 'src/environments/environment';
16+
import { TurnstileComponent } from '../../ui-components/turnstile/turnstile.component';
1517

1618
@Component({
17-
selector: 'app-invite-member-dialog',
18-
templateUrl: './invite-member-dialog.component.html',
19-
styleUrls: ['./invite-member-dialog.component.css'],
20-
standalone: true,
21-
imports: [
22-
NgIf,
23-
NgForOf,
24-
FormsModule,
25-
MatDialogModule,
26-
MatFormFieldModule,
27-
MatInputModule,
28-
MatSelectModule,
29-
MatButtonModule,
30-
MatMenuModule,
31-
MatIconModule,
32-
EmailValidationDirective
33-
]
19+
selector: 'app-invite-member-dialog',
20+
templateUrl: './invite-member-dialog.component.html',
21+
styleUrls: ['./invite-member-dialog.component.css'],
22+
standalone: true,
23+
imports: [
24+
NgIf,
25+
NgForOf,
26+
FormsModule,
27+
MatDialogModule,
28+
MatFormFieldModule,
29+
MatInputModule,
30+
MatSelectModule,
31+
MatButtonModule,
32+
MatMenuModule,
33+
MatIconModule,
34+
EmailValidationDirective,
35+
TurnstileComponent,
36+
],
3437
})
3538
export class InviteMemberDialogComponent {
36-
CompanyMemberRole = CompanyMemberRole;
39+
@ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent;
40+
41+
CompanyMemberRole = CompanyMemberRole;
42+
43+
public isSaas = (environment as any).saas;
44+
public turnstileToken: string | null = null;
45+
public companyMemberEmail: string;
46+
public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member;
47+
public submitting: boolean = false;
48+
public companyUsersGroup: string = null;
49+
public groups: {
50+
title: string;
51+
groups: object[];
52+
}[] = [];
53+
54+
public companyRolesName = {
55+
ADMIN: 'Account Owner',
56+
DB_ADMIN: 'System Admin',
57+
USER: 'Member',
58+
};
59+
60+
constructor(
61+
@Inject(MAT_DIALOG_DATA) public company: any,
62+
public dialogRef: MatDialogRef<InviteMemberDialogComponent>,
63+
private _company: CompanyService,
64+
private angulartics2: Angulartics2,
65+
) {}
66+
67+
ngOnInit(): void {
68+
this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection);
69+
}
3770

38-
public companyMemberEmail: string;
39-
public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member;
40-
public submitting: boolean = false;
41-
public companyUsersGroup: string = null;
42-
public groups: {
43-
title: string,
44-
groups: object[]
45-
}[] = [];
71+
addCompanyMember() {
72+
this.submitting = true;
73+
this._company
74+
.inviteCompanyMember(
75+
this.company.id,
76+
this.companyUsersGroup,
77+
this.companyMemberEmail,
78+
this.companyMemberRole,
79+
this.isSaas ? this.turnstileToken : null,
80+
)
81+
.subscribe(
82+
() => {
83+
this.angulartics2.eventTrack.next({
84+
action: 'Company: member is invited successfully',
85+
});
4686

47-
public companyRolesName = {
48-
'ADMIN': 'Account Owner',
49-
'DB_ADMIN': 'System Admin',
50-
'USER': 'Member'
51-
}
87+
this.submitting = false;
88+
this.dialogRef.close();
89+
},
90+
() => {
91+
this._resetTurnstile();
92+
},
93+
() => {
94+
this.submitting = false;
95+
},
96+
);
97+
}
5298

53-
constructor(
54-
@Inject(MAT_DIALOG_DATA) public company: any,
55-
public dialogRef: MatDialogRef<InviteMemberDialogComponent>,
56-
private _company: CompanyService,
57-
private angulartics2: Angulartics2,
58-
) { }
99+
onTurnstileToken(token: string) {
100+
this.turnstileToken = token;
101+
}
59102

60-
ngOnInit(): void {
61-
this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection);
62-
}
103+
onTurnstileError() {
104+
this.turnstileToken = null;
105+
}
63106

64-
addCompanyMember() {
65-
this.submitting = true;
66-
this._company.inviteCompanyMember(this.company.id, this.companyUsersGroup, this.companyMemberEmail, this.companyMemberRole)
67-
.subscribe(() => {
68-
this.angulartics2.eventTrack.next({
69-
action: 'Company: member is invited successfully',
70-
});
107+
onTurnstileExpired() {
108+
this.turnstileToken = null;
109+
}
71110

72-
this.submitting = false;
73-
this.dialogRef.close();
74-
},
75-
() => {},
76-
() => {this.submitting = false});
77-
}
111+
private _resetTurnstile(): void {
112+
if (this.isSaas && this.turnstileWidget) {
113+
this.turnstileWidget.reset();
114+
this.turnstileToken = null;
115+
}
116+
}
78117
}

0 commit comments

Comments
 (0)