From 27c44ef91e517ebbac926257115f1b11f00e2992 Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:39:43 +0100 Subject: [PATCH 1/3] [DEV-4425] CH resources storage account (#2990) * [DEV-4425] Add storage account to core infra * [DEV-4425] Add storage account replication * [DEV-4425] db-bak not needed in the replication * [DEV-4425] Added script to check the replication * [DEV-4425] Connect Application Insights to Log Analytics Workspace --- infrastructure/bicep/core/core.bicep | 10 +++ .../core/modules/applicationInsights.bicep | 6 +- .../bicep/core/modules/logAnalytics.bicep | 3 + .../bicep/core/modules/storageAccount.bicep | 64 +++++++++++++++++ .../storage-replication/check-replication.sh | 69 +++++++++++++++++++ .../core/storage-replication/copy-initial.sh | 51 ++++++++++++++ .../bicep/core/storage-replication/deploy.sh | 60 ++++++++++++++++ .../storageObjectReplicationDestination.bicep | 43 ++++++++++++ .../storageObjectReplicationSource.bicep | 35 ++++++++++ 9 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 infrastructure/bicep/core/modules/storageAccount.bicep create mode 100755 infrastructure/bicep/core/storage-replication/check-replication.sh create mode 100755 infrastructure/bicep/core/storage-replication/copy-initial.sh create mode 100755 infrastructure/bicep/core/storage-replication/deploy.sh create mode 100644 infrastructure/bicep/core/storage-replication/storageObjectReplicationDestination.bicep create mode 100644 infrastructure/bicep/core/storage-replication/storageObjectReplicationSource.bicep diff --git a/infrastructure/bicep/core/core.bicep b/infrastructure/bicep/core/core.bicep index 96e1f42d78..fead6c5b6d 100644 --- a/infrastructure/bicep/core/core.bicep +++ b/infrastructure/bicep/core/core.bicep @@ -31,6 +31,8 @@ var applicationInsightsName = 'appi-${compName}-${rg}-${env}' var sqlServerName = 'sql-${compName}-${rg}-${env}' var sqlDbName = 'sqldb-${compName}-${rg}-${env}' +var storageAccountName = 'st${compName}${rg}${env}' + // --- MODULES --- // module networkSecurityGroup './modules/networkSecurityGroups.bicep' = { name: 'networkSecurityGroup' @@ -61,6 +63,7 @@ module applicationInsights './modules/applicationInsights.bicep' = { name: 'applicationInsights' params: { applicationInsightsName: applicationInsightsName + logAnalyticsWorkspaceId: logAnalytics.outputs.workspaceId } } @@ -80,3 +83,10 @@ module sqlServer './modules/sqlServer.bicep' = { virtualNetworks ] } + +module storageAccount './modules/storageAccount.bicep' = { + name: 'storageAccount' + params: { + storageAccountName: storageAccountName + } +} diff --git a/infrastructure/bicep/core/modules/applicationInsights.bicep b/infrastructure/bicep/core/modules/applicationInsights.bicep index c27d6825d9..ec65d31a10 100644 --- a/infrastructure/bicep/core/modules/applicationInsights.bicep +++ b/infrastructure/bicep/core/modules/applicationInsights.bicep @@ -5,6 +5,9 @@ param location string = resourceGroup().location @description('Name of the application insights') param applicationInsightsName string +@description('Resource ID of the Log Analytics Workspace') +param logAnalyticsWorkspaceId string + // --- RESOURCES --- // resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { name: applicationInsightsName @@ -12,7 +15,8 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { kind: 'web' properties: { Application_Type: 'web' - IngestionMode: 'ApplicationInsights' + IngestionMode: 'LogAnalytics' + WorkspaceResourceId: logAnalyticsWorkspaceId publicNetworkAccessForIngestion: 'Enabled' publicNetworkAccessForQuery: 'Enabled' } diff --git a/infrastructure/bicep/core/modules/logAnalytics.bicep b/infrastructure/bicep/core/modules/logAnalytics.bicep index b163a0d161..774c58f4e2 100644 --- a/infrastructure/bicep/core/modules/logAnalytics.bicep +++ b/infrastructure/bicep/core/modules/logAnalytics.bicep @@ -21,3 +21,6 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-02 publicNetworkAccessForQuery: 'Enabled' } } + +// --- OUTPUT --- // +output workspaceId string = logAnalyticsWorkspace.id diff --git a/infrastructure/bicep/core/modules/storageAccount.bicep b/infrastructure/bicep/core/modules/storageAccount.bicep new file mode 100644 index 0000000000..462b8842f9 --- /dev/null +++ b/infrastructure/bicep/core/modules/storageAccount.bicep @@ -0,0 +1,64 @@ +// --- PARAMETERS --- // +@description('Azure Location/Region') +param location string = resourceGroup().location + +@description('Name of the storage account') +param storageAccountName string + +@description('SKU for the storage account') +@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS']) +param skuName string = 'Standard_LRS' + +@description('Enable blob versioning (required for object replication)') +param enableVersioning bool = true + +@description('Enable change feed (required for object replication)') +param enableChangeFeed bool = true + +@description('Container names to create') +param containerNames array = [ + 'kyc' + 'support' +] + +// --- RESOURCES --- // +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageAccountName + location: location + sku: { + name: skuName + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + minimumTlsVersion: 'TLS1_2' + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + } +} + +resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { + parent: storageAccount + name: 'default' + properties: { + isVersioningEnabled: enableVersioning + changeFeed: { + enabled: enableChangeFeed + } + } +} + +resource containers 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = [ + for containerName in containerNames: { + parent: blobServices + name: containerName + properties: { + publicAccess: 'None' + } + } +] + +// --- OUTPUT --- // +output storageAccountId string = storageAccount.id +output storageAccountName string = storageAccount.name +output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob diff --git a/infrastructure/bicep/core/storage-replication/check-replication.sh b/infrastructure/bicep/core/storage-replication/check-replication.sh new file mode 100755 index 0000000000..7ec9ebc6d7 --- /dev/null +++ b/infrastructure/bicep/core/storage-replication/check-replication.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -e + +# --- OPTIONS --- # +environmentOptions=("dev" "prd") + +# --- FUNCTIONS --- # +selectOption() { + PS3="${1}: " + shift + options=("$@") + + select opt in "${options[@]}" "quit"; do + case "$REPLY" in + *) selection="${opt}"; break ;; + esac + done + + if [[ ! $selection || $selection == "quit" ]]; then exit -1; fi + echo "${selection}" +} + +ENV=$(selectOption "Select Environment" "${environmentOptions[@]}") + +# Account Keys abrufen +SOURCE_KEY=$(az storage account keys list \ + --account-name stdfxapi${ENV} \ + --query "[0].value" -o tsv) + +DEST_KEY=$(az storage account keys list \ + --account-name stdfxcore${ENV} \ + --query "[0].value" -o tsv) + +# SAS Tokens generieren (gültig für 1 Stunde) +SOURCE_SAS=$(az storage account generate-sas \ + --account-name stdfxapi${ENV} \ + --account-key "${SOURCE_KEY}" \ + --permissions rl \ + --resource-types co \ + --services b \ + --expiry $(date -u -v+1H '+%Y-%m-%dT%H:%MZ') \ + -o tsv) + +DEST_SAS=$(az storage account generate-sas \ + --account-name stdfxcore${ENV} \ + --account-key "${DEST_KEY}" \ + --permissions rl \ + --resource-types co \ + --services b \ + --expiry $(date -u -v+1H '+%Y-%m-%dT%H:%MZ') \ + -o tsv) + +# Container vergleichen +echo "" +echo "Vergleiche kyc Container:" +azcopy sync \ + "https://stdfxapi${ENV}.blob.core.windows.net/kyc?${SOURCE_SAS}" \ + "https://stdfxcore${ENV}.blob.core.windows.net/kyc?${DEST_SAS}" \ + --dry-run + +echo "" +echo "Vergleiche support Container:" +azcopy sync \ + "https://stdfxapi${ENV}.blob.core.windows.net/support?${SOURCE_SAS}" \ + "https://stdfxcore${ENV}.blob.core.windows.net/support?${DEST_SAS}" \ + --dry-run + +echo "" +echo "Ende." diff --git a/infrastructure/bicep/core/storage-replication/copy-initial.sh b/infrastructure/bicep/core/storage-replication/copy-initial.sh new file mode 100755 index 0000000000..f8b2deab07 --- /dev/null +++ b/infrastructure/bicep/core/storage-replication/copy-initial.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -e + +# --- OPTIONS --- # +environmentOptions=("dev" "prd") + +# --- FUNCTIONS --- # +selectOption() { + PS3="${1}: " + shift + options=("$@") + + select opt in "${options[@]}" "quit"; do + case "$REPLY" in + *) selection="${opt}"; break ;; + esac + done + + if [[ ! $selection || $selection == "quit" ]]; then exit -1; fi + echo "${selection}" +} + +ENV=$(selectOption "Select Environment" "${environmentOptions[@]}") + +# SAS Tokens generieren (gültig für 1 Stunde) +SOURCE_SAS=$(az storage account generate-sas \ + --account-name stdfxapi${ENV} \ + --permissions rl \ + --resource-types co \ + --services b \ + --expiry $(date -u -v+1H '+%Y-%m-%dT%H:%MZ') \ + -o tsv) + +DEST_SAS=$(az storage account generate-sas \ + --account-name stdfxcore${ENV} \ + --permissions rwdlac \ + --resource-types co \ + --services b \ + --expiry $(date -u -v+1H '+%Y-%m-%dT%H:%MZ') \ + -o tsv) + +# Kopieren +azcopy copy \ + "https://stdfxapi${ENV}.blob.core.windows.net/kyc/*?${SOURCE_SAS}" \ + "https://stdfxcore${ENV}.blob.core.windows.net/kyc/?${DEST_SAS}" \ + --recursive + +azcopy copy \ + "https://stdfxapi${ENV}.blob.core.windows.net/support/*?${SOURCE_SAS}" \ + "https://stdfxcore${ENV}.blob.core.windows.net/support/?${DEST_SAS}" \ + --recursive diff --git a/infrastructure/bicep/core/storage-replication/deploy.sh b/infrastructure/bicep/core/storage-replication/deploy.sh new file mode 100755 index 0000000000..ab0ba8df62 --- /dev/null +++ b/infrastructure/bicep/core/storage-replication/deploy.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -e + +# --- OPTIONS --- # +environmentOptions=("dev" "prd") + +# --- FUNCTIONS --- # +selectOption() { + PS3="${1}: " + shift + options=("$@") + + select opt in "${options[@]}" "quit"; do + case "$REPLY" in + *) selection="${opt}"; break ;; + esac + done + + if [[ ! $selection || $selection == "quit" ]]; then exit -1; fi + echo "${selection}" +} + +ENV=$(selectOption "Select Environment" "${environmentOptions[@]}") + +echo "Schritt 1: Subscription-ID holen" +SUB_ID=$(az account show --query id -o tsv) + +echo "Sub-ID: $SUB_ID" + +echo "Schritt 2: Destination Policy (rg-dfx-core-${ENV})" +az deployment group create \ + --resource-group rg-dfx-core-${ENV} \ + --template-file ./storageObjectReplicationDestination.bicep \ + --parameters \ + destinationStorageAccountName=stdfxcore${ENV} \ + sourceStorageAccountId="/subscriptions/${SUB_ID}/resourceGroups/rg-dfx-api-${ENV}/providers/Microsoft.Storage/storageAccounts/stdfxapi${ENV}" + +echo "Schritt 3: Policy-ID und Rules von Azure holen" +POLICY_ID=$(az storage account or-policy list \ + --account-name stdfxcore${ENV} \ + --resource-group rg-dfx-core-${ENV} \ + --query "[0].policyId" -o tsv) + +RULES=$(az storage account or-policy list \ + --account-name stdfxcore${ENV} \ + --resource-group rg-dfx-core-${ENV} \ + --query "[0].rules" -o json) + +echo "Policy-ID: $POLICY_ID" +echo "Rules: $RULES" + +echo "Schritt 4: Source Policy (rg-dfx-api-${ENV})" +az deployment group create \ + --resource-group rg-dfx-api-${ENV} \ + --template-file ./storageObjectReplicationSource.bicep \ + --parameters \ + sourceStorageAccountName=stdfxapi${ENV} \ + destinationStorageAccountId="/subscriptions/${SUB_ID}/resourceGroups/rg-dfx-core-${ENV}/providers/Microsoft.Storage/storageAccounts/stdfxcore${ENV}" \ + replicationPolicyId="${POLICY_ID}" \ + containerRules="${RULES}" diff --git a/infrastructure/bicep/core/storage-replication/storageObjectReplicationDestination.bicep b/infrastructure/bicep/core/storage-replication/storageObjectReplicationDestination.bicep new file mode 100644 index 0000000000..7bd71473fc --- /dev/null +++ b/infrastructure/bicep/core/storage-replication/storageObjectReplicationDestination.bicep @@ -0,0 +1,43 @@ +// --- PARAMETERS --- // +@description('Name of the destination storage account') +param destinationStorageAccountName string + +@description('Resource ID of the source storage account') +param sourceStorageAccountId string + +@description('Container rules for replication') +param containerRules array = [ + { sourceContainer: 'kyc', destinationContainer: 'kyc' } + { sourceContainer: 'support', destinationContainer: 'support' } +] + +// --- EXISTING RESOURCES --- // +resource destinationStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: destinationStorageAccountName +} + +// --- RESOURCES --- // +resource destinationReplicationPolicy 'Microsoft.Storage/storageAccounts/objectReplicationPolicies@2023-01-01' = { + parent: destinationStorageAccount + name: 'default' + properties: { + sourceAccount: sourceStorageAccountId + destinationAccount: destinationStorageAccount.id + rules: [ + for rule in containerRules: { + sourceContainer: rule.sourceContainer + destinationContainer: rule.destinationContainer + filters: contains(rule, 'prefixMatch') + ? { + prefixMatch: rule.prefixMatch + } + : null + } + ] + } +} + +// --- OUTPUT --- // +// Die Policy-ID wird aus der Resource-ID extrahiert (letztes Segment = generierte GUID) +output policyId string = last(split(destinationReplicationPolicy.id, '/')) +output destinationStorageAccountId string = destinationStorageAccount.id diff --git a/infrastructure/bicep/core/storage-replication/storageObjectReplicationSource.bicep b/infrastructure/bicep/core/storage-replication/storageObjectReplicationSource.bicep new file mode 100644 index 0000000000..b9f19c3d6b --- /dev/null +++ b/infrastructure/bicep/core/storage-replication/storageObjectReplicationSource.bicep @@ -0,0 +1,35 @@ +// --- PARAMETERS --- // +@description('Name of the source storage account') +param sourceStorageAccountName string + +@description('Resource ID of the destination storage account') +param destinationStorageAccountId string + +@description('Policy ID from the destination deployment') +param replicationPolicyId string + +@description('Container rules from destination policy (including ruleId)') +param containerRules array + +// --- EXISTING RESOURCES --- // +resource sourceStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: sourceStorageAccountName +} + +// --- RESOURCES --- // +resource sourceReplicationPolicy 'Microsoft.Storage/storageAccounts/objectReplicationPolicies@2023-01-01' = { + parent: sourceStorageAccount + name: replicationPolicyId + properties: { + sourceAccount: sourceStorageAccount.id + destinationAccount: destinationStorageAccountId + rules: [for rule in containerRules: { + ruleId: rule.ruleId + sourceContainer: rule.sourceContainer + destinationContainer: rule.destinationContainer + }] + } +} + +// --- OUTPUT --- // +output replicationPolicyId string = sourceReplicationPolicy.name From d6c560647aa9c7b9cb7459da290dc35793ff5d80 Mon Sep 17 00:00:00 2001 From: Yannick <52333989+Yannick1712@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:21:02 +0100 Subject: [PATCH 2/3] [NOTASK] Remove unused code (#3018) --- .../generic/user/models/user-data/user-data.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index 276a7ecd20..acdc49e8cb 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -14,7 +14,6 @@ import JSZip from 'jszip'; import { Config } from 'src/config/config'; import { CreateAccount } from 'src/integration/sift/dto/sift.dto'; import { SiftService } from 'src/integration/sift/services/sift.service'; -import { UserRole } from 'src/shared/auth/user-role.enum'; import { CountryService } from 'src/shared/models/country/country.service'; import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { IpLogService } from 'src/shared/models/ip-log/ip-log.service'; @@ -768,10 +767,6 @@ export class UserDataService { } // --- HELPER METHODS --- // - private async hasRole(userDataId: number, role: UserRole): Promise { - return this.userRepo.existsBy({ userData: { id: userDataId }, role }); - } - private async loadRelationsAndVerify( userData: Partial | UserData, dto: UpdateUserDataDto | CreateUserDataDto, From 8271876fffbe34d1fd22f12c4818d2f43a245acb Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:06:26 +0100 Subject: [PATCH 3/3] fix: Yapeal transaction enrichment, swap error log, Bitcoin pay-in bug (#3026) --- .../bank-tx/bank-tx/services/bank-tx.service.ts | 15 ++++++++++----- .../dex/services/base/dex-evm.service.ts | 11 +++-------- .../supporting/dex/services/dex.service.ts | 2 +- .../payin/services/payin-bitcoin.service.ts | 4 +++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts index bf566b2dc1..63eca31a53 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts @@ -126,18 +126,23 @@ export class BankTxService implements OnModuleInit { @DfxCron(CronExpression.EVERY_5_MINUTES, { process: Process.BANK_TX }) async enrichYapealTransactions(): Promise { - const transactions = await this.bankTxRepo.findBy([ - { created: MoreThan(Util.minutesBefore(30)), familyCode: 'CCRD' }, // credit card => wrong data - ]); + const transactions = await this.bankTxRepo.find({ + where: { familyCode: 'CCRD' }, // credit card => wrong data + order: { id: 'DESC' }, + take: 100, + }); if (transactions.length === 0) return; - const today = new Date(); const ibanGroups = Util.groupBy(transactions, 'accountIban'); for (const [accountIban, groupTransactions] of ibanGroups) { try { - const yapealTransactions = await this.yapealService.getTransactions(accountIban, today, today); + const dates = groupTransactions.map((tx) => (tx.bookingDate ?? tx.created).getTime()); + const fromDate = new Date(Math.min(...dates)); + const toDate = new Date(Math.max(...dates)); + + const yapealTransactions = await this.yapealService.getTransactions(accountIban, fromDate, toDate); for (const transaction of groupTransactions) { const yapealTx = yapealTransactions.find((tx) => tx.accountServiceRef === transaction.accountServiceRef); diff --git a/src/subdomains/supporting/dex/services/base/dex-evm.service.ts b/src/subdomains/supporting/dex/services/base/dex-evm.service.ts index e0ea10e1b6..a3afd601c2 100644 --- a/src/subdomains/supporting/dex/services/base/dex-evm.service.ts +++ b/src/subdomains/supporting/dex/services/base/dex-evm.service.ts @@ -72,14 +72,9 @@ export abstract class DexEvmService implements PurchaseDexService { ): Promise { if (sourceAsset.id === targetAsset.id) return sourceAmount; - return Util.retry( - () => - poolFee != null - ? this.#client.testSwapPool(sourceAsset, sourceAmount, targetAsset, poolFee) - : this.#client.testSwap(sourceAsset, sourceAmount, targetAsset, maxSlippage), - 3, - 1000, - ).then((r) => r.targetAmount); + return poolFee != null + ? this.#client.testSwapPool(sourceAsset, sourceAmount, targetAsset, poolFee).then((r) => r.targetAmount) + : this.#client.testSwap(sourceAsset, sourceAmount, targetAsset, maxSlippage).then((r) => r.targetAmount); } async swap(swapAsset: Asset, swapAmount: number, targetAsset: Asset, maxSlippage: number): Promise { diff --git a/src/subdomains/supporting/dex/services/dex.service.ts b/src/subdomains/supporting/dex/services/dex.service.ts index 5d910cb424..392780bbf8 100644 --- a/src/subdomains/supporting/dex/services/dex.service.ts +++ b/src/subdomains/supporting/dex/services/dex.service.ts @@ -297,7 +297,7 @@ export class DexService { try { return await strategy.calculatePrice(from, to, poolFee); } catch (e) { - this.logger.error('Error while getting target amount:', e); + this.logger.error(`Error while getting target amount (${from.uniqueName} -> ${to.uniqueName}):`, e); // default public exception throw new Error(`Error while getting price from ${from.uniqueName} to ${to.uniqueName}.`); diff --git a/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts b/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts index 55e5eec80c..26644517d2 100644 --- a/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts +++ b/src/subdomains/supporting/payin/services/payin-bitcoin.service.ts @@ -53,7 +53,9 @@ export class PayInBitcoinService extends PayInBitcoinBasedService { this.nodeCallQueue.handle(async () => { const command = `getrawtransaction "${utxo.txid}" 2`; const transaction = await this.client.sendCliCommand(command); - const senderAddresses = transaction.vin.map((vin) => vin.prevout.scriptPubKey.address); + const senderAddresses = transaction.vin + .filter((vin) => vin.prevout?.scriptPubKey?.address) + .map((vin) => vin.prevout.scriptPubKey.address); utxo.prevoutAddresses = [...new Set(senderAddresses)]; utxo.isUnconfirmed = utxo.confirmations === 0; }),