diff --git a/frontend/angular.json b/frontend/angular.json
index 740de4c9a..4077ba201 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -1,202 +1,185 @@
{
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
- "version": 1,
- "newProjectRoot": "projects",
- "projects": {
- "dissendium-v0": {
- "projectType": "application",
- "schematics": {},
- "root": "",
- "sourceRoot": "src",
- "prefix": "app",
- "architect": {
- "build": {
- "builder": "@angular-devkit/build-angular:browser",
- "options": {
- "outputPath": "dist/dissendium-v0",
- "index": "src/index.html",
- "main": "src/main.ts",
- "polyfills": "src/polyfills.ts",
- "tsConfig": "tsconfig.app.json",
- "assets": [
- "src/favicon.ico",
- "src/assets",
- "src/config.json",
- {
- "glob": "**/*",
- "input": "./node_modules/monaco-editor/min",
- "output": "./assets/monaco"
- }
- ],
- "styles": [
- "src/custom-theme.scss",
- "src/styles.scss"
- ],
- "stylePreprocessorOptions": {
- "includePaths": ["node_modules/@brumeilde/ngx-theme/presets/material"]
- },
- "scripts": [
- ],
- "vendorChunk": true,
- "extractLicenses": false,
- "buildOptimizer": false,
- "sourceMap": true,
- "optimization": false,
- "namedChunks": true
- },
- "configurations": {
- "production": {
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.prod.ts"
- }
- ],
- "optimization": true,
- "outputHashing": "all",
- "sourceMap": true,
- "namedChunks": false,
- "extractLicenses": true,
- "vendorChunk": false,
- "buildOptimizer": true,
- "budgets": [
- {
- "type": "initial",
- "maximumWarning": "2mb",
- "maximumError": "5mb"
- },
- {
- "type": "anyComponentStyle",
- "maximumWarning": "10kb",
- "maximumError": "20kb"
- }
- ]
- },
- "development": {
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.dev.ts"
- }
- ],
- "optimization": false,
- "outputHashing": "all",
- "sourceMap": true,
- "namedChunks": false,
- "extractLicenses": true,
- "vendorChunk": false,
- "buildOptimizer": false
- },
- "saas": {
- "index": {
- "input": "src/index.saas.html",
- "output": "index.html"
- },
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.saas-prod.ts"
- }
- ]
- },
- "saas-production": {
- "index": {
- "input": "src/index.saas.html",
- "output": "index.html"
- },
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.saas-prod.ts"
- }
- ],
- "optimization": true,
- "outputHashing": "all",
- "sourceMap": true,
- "namedChunks": false,
- "extractLicenses": true,
- "vendorChunk": false,
- "buildOptimizer": true,
- "budgets": [
- {
- "type": "initial",
- "maximumWarning": "2mb",
- "maximumError": "5mb"
- },
- {
- "type": "anyComponentStyle",
- "maximumWarning": "10kb",
- "maximumError": "20kb"
- }
- ]
- }
- },
- "defaultConfiguration": ""
- },
- "serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
- "options": {
- "buildTarget": "dissendium-v0:build",
- "host": "127.0.0.1",
- "proxyConfig": "src/proxy.conf.json"
- },
- "configurations": {
- "production": {
- "buildTarget": "dissendium-v0:build:production"
- },
- "saas": {
- "buildTarget": "dissendium-v0:build:saas"
- },
- "development": {
- "buildTarget": "dissendium-v0:build:development"
- }
- }
- },
- "extract-i18n": {
- "builder": "@angular-devkit/build-angular:extract-i18n",
- "options": {
- "buildTarget": "dissendium-v0:build"
- }
- },
- "test": {
- "builder": "@angular-devkit/build-angular:karma",
- "options": {
- "main": "src/test.ts",
- "polyfills": "src/polyfills.ts",
- "tsConfig": "tsconfig.spec.json",
- "karmaConfig": "karma.conf.js",
- "assets": [
- "src/favicon.ico",
- "src/assets",
- "src/config.json"
- ],
- "styles": [
- "src/custom-theme.scss",
- "src/styles.scss"
- ],
- "stylePreprocessorOptions": {
- "includePaths": ["node_modules/@brumeilde/ngx-theme/presets/material"]
- },
- "scripts": []
- }
- },
- "lint": {
- "builder": "@angular-devkit/build-angular:tslint",
- "options": {
- "tsConfig": [
- "tsconfig.app.json",
- "tsconfig.spec.json",
- "e2e/tsconfig.json"
- ],
- "exclude": [
- "**/node_modules/**"
- ]
- }
- }
- }
- }
- },
- "cli": {
- "analytics": false
- }
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "dissendium-v0": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/dissendium-v0",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets",
+ "src/config.json",
+ {
+ "glob": "**/*",
+ "input": "./node_modules/monaco-editor/min",
+ "output": "./assets/monaco"
+ }
+ ],
+ "styles": ["src/custom-theme.scss", "src/styles.scss"],
+ "stylePreprocessorOptions": {
+ "includePaths": ["node_modules/@brumeilde/ngx-theme/presets/material"]
+ },
+ "scripts": [],
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "buildOptimizer": false,
+ "sourceMap": true,
+ "optimization": false,
+ "namedChunks": true
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": true,
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "2mb",
+ "maximumError": "5mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "10kb",
+ "maximumError": "20kb"
+ }
+ ]
+ },
+ "development": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.dev.ts"
+ }
+ ],
+ "optimization": false,
+ "outputHashing": "all",
+ "sourceMap": true,
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": false
+ },
+ "saas": {
+ "index": {
+ "input": "src/index.saas.html",
+ "output": "index.html"
+ },
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.saas.ts"
+ }
+ ]
+ },
+ "saas-production": {
+ "index": {
+ "input": "src/index.saas.html",
+ "output": "index.html"
+ },
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.saas-prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": true,
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "2mb",
+ "maximumError": "5mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "10kb",
+ "maximumError": "20kb"
+ }
+ ]
+ }
+ },
+ "defaultConfiguration": ""
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "buildTarget": "dissendium-v0:build",
+ "host": "127.0.0.1",
+ "proxyConfig": "src/proxy.conf.json"
+ },
+ "configurations": {
+ "production": {
+ "buildTarget": "dissendium-v0:build:production"
+ },
+ "saas": {
+ "buildTarget": "dissendium-v0:build:saas"
+ },
+ "development": {
+ "buildTarget": "dissendium-v0:build:development"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "dissendium-v0:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.spec.json",
+ "karmaConfig": "karma.conf.js",
+ "assets": ["src/favicon.ico", "src/assets", "src/config.json"],
+ "styles": ["src/custom-theme.scss", "src/styles.scss"],
+ "stylePreprocessorOptions": {
+ "includePaths": ["node_modules/@brumeilde/ngx-theme/presets/material"]
+ },
+ "scripts": []
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"],
+ "exclude": ["**/node_modules/**"]
+ }
+ }
+ }
+ }
+ },
+ "cli": {
+ "analytics": false
+ }
}
diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html
index e3d465600..299490e5e 100644
--- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html
+++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html
@@ -70,10 +70,16 @@
Add member to {{company.name}} company
+
+
diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts
index 5e295085a..04f75f9c0 100644
--- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts
+++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts
@@ -1,78 +1,117 @@
-import { CompanyMemberRole } from 'src/app/models/company';
-import { CompanyService } from 'src/app/services/company.service';
-import { Component, Inject } from '@angular/core';
-import { Angulartics2 } from 'angulartics2';
+import { NgForOf, NgIf } from '@angular/common';
+import { Component, Inject, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
-import { MatButtonModule } from '@angular/material/button';
+import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
-import { NgForOf, NgIf } from '@angular/common';
+import { Angulartics2 } from 'angulartics2';
import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive';
-import { MatMenuModule } from '@angular/material/menu';
-import { MatIconModule } from '@angular/material/icon';
+import { CompanyMemberRole } from 'src/app/models/company';
+import { CompanyService } from 'src/app/services/company.service';
+import { environment } from 'src/environments/environment';
+import { TurnstileComponent } from '../../ui-components/turnstile/turnstile.component';
@Component({
- selector: 'app-invite-member-dialog',
- templateUrl: './invite-member-dialog.component.html',
- styleUrls: ['./invite-member-dialog.component.css'],
- standalone: true,
- imports: [
- NgIf,
- NgForOf,
- FormsModule,
- MatDialogModule,
- MatFormFieldModule,
- MatInputModule,
- MatSelectModule,
- MatButtonModule,
- MatMenuModule,
- MatIconModule,
- EmailValidationDirective
- ]
+ selector: 'app-invite-member-dialog',
+ templateUrl: './invite-member-dialog.component.html',
+ styleUrls: ['./invite-member-dialog.component.css'],
+ standalone: true,
+ imports: [
+ NgIf,
+ NgForOf,
+ FormsModule,
+ MatDialogModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ MatButtonModule,
+ MatMenuModule,
+ MatIconModule,
+ EmailValidationDirective,
+ TurnstileComponent,
+ ],
})
export class InviteMemberDialogComponent {
- CompanyMemberRole = CompanyMemberRole;
+ @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent;
+
+ CompanyMemberRole = CompanyMemberRole;
+
+ public isSaas = (environment as any).saas;
+ public turnstileToken: string | null = null;
+ public companyMemberEmail: string;
+ public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member;
+ public submitting: boolean = false;
+ public companyUsersGroup: string = null;
+ public groups: {
+ title: string;
+ groups: object[];
+ }[] = [];
+
+ public companyRolesName = {
+ ADMIN: 'Account Owner',
+ DB_ADMIN: 'System Admin',
+ USER: 'Member',
+ };
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public company: any,
+ public dialogRef: MatDialogRef,
+ private _company: CompanyService,
+ private angulartics2: Angulartics2,
+ ) {}
+
+ ngOnInit(): void {
+ this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection);
+ }
- public companyMemberEmail: string;
- public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member;
- public submitting: boolean = false;
- public companyUsersGroup: string = null;
- public groups: {
- title: string,
- groups: object[]
- }[] = [];
+ addCompanyMember() {
+ this.submitting = true;
+ this._company
+ .inviteCompanyMember(
+ this.company.id,
+ this.companyUsersGroup,
+ this.companyMemberEmail,
+ this.companyMemberRole,
+ this.isSaas ? this.turnstileToken : null,
+ )
+ .subscribe(
+ () => {
+ this.angulartics2.eventTrack.next({
+ action: 'Company: member is invited successfully',
+ });
- public companyRolesName = {
- 'ADMIN': 'Account Owner',
- 'DB_ADMIN': 'System Admin',
- 'USER': 'Member'
- }
+ this.submitting = false;
+ this.dialogRef.close();
+ },
+ () => {
+ this._resetTurnstile();
+ },
+ () => {
+ this.submitting = false;
+ },
+ );
+ }
- constructor(
- @Inject(MAT_DIALOG_DATA) public company: any,
- public dialogRef: MatDialogRef,
- private _company: CompanyService,
- private angulartics2: Angulartics2,
- ) { }
+ onTurnstileToken(token: string) {
+ this.turnstileToken = token;
+ }
- ngOnInit(): void {
- this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection);
- }
+ onTurnstileError() {
+ this.turnstileToken = null;
+ }
- addCompanyMember() {
- this.submitting = true;
- this._company.inviteCompanyMember(this.company.id, this.companyUsersGroup, this.companyMemberEmail, this.companyMemberRole)
- .subscribe(() => {
- this.angulartics2.eventTrack.next({
- action: 'Company: member is invited successfully',
- });
+ onTurnstileExpired() {
+ this.turnstileToken = null;
+ }
- this.submitting = false;
- this.dialogRef.close();
- },
- () => {},
- () => {this.submitting = false});
- }
+ private _resetTurnstile(): void {
+ if (this.isSaas && this.turnstileWidget) {
+ this.turnstileWidget.reset();
+ this.turnstileToken = null;
+ }
+ }
}
diff --git a/frontend/src/app/components/registration/registration.component.css b/frontend/src/app/components/registration/registration.component.css
index e24bc9413..af8e97ef4 100644
--- a/frontend/src/app/components/registration/registration.component.css
+++ b/frontend/src/app/components/registration/registration.component.css
@@ -1,202 +1,204 @@
.wrapper {
- display: grid;
- grid-template-columns: auto 62.5%;
- grid-column-gap: 24px;
- height: 100%;
- padding: 40px 24px;
+ display: grid;
+ grid-template-columns: auto 62.5%;
+ grid-column-gap: 24px;
+ height: 100%;
+ padding: 40px 24px;
}
@media (width <= 600px) {
- .wrapper {
- display: flex;
- flex-direction: column;
- padding: 0;
- }
+ .wrapper {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ }
}
.registrationTitle {
- font-weight: 700 !important;
- margin-top: -4px !important;
- margin-bottom: 32px !important;
+ font-weight: 700 !important;
+ margin-top: -4px !important;
+ margin-bottom: 32px !important;
}
.registrationTitle_mobile {
- display: none;
+ display: none;
}
@media (width <= 600px) {
- .registrationTitle_desktop {
- display: none;
- }
+ .registrationTitle_desktop {
+ display: none;
+ }
- .registrationTitle_mobile {
- display: initial;
- }
+ .registrationTitle_mobile {
+ display: initial;
+ }
}
-
-
-
.registrationTitle__emphasis {
- color: var(--color-accentedPalette-500)
+ color: var(--color-accentedPalette-500);
}
.register-form-box {
- display: flex;
- flex-direction: column;
- /* justify-content: space-between; */
- padding-bottom: 20px;
+ display: flex;
+ flex-direction: column;
+ /* justify-content: space-between; */
+ padding-bottom: 20px;
}
@media (width <= 600px) {
- .register-form-box {
- height: 100%;
- }
+ .register-form-box {
+ height: 100%;
+ }
}
.register-form {
- display: flex;
- flex-direction: column;
- height: 100%;
- margin: auto;
- padding: 0 48px;
- width: 496px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ margin: auto;
+ padding: 0 48px;
+ width: 496px;
}
@media (width <= 600px) {
- .register-form {
- padding: 40px 9vw 0;
- width: 100%;
- }
+ .register-form {
+ padding: 40px 9vw 0;
+ width: 100%;
+ }
}
.mat-h1 {
- align-self: center;
- margin-top: 20px;
- font-weight: 500;
- text-align: center;
- width: 75%;
+ align-self: center;
+ margin-top: 20px;
+ font-weight: 500;
+ text-align: center;
+ width: 75%;
}
@media screen and (max-width: 600px) {
- .mat-h1 {
- margin-top: 32px;
- margin-bottom: 32px;
- width: 100%;
- }
+ .mat-h1 {
+ margin-top: 32px;
+ margin-bottom: 32px;
+ width: 100%;
+ }
}
.divider {
- position: relative;
- background-color: #E8E8E8;
- height: 1px;
- margin: 24px 0;
- width: 100%
+ position: relative;
+ background-color: #e8e8e8;
+ height: 1px;
+ margin: 24px 0;
+ width: 100%;
}
.divider__label {
- position: absolute;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%);
- background-color: var(--mat-sidenav-content-background-color);
- font-size: 14px;
- padding: 0 16px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: var(--mat-sidenav-content-background-color);
+ font-size: 14px;
+ padding: 0 16px;
}
@media screen and (max-width: 600px) {
- #google_registration_button ::ng-deep iframe {
- width: 100% !important;
- }
+ #google_registration_button ::ng-deep iframe {
+ width: 100% !important;
+ }
}
.password-field {
- margin-top: 8px;
+ margin-top: 8px;
+}
+
+.turnstile-widget {
+ margin-top: 24px;
}
.submit-button {
- margin-top: 64px;
+ margin-top: 40px;
}
.register-image-box {
- display: flex;
- justify-content: flex-end;
- align-items: flex-end;
- background-color: var(--image-box-bg);
- border-radius: 12px;
- height: calc(100vh - 120px);
- overflow: hidden;
- padding-top: 3vw;
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-end;
+ background-color: var(--image-box-bg);
+ border-radius: 12px;
+ height: calc(100vh - 120px);
+ overflow: hidden;
+ padding-top: 3vw;
}
@media (prefers-color-scheme: light) {
- .register-image-box {
- --image-box-bg: #E8F1EA;
- }
+ .register-image-box {
+ --image-box-bg: #e8f1ea;
+ }
}
@media (prefers-color-scheme: dark) {
- .register-image-box {
- --image-box-bg: #636363;
- }
+ .register-image-box {
+ --image-box-bg: #636363;
+ }
}
@media screen and (max-width: 600px) {
- .register-image-box {
- display: none;
- }
+ .register-image-box {
+ display: none;
+ }
}
.password-visibility-button {
- cursor: pointer;
+ cursor: pointer;
}
.register-image {
- height: 85vh;
- margin-bottom: -5px;
+ height: 85vh;
+ margin-bottom: -5px;
}
.register-form .agreement {
- margin-top: auto !important;
+ margin-top: auto !important;
}
.link {
- color: var(--color-accentedPalette-500);
+ color: var(--color-accentedPalette-500);
}
.register-form__github-button {
- background-color: #24292f;
- margin-top: 20px;
- padding: 6px;
- width: 400px;
+ background-color: #24292f;
+ margin-top: 20px;
+ padding: 6px;
+ width: 400px;
}
@media (width <= 600px) {
- .register-form__github-button {
- width: auto;
- }
+ .register-form__github-button {
+ width: auto;
+ }
}
.register-form__github-button ::ng-deep .mdc-button__label {
- display: flex;
- justify-content: space-between;
- width: 100%;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
}
.register-form__github-caption {
- flex: 1 0 auto;
- color: #fff;
- margin-top: 2px;
- text-align: center;
+ flex: 1 0 auto;
+ color: #fff;
+ margin-top: 2px;
+ text-align: center;
}
.register-form__github-icon {
- --mat-outlined-button-icon-spacing: 0;
- --mat-outlined-button-icon-offset: 0;
+ --mat-outlined-button-icon-spacing: 0;
+ --mat-outlined-button-icon-offset: 0;
- height: 24px !important;
- width: 24px !important;
+ height: 24px !important;
+ width: 24px !important;
}
.register-form__github-icon ::ng-deep svg path {
- fill: #fff;
+ fill: #fff;
}
diff --git a/frontend/src/app/components/registration/registration.component.html b/frontend/src/app/components/registration/registration.component.html
index ffa07c28c..294c98871 100644
--- a/frontend/src/app/components/registration/registration.component.html
+++ b/frontend/src/app/components/registration/registration.component.html
@@ -44,10 +44,17 @@
(onFieldChange)="updatePasswordField($event)">
+
+
+
diff --git a/frontend/src/app/components/registration/registration.component.spec.ts b/frontend/src/app/components/registration/registration.component.spec.ts
index 41e7be1ec..f36c77485 100644
--- a/frontend/src/app/components/registration/registration.component.spec.ts
+++ b/frontend/src/app/components/registration/registration.component.spec.ts
@@ -1,5 +1,5 @@
import { provideHttpClient } from '@angular/common/http';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatSnackBarModule } from '@angular/material/snack-bar';
@@ -36,6 +36,14 @@ describe('RegistrationComponent', () => {
// @ts-expect-error
global.window.google.accounts = jasmine.createSpyObj(['id']);
global.window.google.accounts.id = jasmine.createSpyObj(['initialize', 'renderButton', 'prompt']);
+
+ // Mock Turnstile
+ window.turnstile = {
+ render: jasmine.createSpy('render').and.returnValue('mock-widget-id'),
+ reset: jasmine.createSpy('reset'),
+ getResponse: jasmine.createSpy('getResponse'),
+ remove: jasmine.createSpy('remove'),
+ };
});
beforeEach(() => {
@@ -45,11 +53,16 @@ describe('RegistrationComponent', () => {
fixture.detectChanges();
});
+ afterEach(() => {
+ delete window.turnstile;
+ });
+
it('should create', () => {
expect(component).toBeTruthy();
});
- it('should sign a user in', () => {
+ it('should sign a user in without turnstile token when not SaaS', () => {
+ component.isSaas = false;
component.user = {
email: 'john@smith.com',
password: 'kK123456789',
@@ -64,4 +77,39 @@ describe('RegistrationComponent', () => {
});
expect(component.submitting).toBeFalse();
});
+
+ it('should include turnstile token in registration request when SaaS', () => {
+ component.isSaas = true;
+ component.user = {
+ email: 'john@smith.com',
+ password: 'kK123456789',
+ };
+ component.turnstileToken = 'test-turnstile-token';
+
+ const fakeSignUpUser = spyOn(authService, 'signUpUser').and.returnValue(of());
+
+ component.registerUser();
+ expect(fakeSignUpUser).toHaveBeenCalledOnceWith({
+ email: 'john@smith.com',
+ password: 'kK123456789',
+ turnstileToken: 'test-turnstile-token',
+ });
+ });
+
+ it('should set turnstileToken when onTurnstileToken is called', () => {
+ component.onTurnstileToken('new-token');
+ expect(component.turnstileToken).toBe('new-token');
+ });
+
+ it('should clear turnstileToken when onTurnstileError is called', () => {
+ component.turnstileToken = 'existing-token';
+ component.onTurnstileError();
+ expect(component.turnstileToken).toBeNull();
+ });
+
+ it('should clear turnstileToken when onTurnstileExpired is called', () => {
+ component.turnstileToken = 'existing-token';
+ component.onTurnstileExpired();
+ expect(component.turnstileToken).toBeNull();
+ });
});
diff --git a/frontend/src/app/components/registration/registration.component.ts b/frontend/src/app/components/registration/registration.component.ts
index 59f94cbc5..fb5863bb6 100644
--- a/frontend/src/app/components/registration/registration.component.ts
+++ b/frontend/src/app/components/registration/registration.component.ts
@@ -1,127 +1,161 @@
-import { AfterViewInit, CUSTOM_ELEMENTS_SCHEMA, Component, NgZone, OnInit } from '@angular/core';
-import { AlertActionType, AlertType } from 'src/app/models/alert';
-
-import { AlertComponent } from '../ui-components/alert/alert.component';
-import { Angulartics2, Angulartics2OnModule } from 'angulartics2';
-import { AuthService } from 'src/app/services/auth.service';
import { CommonModule } from '@angular/common';
-import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive';
+import { AfterViewInit, Component, CUSTOM_ELEMENTS_SCHEMA, NgZone, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
+import { Router } from '@angular/router';
+import { Angulartics2, Angulartics2OnModule } from 'angulartics2';
+import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive';
+import { AlertActionType, AlertType } from 'src/app/models/alert';
import { NewAuthUser } from 'src/app/models/user';
+import { AuthService } from 'src/app/services/auth.service';
import { NotificationsService } from 'src/app/services/notifications.service';
-import { Router } from '@angular/router';
-import { UserPasswordComponent } from '../ui-components/user-password/user-password.component';
import { environment } from 'src/environments/environment';
+import { AlertComponent } from '../ui-components/alert/alert.component';
+import { TurnstileComponent } from '../ui-components/turnstile/turnstile.component';
+import { UserPasswordComponent } from '../ui-components/user-password/user-password.component';
@Component({
- selector: 'app-registration',
- templateUrl: './registration.component.html',
- styleUrls: ['./registration.component.css'],
- imports: [
- CommonModule,
- FormsModule,
- MatFormFieldModule,
- MatInputModule,
- MatButtonModule,
- MatIconModule,
- EmailValidationDirective,
- AlertComponent,
- UserPasswordComponent,
- Angulartics2OnModule
- ],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ selector: 'app-registration',
+ templateUrl: './registration.component.html',
+ styleUrls: ['./registration.component.css'],
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatButtonModule,
+ MatIconModule,
+ EmailValidationDirective,
+ AlertComponent,
+ TurnstileComponent,
+ UserPasswordComponent,
+ Angulartics2OnModule,
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class RegistrationComponent implements OnInit, AfterViewInit {
+ @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent;
+
+ public isSaas = (environment as any).saas;
+ public user: NewAuthUser = {
+ email: '',
+ password: '',
+ };
+ public submitting: boolean;
+ public turnstileToken: string | null = null;
+ public errors = {
+ 'User_with_this_email_is_already_registered.': 'User with this email is already registered',
+ 'GitHub_registration_failed._Please_contact_our_support_team.':
+ 'GitHub registration failed. Please contact our support team.',
+ };
+
+ constructor(
+ private ngZone: NgZone,
+ private angulartics2: Angulartics2,
+ public router: Router,
+ private _auth: AuthService,
+ private _notifications: NotificationsService,
+ ) {}
+
+ ngOnInit(): void {
+ this.angulartics2.eventTrack.next({
+ action: 'Reg: Registration page (component) is loaded',
+ });
+
+ const error = new URLSearchParams(location.search).get('error');
+ if (error)
+ this._notifications.showAlert(AlertType.Error, this.errors[error] || error, [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ]);
+ }
+
+ ngAfterViewInit() {
+ //@ts-expect-error
+ gtag('event', 'conversion', { send_to: 'AW-419937947/auKoCOvwgoYDEJv9nsgB' });
+
+ //@ts-expect-error
+ google.accounts.id.initialize({
+ client_id: '681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com',
+ callback: (authUser) => {
+ this.ngZone.run(() => {
+ this._auth.signUpWithGoogle(authUser.credential).subscribe(() => {
+ this.angulartics2.eventTrack.next({
+ action: 'Reg: google register success',
+ });
+ });
+ });
+ },
+ });
+ //@ts-expect-error
+ google.accounts.id.renderButton(document.getElementById('google_registration_button'), {
+ theme: 'filled_blue',
+ size: 'large',
+ width: 400,
+ text: 'signup_with',
+ });
+ //@ts-expect-error
+ google.accounts.id.prompt();
+ }
+
+ updatePasswordField(updatedValue: string) {
+ this.user.password = updatedValue;
+ }
+
+ registerUser() {
+ this.submitting = true;
+
+ const userData: NewAuthUser = {
+ ...this.user,
+ ...(this.isSaas && this.turnstileToken ? { turnstileToken: this.turnstileToken } : {}),
+ };
+
+ this._auth.signUpUser(userData).subscribe(
+ () => {
+ this.angulartics2.eventTrack.next({
+ action: 'Reg: sing up success',
+ });
+ },
+ (_error) => {
+ this.angulartics2.eventTrack.next({
+ action: 'Reg: sing up unsuccessful',
+ });
+ this.submitting = false;
+ this._resetTurnstile();
+ },
+ () => (this.submitting = false),
+ );
+ }
+
+ onTurnstileToken(token: string) {
+ this.turnstileToken = token;
+ }
+
+ onTurnstileError() {
+ this.turnstileToken = null;
+ }
+
+ onTurnstileExpired() {
+ this.turnstileToken = null;
+ }
+
+ private _resetTurnstile(): void {
+ if (this.isSaas && this.turnstileWidget) {
+ this.turnstileWidget.reset();
+ this.turnstileToken = null;
+ }
+ }
- public isSaas = (environment as any).saas;
- public user: NewAuthUser = {
- email: '',
- password: ''
- };
- public submitting: boolean;
- public errors = {
- 'User_with_this_email_is_already_registered.': 'User with this email is already registered',
- 'GitHub_registration_failed._Please_contact_our_support_team.': 'GitHub registration failed. Please contact our support team.',
- }
-
- constructor(
- private ngZone: NgZone,
- private angulartics2: Angulartics2,
- public router: Router,
- private _auth: AuthService,
- private _notifications: NotificationsService,
- ) {
- }
-
- ngOnInit(): void {
- this.angulartics2.eventTrack.next({
- action: 'Reg: Registration page (component) is loaded'
- });
-
- const error = new URLSearchParams(location.search).get('error');
- if (error) this._notifications.showAlert(AlertType.Error, this.errors[error] || error, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- }
-
- ngAfterViewInit() {
- //@ts-expect-error
- gtag('event', 'conversion', {'send_to': 'AW-419937947/auKoCOvwgoYDEJv9nsgB'});
-
- //@ts-expect-error
- google.accounts.id.initialize({
- client_id: "681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com",
- callback: (authUser) => {
- this.ngZone.run(() => {
- this._auth.signUpWithGoogle(authUser.credential).subscribe(() => {
- this.angulartics2.eventTrack.next({
- action: 'Reg: google register success'
- });
- });
- })
- }
- });
- //@ts-expect-error
- google.accounts.id.renderButton(
- document.getElementById("google_registration_button"),
- { theme: "filled_blue", size: "large", width: 400, text: "signup_with" }
- );
- //@ts-expect-error
- google.accounts.id.prompt();
- }
-
- updatePasswordField(updatedValue: string) {
- this.user.password = updatedValue;
- }
-
- registerUser() {
- this.submitting = true;
-
- this._auth.signUpUser(this.user)
- .subscribe(() => {
- this.angulartics2.eventTrack.next({
- action: 'Reg: sing up success'
- });
- }, (_error) => {
- this.angulartics2.eventTrack.next({
- action: 'Reg: sing up unsuccessful'
- });
- this.submitting = false;
- }, () => this.submitting = false)
- }
-
- registerWithGithub() {
- this._auth.signUpWithGithub();
- this.angulartics2.eventTrack.next({
- action: 'Reg: github register redirect'
- });
- }
+ registerWithGithub() {
+ this._auth.signUpWithGithub();
+ this.angulartics2.eventTrack.next({
+ action: 'Reg: github register redirect',
+ });
+ }
}
diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.css b/frontend/src/app/components/ui-components/turnstile/turnstile.component.css
new file mode 100644
index 000000000..46f2ea33b
--- /dev/null
+++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.css
@@ -0,0 +1,5 @@
+.turnstile-container {
+ display: flex;
+ justify-content: center;
+ margin: 16px 0;
+}
diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.html b/frontend/src/app/components/ui-components/turnstile/turnstile.component.html
new file mode 100644
index 000000000..515fd4bd4
--- /dev/null
+++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.html
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts
new file mode 100644
index 000000000..d3db60592
--- /dev/null
+++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts
@@ -0,0 +1,123 @@
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { TurnstileComponent } from './turnstile.component';
+
+describe('TurnstileComponent', () => {
+ let component: TurnstileComponent;
+ let fixture: ComponentFixture;
+ let mockWidgetId: string;
+
+ beforeEach(async () => {
+ mockWidgetId = 'mock-widget-id';
+
+ window.turnstile = {
+ render: jasmine.createSpy('render').and.returnValue(mockWidgetId),
+ reset: jasmine.createSpy('reset'),
+ getResponse: jasmine.createSpy('getResponse'),
+ remove: jasmine.createSpy('remove'),
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [TurnstileComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TurnstileComponent);
+ component = fixture.componentInstance;
+ });
+
+ afterEach(() => {
+ delete window.turnstile;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render turnstile widget on init', fakeAsync(() => {
+ fixture.detectChanges();
+ tick(100);
+
+ expect(window.turnstile?.render).toHaveBeenCalled();
+ }));
+
+ it('should emit tokenReceived when callback is triggered', fakeAsync(() => {
+ let receivedToken: string | null = null;
+ component.tokenReceived.subscribe((token: string) => {
+ receivedToken = token;
+ });
+
+ (window.turnstile?.render as jasmine.Spy).and.callFake((_container: any, options: any) => {
+ options.callback('test-token');
+ return mockWidgetId;
+ });
+
+ fixture.detectChanges();
+ tick(100);
+
+ expect(receivedToken).toBe('test-token');
+ }));
+
+ it('should emit tokenError when error-callback is triggered', fakeAsync(() => {
+ let errorEmitted = false;
+ component.tokenError.subscribe(() => {
+ errorEmitted = true;
+ });
+
+ (window.turnstile?.render as jasmine.Spy).and.callFake((_container: any, options: any) => {
+ options['error-callback']();
+ return mockWidgetId;
+ });
+
+ fixture.detectChanges();
+ tick(100);
+
+ expect(errorEmitted).toBeTrue();
+ }));
+
+ it('should emit tokenExpired when expired-callback is triggered', fakeAsync(() => {
+ let expiredEmitted = false;
+ component.tokenExpired.subscribe(() => {
+ expiredEmitted = true;
+ });
+
+ (window.turnstile?.render as jasmine.Spy).and.callFake((_container: any, options: any) => {
+ options['expired-callback']();
+ return mockWidgetId;
+ });
+
+ fixture.detectChanges();
+ tick(100);
+
+ expect(expiredEmitted).toBeTrue();
+ }));
+
+ it('should reset the widget when reset() is called', fakeAsync(() => {
+ fixture.detectChanges();
+ tick(100);
+
+ component.reset();
+
+ expect(window.turnstile?.reset).toHaveBeenCalledWith(mockWidgetId);
+ }));
+
+ it('should remove widget on destroy', fakeAsync(() => {
+ fixture.detectChanges();
+ tick(100);
+
+ component.ngOnDestroy();
+
+ expect(window.turnstile?.remove).toHaveBeenCalledWith(mockWidgetId);
+ }));
+
+ it('should emit error if turnstile fails to load', fakeAsync(() => {
+ delete window.turnstile;
+ let errorEmitted = false;
+ component.tokenError.subscribe(() => {
+ errorEmitted = true;
+ });
+
+ fixture.detectChanges();
+ tick(5100);
+
+ expect(errorEmitted).toBeTrue();
+ }));
+});
diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts b/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts
new file mode 100644
index 000000000..73ff0cbef
--- /dev/null
+++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts
@@ -0,0 +1,95 @@
+import { CommonModule } from '@angular/common';
+import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
+import { environment } from 'src/environments/environment';
+
+@Component({
+ selector: 'app-turnstile',
+ templateUrl: './turnstile.component.html',
+ styleUrls: ['./turnstile.component.css'],
+ imports: [CommonModule],
+})
+export class TurnstileComponent implements OnInit, OnDestroy {
+ @ViewChild('turnstileContainer', { static: true }) turnstileContainer: ElementRef;
+
+ @Input() siteKey: string = (environment as any).turnstileSiteKey;
+ @Input() theme: 'light' | 'dark' | 'auto' = 'auto';
+
+ @Output() tokenReceived = new EventEmitter();
+ @Output() tokenError = new EventEmitter();
+ @Output() tokenExpired = new EventEmitter();
+
+ private widgetId: string | null = null;
+ private pollInterval: ReturnType | null = null;
+ private readonly MAX_POLL_ATTEMPTS = 50;
+ private readonly POLL_INTERVAL_MS = 100;
+
+ ngOnInit(): void {
+ this._waitForTurnstileAndRender();
+ }
+
+ ngOnDestroy(): void {
+ this._clearPollInterval();
+ this._removeWidget();
+ }
+
+ public reset(): void {
+ if (window.turnstile && this.widgetId) {
+ window.turnstile.reset(this.widgetId);
+ }
+ }
+
+ private _waitForTurnstileAndRender(): void {
+ let attempts = 0;
+
+ this.pollInterval = setInterval(() => {
+ attempts++;
+
+ if (window.turnstile) {
+ this._clearPollInterval();
+ this._renderWidget();
+ return;
+ }
+
+ if (attempts >= this.MAX_POLL_ATTEMPTS) {
+ this._clearPollInterval();
+ console.error('Turnstile script failed to load');
+ this.tokenError.emit();
+ }
+ }, this.POLL_INTERVAL_MS);
+ }
+
+ private _renderWidget(): void {
+ if (!window.turnstile || !this.turnstileContainer?.nativeElement) {
+ return;
+ }
+
+ this.widgetId = window.turnstile.render(this.turnstileContainer.nativeElement, {
+ sitekey: this.siteKey,
+ callback: (token: string) => {
+ this.tokenReceived.emit(token);
+ },
+ 'error-callback': () => {
+ this.tokenError.emit();
+ },
+ 'expired-callback': () => {
+ this.tokenExpired.emit();
+ },
+ theme: this.theme,
+ appearance: 'always',
+ });
+ }
+
+ private _removeWidget(): void {
+ if (window.turnstile && this.widgetId) {
+ window.turnstile.remove(this.widgetId);
+ this.widgetId = null;
+ }
+ }
+
+ private _clearPollInterval(): void {
+ if (this.pollInterval) {
+ clearInterval(this.pollInterval);
+ this.pollInterval = null;
+ }
+ }
+}
diff --git a/frontend/src/app/models/user.ts b/frontend/src/app/models/user.ts
index 9ad3bae67..d64b74b77 100644
--- a/frontend/src/app/models/user.ts
+++ b/frontend/src/app/models/user.ts
@@ -2,99 +2,100 @@ import { CompanyMemberRole } from './company';
import { TablePermissions } from './table';
export interface NewAuthUser {
- email: string,
- password: string,
+ email: string;
+ password: string;
+ turnstileToken?: string;
}
export interface ExistingAuthUser {
- email: string,
- password: string,
- companyId: string
+ email: string;
+ password: string;
+ companyId: string;
}
export interface UserGroup {
- id: string,
- title: string,
- isMain: boolean,
- users?: {
- id: string,
- isActive: boolean,
- email: string,
- createdAt?: string,
- name: string,
- is_2fa_enabled: boolean,
- role: CompanyMemberRole
- }[]
+ id: string;
+ title: string;
+ isMain: boolean;
+ users?: {
+ id: string;
+ isActive: boolean;
+ email: string;
+ createdAt?: string;
+ name: string;
+ is_2fa_enabled: boolean;
+ role: CompanyMemberRole;
+ }[];
}
export interface UserGroupInfo {
- group: UserGroup,
- accessLevel: string
+ group: UserGroup;
+ accessLevel: string;
}
export interface GroupUser {
- id: string,
- createdAt: string,
- gclid: string | null,
- isActive: boolean,
- stripeId: string,
- email: string,
+ id: string;
+ createdAt: string;
+ gclid: string | null;
+ isActive: boolean;
+ stripeId: string;
+ email: string;
}
export enum SubscriptionPlans {
- free = 'FREE_PLAN',
- team = 'TEAM_PLAN',
- enterprise = 'ENTERPRISE_PLAN',
- teamAnnual = 'ANNUAL_TEAM_PLAN',
- enterpriseAnnual = 'ANNUAL_ENTERPRISE_PLAN',
+ free = 'FREE_PLAN',
+ team = 'TEAM_PLAN',
+ enterprise = 'ENTERPRISE_PLAN',
+ teamAnnual = 'ANNUAL_TEAM_PLAN',
+ enterpriseAnnual = 'ANNUAL_ENTERPRISE_PLAN',
}
export enum RegistrationProvider {
- Google = 'GOOGLE',
- Github = 'GITHUB'
+ Google = 'GOOGLE',
+ Github = 'GITHUB',
}
export interface User {
- id: string,
- isActive: boolean,
- email: string,
- name?: string,
- createdAt?: string,
- portal_link: string,
- subscriptionLevel: SubscriptionPlans,
- is_2fa_enabled: boolean,
- role: CompanyMemberRole,
- externalRegistrationProvider: RegistrationProvider | null,
- company: {
- id: string,
- }
+ id: string;
+ isActive: boolean;
+ email: string;
+ name?: string;
+ createdAt?: string;
+ portal_link: string;
+ subscriptionLevel: SubscriptionPlans;
+ is_2fa_enabled: boolean;
+ role: CompanyMemberRole;
+ externalRegistrationProvider: RegistrationProvider | null;
+ company: {
+ id: string;
+ };
}
export enum AccessLevel {
- None = 'none',
- Readonly = 'readonly',
- Edit = 'edit'
+ None = 'none',
+ Readonly = 'readonly',
+ Edit = 'edit',
}
export interface TablePermission {
- tableName: string,
- display_name: string,
- accessLevel: TablePermissions
+ tableName: string;
+ display_name: string;
+ accessLevel: TablePermissions;
}
export interface Permissions {
- connection: {
- connectionId: string,
- accessLevel: AccessLevel
- },
- group: {
- groupId: string,
- accessLevel: AccessLevel
- },
- tables: TablePermission[]
+ connection: {
+ connectionId: string;
+ accessLevel: AccessLevel;
+ };
+ group: {
+ groupId: string;
+ accessLevel: AccessLevel;
+ };
+ tables: TablePermission[];
}
export interface ApiKey {
- title: string,
- id: string
-}
\ No newline at end of file
+ title: string;
+ id: string;
+}
diff --git a/frontend/src/app/services/company.service.ts b/frontend/src/app/services/company.service.ts
index b41249550..9b68ffcc3 100644
--- a/frontend/src/app/services/company.service.ts
+++ b/frontend/src/app/services/company.service.ts
@@ -1,523 +1,542 @@
-import { AlertActionType, AlertType } from '../models/alert';
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
import { BehaviorSubject, EMPTY } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
-
+import { environment } from 'src/environments/environment';
+import { AlertActionType, AlertType } from '../models/alert';
import { CompanyMemberRole, SamlConfig } from '../models/company';
import { ConfigurationService } from './configuration.service';
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
import { NotificationsService } from './notifications.service';
-import { environment } from 'src/environments/environment';
@Injectable({
- providedIn: 'root'
+ providedIn: 'root',
})
export class CompanyService {
-
- public saasHostnames = (environment as any).saasHostnames;
-
- private company = new BehaviorSubject('');
- public cast = this.company.asObservable();
-
- private companyTabTitleSubject: BehaviorSubject = new BehaviorSubject('Rocketadmin');
-
- private companyLogo: string;
- private companyFavicon: string;
- public companyTabTitle: string;
-
- constructor(
- private _http: HttpClient,
- private _notifications: NotificationsService,
- private _configuration: ConfigurationService
- ) { }
-
- get whiteLabelSettings() {
- return {
- logo: this.companyLogo,
- favicon: this.companyFavicon,
- tabTitle: this.companyTabTitle,
- };
- }
-
- getCurrentTabTitle() {
- return this.companyTabTitleSubject.asObservable();
- }
-
- isCustomDomain() {
- const domain = window.location.hostname;
- return !this.saasHostnames?.includes(domain);
- }
-
- fetchCompany() {
- return this._http.get(`/company/my/full`)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- };
-
- fetchCompanyMembers(companyId: string) {
- return this._http.get(`/company/users/${companyId}`)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- };
-
- fetchCompanyName(companyId: string) {
- return this._http.get(`/company/name/${companyId}`)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- updateCompanyName(companyId: string, name: string) {
- return this._http.put(`/company/name/${companyId}`, {name})
- .pipe(
- map(_res => this._notifications.showSuccessSnackbar('Company name has been updated.')),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- inviteCompanyMember(companyId: string, groupId: string, email: string, role: CompanyMemberRole) {
- return this._http.put(`/company/user/${companyId}`, {
- groupId,
- email,
- role
- })
- .pipe(
- map(() => {
- this._notifications.showSuccessSnackbar(`Invitation link has been sent to ${email}.`);
- this.company.next('invited');
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- revokeInvitation(companyId: string, email: string) {
- return this._http.put(`/company/invitation/revoke/${companyId}`, { email })
- .pipe(
- map(() => {
- this._notifications.showSuccessSnackbar(`Invitation has been revoked for ${email}.`);
- this.company.next('revoked');
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- removeCompanyMemder(companyId: string, userId:string, email: string, userName: string) {
- return this._http.delete(`/company/${companyId}/user/${userId}`)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar(`${userName || email} has been removed from company.`);
- this.company.next('deleted');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- }
-
- updateCompanyMemberRole(companyId: string, userId: string, role: CompanyMemberRole) {
- return this._http.put(`/company/users/roles/${companyId}`, { users: [{userId, role}] })
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar(`Company member role has been updated.`);
- this.company.next('role');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- }
-
- suspendCompanyMember(companyId: string, usersEmails: string[]) {
- return this._http.put(`/company/users/suspend/${companyId}`, { usersEmails })
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar(`Company member has been suspended.`);
- this.company.next('suspended');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- }
-
- restoreCompanyMember(companyId: string, usersEmails: string[]) {
- return this._http.put(`/company/users/unsuspend/${companyId}`, { usersEmails })
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar(`Company member has been restored.`);
- this.company.next('unsuspended');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- }
-
- updateShowTestConnections(displayMode: 'on' | 'off') {
- return this._http.put(`/company/connections/display`, undefined, { params: { displayMode }})
- .pipe(
- map(res => {
- if (displayMode === 'on') {
- this._notifications.showSuccessSnackbar('Test connections now are displayed to your company members.');
- } else {
- this._notifications.showSuccessSnackbar('Test connections now are hidden from your company members.');
- }
- this.company.next('');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]);
- return EMPTY;
- })
- );
- }
-
- getCustomDomain(companyId: string) {
- const config = this._configuration.getConfig();
-
- return this._http.get(config.saasURL + `/saas/custom-domain/${companyId}`)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- createCustomDomain(companyId: string, hostname: string) {
- const config = this._configuration.getConfig();
-
- return this._http.post(config.saasURL + `/saas/custom-domain/register/${companyId}`, { hostname })
- .pipe(
- map(res => {
- // this._notifications.showAlert('Custom domain has been added.');
- this._notifications.showAlert(AlertType.Success,
- {
- abstract: `Now your admin panel is live on your own domain: ${hostname}`,
- details: 'Check it out! If you have any issues or need help setting up your domain or CNAME record, please reach out to our support team.'
- },
- [
- {
- type: AlertActionType.Anchor,
- caption: 'Open',
- to: `https://${hostname}`
- },
- {
- type: AlertActionType.Button,
- caption: 'Dismiss',
- action: (_id: number) => this._notifications.dismissAlert()
- }
- ]
- );
- this.company.next('domain');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- updateCustomDomain(companyId: string, hostname: string) {
- const config = this._configuration.getConfig();
-
- return this._http.put(config.saasURL + `/saas/custom-domain/update/${companyId}`, { hostname })
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Custom domain has been updated.');
- this.company.next('domain');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- deleteCustomDomain(companyId: string) {
- const config = this._configuration.getConfig();
-
- return this._http.delete(config.saasURL + `/saas/custom-domain/delete/${companyId}`)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Custom domain has been removed.');
- this.company.next('domain');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- uploadLogo(companyId: string, file: File) {
- const formData = new FormData();
- formData.append('file', file);
-
- return this._http.post(`/company/logo/${companyId}`, formData)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Logo has been updated. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- removeLogo(companyId: string) {
- return this._http.delete(`/company/logo/${companyId}`)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Logo has been removed. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- uploadFavicon(companyId: string, file: File) {
- const formData = new FormData();
- formData.append('file', file);
-
- return this._http.post(`/company/favicon/${companyId}`, formData)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Favicon has been updated. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- removeFavicon(companyId: string) {
- return this._http.delete(`/company/favicon/${companyId}`)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Favicon has been removed. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- updateTabTitle(companyId: string, tab_title: string) {
- return this._http.post(`/company/tab-title/${companyId}`, {tab_title})
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Tab title has been saved. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- removeTabTitle(companyId: string) {
- return this._http.delete(`/company/tab-title/${companyId}`)
- .pipe(
- map(res => {
- this._notifications.showSuccessSnackbar('Tab title has been removed. Please rerefresh the page to see the changes.');
- this.company.next('updated-white-label-settings');
- return res
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
-
- getWhiteLabelProperties(companyId: string) {
- return this._http.get(`/company/white-label-properties/${companyId}`)
- .pipe(
- map(res => {
- if (res.logo?.image && res.logo.mimeType) {
- this.companyLogo = `data:${res.logo.mimeType};base64,${res.logo.image}`;
- } else {
- this.companyLogo = null;
- }
-
- if (res.favicon?.image && res.favicon.mimeType) {
- this.companyFavicon = `data:${res.favicon.mimeType};base64,${res.favicon.image}`;
- } else {
- this.companyFavicon = null;
- }
-
- if (res.tab_title) {
- this.companyTabTitle = res.tab_title;
- } else {
- this.companyTabTitle = null;
- }
-
- this.companyTabTitleSubject.next(res.tab_title);
-
- this.company.next('');
-
- return {
- logo: this.companyLogo,
- favicon: this.companyFavicon,
- tab_title: res.tab_title,
- }
- }),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- fetchSamlConfiguration(companyId: string) {
- return this._http.get(`/saas/saml/company/full/${companyId}`)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- createSamlConfiguration(companyId: string, config: SamlConfig) {
- return this._http.post(`/saas/saml/company/${companyId}`, config)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
-
- updateSamlConfiguration(config: SamlConfig) {
- return this._http.put(`/saas/saml/${config.id}`, config)
- .pipe(
- map(res => res),
- catchError((err) => {
- console.log(err);
- this._notifications.showErrorSnackbar(err.error.message || err.message);
- return EMPTY;
- })
- );
- }
+ public saasHostnames = (environment as any).saasHostnames;
+
+ private company = new BehaviorSubject('');
+ public cast = this.company.asObservable();
+
+ private companyTabTitleSubject: BehaviorSubject = new BehaviorSubject('Rocketadmin');
+
+ private companyLogo: string;
+ private companyFavicon: string;
+ public companyTabTitle: string;
+
+ constructor(
+ private _http: HttpClient,
+ private _notifications: NotificationsService,
+ private _configuration: ConfigurationService,
+ ) {}
+
+ get whiteLabelSettings() {
+ return {
+ logo: this.companyLogo,
+ favicon: this.companyFavicon,
+ tabTitle: this.companyTabTitle,
+ };
+ }
+
+ getCurrentTabTitle() {
+ return this.companyTabTitleSubject.asObservable();
+ }
+
+ isCustomDomain() {
+ const domain = window.location.hostname;
+ return !this.saasHostnames?.includes(domain);
+ }
+
+ fetchCompany() {
+ return this._http.get(`/company/my/full`).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ fetchCompanyMembers(companyId: string) {
+ return this._http.get(`/company/users/${companyId}`).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ fetchCompanyName(companyId: string) {
+ return this._http.get(`/company/name/${companyId}`).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateCompanyName(companyId: string, name: string) {
+ return this._http.put(`/company/name/${companyId}`, { name }).pipe(
+ map((_res) => this._notifications.showSuccessSnackbar('Company name has been updated.')),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ inviteCompanyMember(
+ companyId: string,
+ groupId: string,
+ email: string,
+ role: CompanyMemberRole,
+ turnstileToken?: string,
+ ) {
+ return this._http
+ .put(`/company/user/${companyId}`, {
+ groupId,
+ email,
+ role,
+ ...(turnstileToken ? { turnstileToken } : {}),
+ })
+ .pipe(
+ map(() => {
+ this._notifications.showSuccessSnackbar(`Invitation link has been sent to ${email}.`);
+ this.company.next('invited');
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ revokeInvitation(companyId: string, email: string) {
+ return this._http.put(`/company/invitation/revoke/${companyId}`, { email }).pipe(
+ map(() => {
+ this._notifications.showSuccessSnackbar(`Invitation has been revoked for ${email}.`);
+ this.company.next('revoked');
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ removeCompanyMemder(companyId: string, userId: string, email: string, userName: string) {
+ return this._http.delete(`/company/${companyId}/user/${userId}`).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(`${userName || email} has been removed from company.`);
+ this.company.next('deleted');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateCompanyMemberRole(companyId: string, userId: string, role: CompanyMemberRole) {
+ return this._http.put(`/company/users/roles/${companyId}`, { users: [{ userId, role }] }).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(`Company member role has been updated.`);
+ this.company.next('role');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ suspendCompanyMember(companyId: string, usersEmails: string[]) {
+ return this._http.put(`/company/users/suspend/${companyId}`, { usersEmails }).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(`Company member has been suspended.`);
+ this.company.next('suspended');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ restoreCompanyMember(companyId: string, usersEmails: string[]) {
+ return this._http.put(`/company/users/unsuspend/${companyId}`, { usersEmails }).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(`Company member has been restored.`);
+ this.company.next('unsuspended');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateShowTestConnections(displayMode: 'on' | 'off') {
+ return this._http.put(`/company/connections/display`, undefined, { params: { displayMode } }).pipe(
+ map((res) => {
+ if (displayMode === 'on') {
+ this._notifications.showSuccessSnackbar('Test connections now are displayed to your company members.');
+ } else {
+ this._notifications.showSuccessSnackbar('Test connections now are hidden from your company members.');
+ }
+ this.company.next('');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showAlert(
+ AlertType.Error,
+ { abstract: err.error.message, details: err.error.originalMessage },
+ [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ return EMPTY;
+ }),
+ );
+ }
+
+ getCustomDomain(companyId: string) {
+ const config = this._configuration.getConfig();
+
+ return this._http.get(config.saasURL + `/saas/custom-domain/${companyId}`).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ createCustomDomain(companyId: string, hostname: string) {
+ const config = this._configuration.getConfig();
+
+ return this._http.post(config.saasURL + `/saas/custom-domain/register/${companyId}`, { hostname }).pipe(
+ map((res) => {
+ // this._notifications.showAlert('Custom domain has been added.');
+ this._notifications.showAlert(
+ AlertType.Success,
+ {
+ abstract: `Now your admin panel is live on your own domain: ${hostname}`,
+ details:
+ 'Check it out! If you have any issues or need help setting up your domain or CNAME record, please reach out to our support team.',
+ },
+ [
+ {
+ type: AlertActionType.Anchor,
+ caption: 'Open',
+ to: `https://${hostname}`,
+ },
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ],
+ );
+ this.company.next('domain');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateCustomDomain(companyId: string, hostname: string) {
+ const config = this._configuration.getConfig();
+
+ return this._http.put(config.saasURL + `/saas/custom-domain/update/${companyId}`, { hostname }).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar('Custom domain has been updated.');
+ this.company.next('domain');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ deleteCustomDomain(companyId: string) {
+ const config = this._configuration.getConfig();
+
+ return this._http.delete(config.saasURL + `/saas/custom-domain/delete/${companyId}`).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar('Custom domain has been removed.');
+ this.company.next('domain');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ uploadLogo(companyId: string, file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ return this._http.post(`/company/logo/${companyId}`, formData).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar('Logo has been updated. Please rerefresh the page to see the changes.');
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ removeLogo(companyId: string) {
+ return this._http.delete(`/company/logo/${companyId}`).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar('Logo has been removed. Please rerefresh the page to see the changes.');
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ uploadFavicon(companyId: string, file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ return this._http.post(`/company/favicon/${companyId}`, formData).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(
+ 'Favicon has been updated. Please rerefresh the page to see the changes.',
+ );
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ removeFavicon(companyId: string) {
+ return this._http.delete(`/company/favicon/${companyId}`).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(
+ 'Favicon has been removed. Please rerefresh the page to see the changes.',
+ );
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateTabTitle(companyId: string, tab_title: string) {
+ return this._http.post(`/company/tab-title/${companyId}`, { tab_title }).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(
+ 'Tab title has been saved. Please rerefresh the page to see the changes.',
+ );
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ removeTabTitle(companyId: string) {
+ return this._http.delete(`/company/tab-title/${companyId}`).pipe(
+ map((res) => {
+ this._notifications.showSuccessSnackbar(
+ 'Tab title has been removed. Please rerefresh the page to see the changes.',
+ );
+ this.company.next('updated-white-label-settings');
+ return res;
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ getWhiteLabelProperties(companyId: string) {
+ return this._http.get(`/company/white-label-properties/${companyId}`).pipe(
+ map((res) => {
+ if (res.logo?.image && res.logo.mimeType) {
+ this.companyLogo = `data:${res.logo.mimeType};base64,${res.logo.image}`;
+ } else {
+ this.companyLogo = null;
+ }
+
+ if (res.favicon?.image && res.favicon.mimeType) {
+ this.companyFavicon = `data:${res.favicon.mimeType};base64,${res.favicon.image}`;
+ } else {
+ this.companyFavicon = null;
+ }
+
+ if (res.tab_title) {
+ this.companyTabTitle = res.tab_title;
+ } else {
+ this.companyTabTitle = null;
+ }
+
+ this.companyTabTitleSubject.next(res.tab_title);
+
+ this.company.next('');
+
+ return {
+ logo: this.companyLogo,
+ favicon: this.companyFavicon,
+ tab_title: res.tab_title,
+ };
+ }),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ fetchSamlConfiguration(companyId: string) {
+ return this._http.get(`/saas/saml/company/full/${companyId}`).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ createSamlConfiguration(companyId: string, config: SamlConfig) {
+ return this._http.post(`/saas/saml/company/${companyId}`, config).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
+
+ updateSamlConfiguration(config: SamlConfig) {
+ return this._http.put(`/saas/saml/${config.id}`, config).pipe(
+ map((res) => res),
+ catchError((err) => {
+ console.log(err);
+ this._notifications.showErrorSnackbar(err.error.message || err.message);
+ return EMPTY;
+ }),
+ );
+ }
}
diff --git a/frontend/src/app/types/turnstile.d.ts b/frontend/src/app/types/turnstile.d.ts
new file mode 100644
index 000000000..15e4a2a50
--- /dev/null
+++ b/frontend/src/app/types/turnstile.d.ts
@@ -0,0 +1,22 @@
+export interface TurnstileOptions {
+ sitekey: string;
+ callback?: (token: string) => void;
+ 'error-callback'?: () => void;
+ 'expired-callback'?: () => void;
+ theme?: 'light' | 'dark' | 'auto';
+ appearance?: 'always' | 'execute' | 'interaction-only';
+ size?: 'normal' | 'compact';
+}
+
+export interface TurnstileInstance {
+ render: (container: string | HTMLElement, options: TurnstileOptions) => string;
+ reset: (widgetId?: string) => void;
+ getResponse: (widgetId?: string) => string | undefined;
+ remove: (widgetId?: string) => void;
+}
+
+declare global {
+ interface Window {
+ turnstile?: TurnstileInstance;
+ }
+}
diff --git a/frontend/src/environments/environment.saas-prod.ts b/frontend/src/environments/environment.saas-prod.ts
index b01db2531..3810d785f 100644
--- a/frontend/src/environments/environment.saas-prod.ts
+++ b/frontend/src/environments/environment.saas-prod.ts
@@ -1,9 +1,10 @@
export const environment = {
- production: true,
- saas: true,
- apiRoot: "/api",
- saasURL: "",
- saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'],
- stagingHost: "rocketadmin-dev.tail9f8b2.ts.net", // Tailscale host
- version: '0.0.0'
- };
+ production: true,
+ saas: true,
+ apiRoot: '/api',
+ saasURL: '',
+ saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'],
+ stagingHost: 'rocketadmin-dev.tail9f8b2.ts.net', // Tailscale host
+ version: '0.0.0',
+ turnstileSiteKey: '0x4AAAAAACM2ZuNYhGhncig_',
+};
diff --git a/frontend/src/environments/environment.saas.ts b/frontend/src/environments/environment.saas.ts
index c1b1dd0f8..0c61886f2 100644
--- a/frontend/src/environments/environment.saas.ts
+++ b/frontend/src/environments/environment.saas.ts
@@ -1,9 +1,10 @@
export const environment = {
- saas: true,
- production: false,
- apiRoot: "/api",
- saasURL: "",
- saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'],
- stagingHost: "rocketadmin-dev.tail9f8b2.ts.net", // Tailscale host
- version: '0.0.0'
+ saas: true,
+ production: false,
+ apiRoot: 'https://rocketadmin-dev.tail9f8b2.ts.net/api',
+ saasURL: 'https://rocketadmin-dev.tail9f8b2.ts.net',
+ saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'],
+ stagingHost: 'rocketadmin-dev.tail9f8b2.ts.net', // Tailscale host
+ version: '0.0.0',
+ turnstileSiteKey: '1x00000000000000000000AA', // Test key - always passes
};
diff --git a/frontend/src/index.saas.html b/frontend/src/index.saas.html
index 58a8d8b85..294c7f2d0 100644
--- a/frontend/src/index.saas.html
+++ b/frontend/src/index.saas.html
@@ -152,6 +152,9 @@
}
}
+
+
+