From 49c4aa166790c943d594cc0e7161bca39fb911dc Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 30 Jan 2025 13:34:31 +0200 Subject: [PATCH 1/5] filters: add not-contain comparator --- .../db-table-filters-dialog.component.html | 3 +++ .../app/components/dashboard/db-table/db-table.component.ts | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html index 1f653b69e..d6f8fb042 100644 --- a/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -78,6 +78,9 @@

contains + + not contains + is empty diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.ts b/frontend/src/app/components/dashboard/db-table/db-table.component.ts index cad3fb354..2278da222 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.ts +++ b/frontend/src/app/components/dashboard/db-table/db-table.component.ts @@ -6,6 +6,7 @@ import { map, startWith, tap } from 'rxjs/operators'; import { AccessLevel } from 'src/app/models/user'; import { ActivatedRoute } from '@angular/router'; +import { Angulartics2OnModule } from 'angulartics2'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { DbTableExportDialogComponent } from '../db-table-export-dialog/db-table-export-dialog.component'; @@ -33,7 +34,6 @@ import { RouterModule } from '@angular/router'; import { SelectionModel } from '@angular/cdk/collections'; import { TableStateService } from 'src/app/services/table-state.service'; import { normalizeTableName } from '../../../lib/normalize' -import { Angulartics2OnModule } from 'angulartics2'; interface Column { title: string, @@ -286,6 +286,8 @@ export class DbTableComponent implements OnInit { return `${displayedName} = ...${filterValue}` } else if (comparator == 'contains') { return `${displayedName} = ...${filterValue}...` + } else if (comparator == 'icontains') { + return `${displayedName} != ...${filterValue}...` } else if (comparator == 'empty') { return `${displayedName} = ' '` } else { From 2cce761b315a2633b50d1512cda014fbb11c4bf4 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 30 Jan 2025 22:04:09 +0200 Subject: [PATCH 2/5] Rules: - display code snippets; - fix signingKey receiving; - remove single variant of code snippets. --- .../db-table-actions.component.css | 43 +-- .../db-table-actions.component.html | 107 ++++---- .../db-table-actions.component.ts | 19 +- frontend/src/app/consts/code-snippets.ts | 252 +----------------- .../src/app/services/connections.service.ts | 6 + 5 files changed, 92 insertions(+), 335 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css index f0def1a9a..9ffc4a256 100644 --- a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css +++ b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.css @@ -191,46 +191,17 @@ .code-snippet-box { position: relative; - display: flex; - flex-direction: column; - align-items: flex-end; - height: calc(100vh - 200px); - overflow-y: auto; - flex-grow: 1; -} - -.action-codeSnippet { - height: 100%; - width: 100%; -} - -.action-codeSnippet ::ng-deep .ngs-code-editor { - height: 100%; -} - -.lang-select { - position: absolute; - top: 20px; - right: 20px; - width: 156px; - z-index: 5; -} - -.lang-select ::ng-deep .mdc-text-field { - background-color: #fff; -} - -::ng-deep .CodeMirror-wrap pre.CodeMirror-line, -::ng-deep .CodeMirror-wrap pre.CodeMirror-line-like { - word-wrap: break-word; - white-space: pre-wrap; - word-break: break-word; + border: 1px solid #b0b0b0; + margin-top: 8px; + margin-bottom: 20px; + max-width: 1100px; + padding: 8px 12px; } .copy-button { position: absolute; - right: 184px; - top: 26px; + right: 32px; + top: 32px; background-color: #fff; z-index: 5; } diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html index ccf7f475b..798e77edb 100644 --- a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html +++ b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.html @@ -125,31 +125,59 @@

Rules

then perform action(s) -
- - Actions type - - Email notification - Slack notification - URL webhook - - - - Action URL - - - - - Slack URL - - - - - Emails - - {{companyMember.name}} | {{companyMember.email}} - - +
+
+ + Actions type + + Email notification + Slack notification + URL webhook + + + + Action URL + + + + + Slack URL + + + + + Emails + + {{companyMember.name}} | {{companyMember.email}} + + +
+ +
+ + + + + + + + +
@@ -164,35 +192,6 @@

Rules

- -
diff --git a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts index 6d4c658af..6ea0ea160 100644 --- a/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts +++ b/frontend/src/app/components/dashboard/db-table-actions/db-table-actions.component.ts @@ -1,11 +1,13 @@ import { AlertActionType, AlertType } from 'src/app/models/alert'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; import { Component, OnInit } from '@angular/core'; import { CustomAction, CustomActionMethod, CustomActionType, CustomEvent, EventType, Rule } from 'src/app/models/table'; import { ActionDeleteDialogComponent } from './action-delete-dialog/action-delete-dialog.component'; import { AlertComponent } from '../../ui-components/alert/alert.component'; -import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; import { BreadcrumbsComponent } from '../../ui-components/breadcrumbs/breadcrumbs.component'; +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { CodeEditorModule } from '@ngstack/code-editor'; import { CommonModule } from '@angular/common'; import { CompanyMember } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; @@ -15,6 +17,7 @@ import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { IconPickerComponent } from '../../ui-components/icon-picker/icon-picker.component'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; @@ -23,6 +26,7 @@ import { MatListModule } from '@angular/material/list'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { NotificationsService } from 'src/app/services/notifications.service'; import { TablesService } from 'src/app/services/tables.service'; @@ -30,7 +34,6 @@ import { Title } from '@angular/platform-browser'; import { UserService } from 'src/app/services/user.service'; import { codeSnippets } from 'src/app/consts/code-snippets'; import { normalizeTableName } from 'src/app/lib/normalize'; -import { MatCheckboxModule } from '@angular/material/checkbox'; @Component({ selector: 'app-db-table-actions', @@ -38,6 +41,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; styleUrls: ['./db-table-actions.component.css'], imports: [ CommonModule, + ClipboardModule, FormsModule, MatButtonModule, MatCheckboxModule, @@ -49,6 +53,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; MatSidenavModule, MatListModule, MatRadioModule, + MatTabsModule, + CodeEditorModule, AlertComponent, BreadcrumbsComponent, ContentLoaderComponent, @@ -110,10 +116,9 @@ export class DbTableActionsComponent implements OnInit { async ngOnInit() { this.connectionID = this._connections.currentConnectionID; - this.signingKey = this._connections.currentConnection.signing_key; this.tableName = this._tables.currentTableName; this.normalizedTableName = normalizeTableName(this.tableName); - this.codeSnippets = codeSnippets(this._connections.currentConnection.signing_key); + this._connections.getCurrentConnectionSigningKey().subscribe(signingKey => this.codeSnippets = codeSnippets(signingKey)); try { this.rulesData = await this.getRules(); @@ -139,7 +144,6 @@ export class DbTableActionsComponent implements OnInit { if (arg === 'delete-rule') { try { this.rulesData = await this.getRules(); - console.log(this.rulesData); this.rules = this.rulesData.action_rules; this.selectedRule = this.rules[0]; this.selectedRuleTitle = this.selectedRule.title; @@ -153,9 +157,14 @@ export class DbTableActionsComponent implements OnInit { } get currentConnection() { + // this.codeSnippets = codeSnippets(this._connections.currentConnection.signing_key); return this._connections.currentConnection; } + // get codeSnippets() { + // return codeSnippets(this._connections.currentConnection.signing_key); + // } + getCrumbs(name: string) { return [ { diff --git a/frontend/src/app/consts/code-snippets.ts b/frontend/src/app/consts/code-snippets.ts index f15fc7e33..728671b32 100644 --- a/frontend/src/app/consts/code-snippets.ts +++ b/frontend/src/app/consts/code-snippets.ts @@ -3,42 +3,7 @@ export function codeSnippets(signingKey: string) { cs: { langName: 'C#', mode: 'csharp', - snippet: { - single: `using System.Linq; - using System.Security.Cryptography; - using System.Text; - using Microsoft.AspNetCore.Mvc; - - namespace MyApp.Controllers - { - [Route("api/[controller]")] - [ApiController] - public class RocketadminController : ControllerBase - { - [HttpPost] - public IActionResult Create([FromHeader] string RocketadminSignature, [FromBody] dynamic bodyData) - { - string[] yourTablePrimaryKeysNames = { "id" }; - string stringifiedPkeys = string.Join("\n", yourTablePrimaryKeysNames.Select(keyName => $"{keyName}::{bodyData[keyName]}")) - .TrimEnd(); - - string strTohash = $"{bodyData["$$_date"]}$\${stringifiedPkeys}$\${bodyData["$$_actionId"]}$\${bodyData["$$_tableName"]}"; - using (var hmac = new HMACSHA256("${signingKey}")) - { - string hmacString = Encoding.UTF8.GetString(hmac.ComputeHash(Encoding.UTF8.GetBytes(strTohash))); - if (hmacString != RocketadminSignature) - { - return BadRequest("Signature invalid"); - } - } - - // Your code here - return Ok(); - } - } - } - `, - multiple: `using System; + snippet: `using System; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; @@ -54,7 +19,7 @@ public class RocketAdminController : ControllerBase string bodyToJson = Newtonsoft.Json.JsonConvert.SerializeObject(bodyData); - var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("your_rocketadmin_signing_key")); + var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("${signingKey}")); byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(bodyToJson)); string hashString = BitConverter.ToString(hash).Replace("-", "").ToLower(); @@ -71,64 +36,12 @@ public class RocketAdminController : ControllerBase } } ` - } }, java: { langName: 'Java', mode: 'java', - snippet: { - single: `import java.util.Arrays; - import java.util.Map; - - import javax.crypto.Mac; - import javax.crypto.spec.SecretKeySpec; - - import org.springframework.http.HttpStatus; - import org.springframework.web.bind.annotation.PostMapping; - import org.springframework.web.bind.annotation.RequestBody; - import org.springframework.web.bind.annotation.RestController; - - @RestController - public class RocketadminController { - private static final String[] YOUR_TABLE_PRIMARY_KEYS_NAMES = {"id"}; - private static final String HMAC_SHA_256 = "HmacSHA256"; - private static final String YOUR_CONNECTION_SIGNING_KEY = "${signingKey}"; - - @PostMapping("/rocketadmin") - public void handleRocketadmin(@RequestBody Map bodyData) { - String rocketadminSignature = bodyData.get("Rocketadmin-Signature"); - - String stringifiedPkeys = Arrays.stream(YOUR_TABLE_PRIMARY_KEYS_NAMES) - .map(key -> key + "::" + bodyData.get(key)) - .reduce((str1, str2) -> str1 + "\n" + str2) - .orElse(""); - - String strTohash = String.join("$$", bodyData.get("$$_date"), stringifiedPkeys, bodyData.get("$$_actionId"), bodyData.get("$$_tableName")); - - String hash = hmacSHA256(strTohash, YOUR_CONNECTION_SIGNING_KEY); - if (hash.equals(rocketadminSignature)) { - // Your code here - } else { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signature invalid"); - } - } - - private static String hmacSHA256(String data, String key) { - try { - Mac hmac = Mac.getInstance(HMAC_SHA_256); - SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), HMAC_SHA_256); - hmac.init(secretKey); - byte[] hash = hmac.doFinal(data.getBytes("UTF-8")); - return DatatypeConverter.printHexBinary(hash).toLowerCase(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - `, - multiple: `import org.springframework.http.ResponseEntity; + snippet: `import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -148,7 +61,7 @@ public class RocketAdminController { @RequestHeader("rocketadmin-signature") String rocketadminSignature) throws NoSuchAlgorithmException, InvalidKeyException { String bodyToJson = new ObjectMapper().writeValueAsString(bodyData); - SecretKeySpec secretKeySpec = new SecretKeySpec("your_rocketadmin_signing_key".getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec("${signingKey}".getBytes(StandardCharsets.UTF_8), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(bodyToJson.getBytes(StandardCharsets.UTF_8)); @@ -164,52 +77,12 @@ public class RocketAdminController { } } ` - } }, php: { langName: 'PHP', mode: 'php', - snippet: { - single: `header("Rocketadmin-Signature"); - $bodyData = $request->all(); - $primaryKeysValues = []; - - foreach ($yourTablePrimaryKeysNames as $keyName) { - $primaryKeysValues[$keyName] = $bodyData[$keyName]; - } - - $stringifiedPkeys = ""; - foreach ($primaryKeysValues as $p => $val) { - $stringifiedPkeys .= "$p::$val\n"; - } - $stringifiedPkeys = rtrim($stringifiedPkeys); - - $strTohash = "{$bodyData['$$_date']}$$$stringifiedPkeys$\${$bodyData['$$_action_id']}$\${$bodyData['$$_table_name']}"; - $hmac = Hash::make("${signingKey}" . $strTohash); - - if (Hash::check($rocketadminSignature, $hmac)) { - // Your code here - return response()->json(['result' => 'success']); - } else { - return response()->json(['error' => 'Signature invalid'], 400); - } - } -} - `, - multiple: ` { - const [pKey] = ["id"]; - const rocketadminSignature = req.headers["Rocketadmin-Signature"]; - const bodyData = req.body; - const primaryKeysValues = { - [pKey]: bodyData[pKey] - }; - const stringifiedPkeys = Object.entries(primaryKeysValues) - .map(([key, value]) => \`\${key}::\${value}\`) - .join(""); - const strTohash = \`$\{bodyData["$$_date"]}\$\$\${stringifiedPkeys}$$\${bodyData["$$_actionId"]}$$\${bodyData["$$_tableName"]}\`; - const hmac = createHmac("sha256", "${signingKey}")\; - hmac.update(strTohash); - const hash = hmac.digest("hex"); - if (crypto.timingSafeEqual(hash, rocketadminSignature)) { - // Your code here - res.status(200).send(); - } else { - res.status(400).send("Signature invalid"); - } -}); - -app.listen(3000, () => console.log("Running on port 3000")); - `, - multiple: `const crypto = require("crypto"); + snippet: `const crypto = require("crypto"); app.use(express.json()); @@ -419,7 +192,7 @@ router.post("/rocketadmin", (req, res) => { const bodyToJson = JSON.stringify(bodyData); - const hmac = crypto.createHmac("sha256", "your_rocketadmin_signing_key"); + const hmac = crypto.createHmac("sha256", "${signingKey}"); hmac.update(bodyToJson); const hash = hmac.digest("hex"); @@ -433,7 +206,6 @@ router.post("/rocketadmin", (req, res) => { } }); ` - } } } } diff --git a/frontend/src/app/services/connections.service.ts b/frontend/src/app/services/connections.service.ts index 66919574c..a3d54a98d 100644 --- a/frontend/src/app/services/connections.service.ts +++ b/frontend/src/app/services/connections.service.ts @@ -57,6 +57,7 @@ export class ConnectionsService { public companyName: string; private connectionNameSubject: BehaviorSubject = new BehaviorSubject('Rocketadmin'); + private connectionSigningKeySubject: BehaviorSubject = new BehaviorSubject(null); constructor( private _http: HttpClient, @@ -126,6 +127,10 @@ export class ConnectionsService { return this.connectionNameSubject.asObservable(); } + getCurrentConnectionSigningKey() { + return this.connectionSigningKeySubject.asObservable(); + } + setConnectionID(id: string) { this.connectionID = id; } @@ -139,6 +144,7 @@ export class ConnectionsService { this.connectionAccessLevel = res.accessLevel; this.groupsAccessLevel = res.groupManagement; this.connectionNameSubject.next(res.connection.title || res.connection.database); + this.connectionSigningKeySubject.next(res.connection.signing_key); if (res.connectionProperties) { console.log('setConnectionInfo ui'); this.connectionLogo = res.connectionProperties.logo_url; From 395023e9262b32b28e2144fb0c59faa7b1ff5f66 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 30 Jan 2025 22:18:00 +0200 Subject: [PATCH 3/5] icon picker: fix title --- .../ui-components/icon-picker/icon-picker.component.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/ui-components/icon-picker/icon-picker.component.css b/frontend/src/app/components/ui-components/icon-picker/icon-picker.component.css index 847c760ca..a65c695f4 100644 --- a/frontend/src/app/components/ui-components/icon-picker/icon-picker.component.css +++ b/frontend/src/app/components/ui-components/icon-picker/icon-picker.component.css @@ -24,6 +24,8 @@ display: flex; align-items: center; justify-content: space-between; + color: var(--mat-sidenav-content-text-color); + margin-top: 8px; } .icons-list { @@ -32,7 +34,7 @@ grid-template-columns: repeat(5, 40px); grid-gap: 4px; justify-content: space-between; - margin-top: 12px; + margin-top: 8px; margin-bottom: 12px; } From ad64173f0176c96e105bd740d10f9010ba91aa79 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 30 Jan 2025 22:18:36 +0200 Subject: [PATCH 4/5] footer: update year --- frontend/src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 204f85ea5..ecc5f9228 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -183,7 +183,7 @@ From 6e13c8c1761904847c6d9aeabd45c050ebff2e7d Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 30 Jan 2025 22:24:39 +0200 Subject: [PATCH 5/5] fix unit tests --- frontend/src/app/services/auth.service.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/services/auth.service.spec.ts b/frontend/src/app/services/auth.service.spec.ts index 06e9c5cef..172d02230 100644 --- a/frontend/src/app/services/auth.service.spec.ts +++ b/frontend/src/app/services/auth.service.spec.ts @@ -1,5 +1,5 @@ -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { AlertActionType, AlertType } from '../models/alert'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { AuthService } from './auth.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -69,7 +69,7 @@ describe('AuthService', () => { isSignUpUserCalled = true; }); - const req = httpMock.expectOne('https://saas.rocketadmin.com/saas/user/register'); + const req = httpMock.expectOne('/saas/user/register'); expect(req.request.method).toBe("POST"); expect(req.request.body).toEqual(userData); req.flush(signUpResponse); @@ -85,7 +85,7 @@ describe('AuthService', () => { const tokenExpiration = service.signUpUser(userData).toPromise(); - const req = httpMock.expectOne('https://saas.rocketadmin.com/saas/user/register'); + const req = httpMock.expectOne('/saas/user/register'); expect(req.request.method).toBe("POST"); req.flush(fakeError, {status: 400, statusText: ''}); await tokenExpiration; @@ -185,7 +185,7 @@ describe('AuthService', () => { isLoginWithGoogleCalled = true; }); - const req = httpMock.expectOne(`https://saas.rocketadmin.com/saas/user/google/login`); + const req = httpMock.expectOne(`/saas/user/google/login`); expect(req.request.method).toBe("POST"); expect(req.request.body).toEqual({ token: 'google-token-12345678'}); req.flush(googleResponse); @@ -196,7 +196,7 @@ describe('AuthService', () => { it('should fall for loginWithGoogle and show Error alert', async () => { const googleResponse = service.loginWithGoogle('google-token-12345678').toPromise(); - const req = httpMock.expectOne(`https://saas.rocketadmin.com/saas/user/google/login`); + const req = httpMock.expectOne(`/saas/user/google/login`); expect(req.request.method).toBe("POST"); expect(req.request.body).toEqual({ token: 'google-token-12345678'}); req.flush(fakeError, {status: 400, statusText: ''});