Przejdź do treści

Integracje

Wprowadzenie

System Masaku integruje się z wieloma usługami zewnętrznymi zapewniającymi autentykację, storage, OCR, email i inne funkcjonalności.

Azure AD B2C (Autentykacja)

Konfiguracja

Frontend (Angular):

// src/environments/de/environment.prod.ts
export const environment = {
  azureAdB2C: {
    clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
    authority: 'https://masakude.b2clogin.com/masakude.onmicrosoft.com/B2C_1_SignUpSignIn',
    redirectUri: 'https://app.masaku.de',
    postLogoutRedirectUri: 'https://app.masaku.de',
    scopes: ['https://masakude.onmicrosoft.com/api/read']
  }
};

// app.module.ts
import { MsalModule, MsalInterceptor } from '@azure/msal-angular';

@NgModule({
  imports: [
    MsalModule.forRoot({
      auth: environment.azureAdB2C
    }, {
      interactionType: InteractionType.Redirect,
      protectedResourceMap: new Map([
        [environment.apiUrl, environment.azureAdB2C.scopes]
      ])
    })
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

Backend (.NET):

// appsettings.json
{
  "AzureAdB2C": {
    "Instance": "https://masakude.b2clogin.com",
    "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "Domain": "masakude.onmicrosoft.com",
    "SignUpSignInPolicyId": "B2C_1_SignUpSignIn"
  }
}

// Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(options =>
    {
        Configuration.Bind("AzureAdB2C", options);
        options.TokenValidationParameters.NameClaimType = "name";
    },
    options => { Configuration.Bind("AzureAdB2C", options); });

User flow

  1. User klika "Login" w aplikacji
  2. Frontend przekierowuje do Azure AD B2C
  3. User loguje się lub rejestruje
  4. Azure AD B2C zwraca JWT token
  5. Frontend przechowuje token i używa go w API requests
  6. Backend waliduje token przy każdym request

Azure Blob Storage (Pliki)

Konfiguracja

Backend:

// appsettings.json
{
  "AzureStorage": {
    "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=masakustorage;AccountKey=...",
    "ContainerName": "receipts"
  }
}

// Services/BlobStorageService.cs
public class BlobStorageService : IBlobStorageService
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly string _containerName;

    public BlobStorageService(IConfiguration configuration)
    {
        var connectionString = configuration["AzureStorage:ConnectionString"];
        _blobServiceClient = new BlobServiceClient(connectionString);
        _containerName = configuration["AzureStorage:ContainerName"];
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient(fileName);
        await blobClient.UploadAsync(fileStream, overwrite: true);

        return blobClient.Uri.ToString();
    }

    public async Task<Stream> DownloadFileAsync(string fileName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
        var blobClient = containerClient.GetBlobClient(fileName);

        var response = await blobClient.DownloadAsync();
        return response.Value.Content;
    }

    public async Task DeleteFileAsync(string fileName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
        var blobClient = containerClient.GetBlobClient(fileName);

        await blobClient.DeleteIfExistsAsync();
    }
}

Upload z frontendu

// receipt-upload.service.ts
@Injectable()
export class ReceiptUploadService {
  private apiUrl = `${environment.apiUrl}/api/receipts`;

  constructor(private http: HttpClient) {}

  uploadReceipt(file: File): Observable<ReceiptDto> {
    const formData = new FormData();
    formData.append('file', file, file.name);

    return this.http.post<ReceiptDto>(`${this.apiUrl}/upload`, formData);
  }
}
// Controllers/ReceiptsController.cs
[HttpPost("upload")]
public async Task<IActionResult> Upload([FromForm] IFormFile file)
{
    if (file == null || file.Length == 0)
        return BadRequest("File is required");

    using var stream = file.OpenReadStream();
    var fileName = $"{Guid.NewGuid()}_{file.FileName}";
    var blobUrl = await _blobStorageService.UploadFileAsync(stream, fileName);

    var receipt = new Receipt
    {
        FileName = file.FileName,
        BlobUrl = blobUrl,
        UserId = User.GetUserId()
    };

    await _receiptService.CreateAsync(receipt);

    return Ok(_mapper.Map<ReceiptDto>(receipt));
}

Azure Cognitive Services (OCR)

Konfiguracja

// appsettings.json
{
  "CognitiveServices": {
    "Endpoint": "https://masakucognitive.cognitiveservices.azure.com/",
    "ApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

Implementacja

// Services/OcrService.cs
public class OcrService : IOcrService
{
    private readonly ComputerVisionClient _client;

    public OcrService(IConfiguration configuration)
    {
        var endpoint = configuration["CognitiveServices:Endpoint"];
        var apiKey = configuration["CognitiveServices:ApiKey"];

        _client = new ComputerVisionClient(new ApiKeyServiceClientCredentials(apiKey))
        {
            Endpoint = endpoint
        };
    }

    public async Task<string> ExtractTextAsync(string imageUrl)
    {
        var headers = await _client.ReadAsync(imageUrl);
        var operationId = headers.OperationLocation.Split('/').Last();

        ReadOperationResult result;
        do
        {
            await Task.Delay(1000);
            result = await _client.GetReadResultAsync(Guid.Parse(operationId));
        }
        while (result.Status == OperationStatusCodes.Running ||
               result.Status == OperationStatusCodes.NotStarted);

        var text = new StringBuilder();
        foreach (var page in result.AnalyzeResult.ReadResults)
        {
            foreach (var line in page.Lines)
            {
                text.AppendLine(line.Text);
            }
        }

        return text.ToString();
    }
}

MailHog / SendGrid (Email)

Local development (MailHog)

# docker-compose.yml
mailhog:
  image: mailhog/mailhog
  ports:
    - "1025:1025"  # SMTP
    - "8025:8025"  # Web UI

Web UI: http://localhost:8025

Production (SendGrid)

// appsettings.json
{
  "Email": {
    "Provider": "SendGrid",
    "SendGrid": {
      "ApiKey": "SG.xxxxxxxxxxxxxxxxxxxx",
      "FromEmail": "noreply@masaku.de",
      "FromName": "Masaku"
    }
  }
}
// Services/EmailService.cs
public class EmailService : IEmailService
{
    private readonly SendGridClient _client;
    private readonly string _fromEmail;
    private readonly string _fromName;

    public EmailService(IConfiguration configuration)
    {
        var apiKey = configuration["Email:SendGrid:ApiKey"];
        _client = new SendGridClient(apiKey);
        _fromEmail = configuration["Email:SendGrid:FromEmail"];
        _fromName = configuration["Email:SendGrid:FromName"];
    }

    public async Task SendEmailAsync(string toEmail, string subject, string body)
    {
        var from = new EmailAddress(_fromEmail, _fromName);
        var to = new EmailAddress(toEmail);
        var message = MailHelper.CreateSingleEmail(from, to, subject, body, body);

        var response = await _client.SendEmailAsync(message);

        if (response.StatusCode != System.Net.HttpStatusCode.Accepted)
        {
            throw new Exception($"Failed to send email: {response.StatusCode}");
        }
    }

    public async Task SendInvoiceEmailAsync(string toEmail, Invoice invoice, byte[] pdfBytes)
    {
        var from = new EmailAddress(_fromEmail, _fromName);
        var to = new EmailAddress(toEmail);

        var message = new SendGridMessage
        {
            From = from,
            Subject = $"Invoice {invoice.InvoiceNumber}",
            PlainTextContent = $"Please find attached invoice {invoice.InvoiceNumber}.",
            HtmlContent = $"<p>Please find attached invoice <strong>{invoice.InvoiceNumber}</strong>.</p>"
        };

        message.AddTo(to);
        message.AddAttachment(
            $"Invoice_{invoice.InvoiceNumber}.pdf",
            Convert.ToBase64String(pdfBytes),
            "application/pdf"
        );

        var response = await _client.SendEmailAsync(message);

        if (response.StatusCode != System.Net.HttpStatusCode.Accepted)
        {
            throw new Exception($"Failed to send invoice email: {response.StatusCode}");
        }
    }
}

Localise.biz (Translations)

API Integration

# Import translations
npm run i18n:import

# Export translations
npm run i18n:export
// scripts/import-translations.ts
import axios from 'axios';
import * as fs from 'fs';

const API_KEY = process.env.LOCALISE_API_KEY;
const PROJECT_ID = 'masaku';

async function importTranslations() {
  const locales = ['de', 'en', 'pl'];

  for (const locale of locales) {
    const response = await axios.get(
      `https://localise.biz/api/export/locale/${locale}.json`,
      {
        headers: {
          'Authorization': `Loco ${API_KEY}`
        }
      }
    );

    fs.writeFileSync(
      `src/assets/i18n/${locale}.json`,
      JSON.stringify(response.data, null, 2)
    );

    console.log(`Imported translations for ${locale}`);
  }
}

importTranslations();

Azure Application Insights (Monitoring)

Konfiguracja

// appsettings.json
{
  "ApplicationInsights": {
    "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}
// Program.cs
builder.Services.AddApplicationInsightsTelemetry(
    builder.Configuration["ApplicationInsights:InstrumentationKey"]
);

Custom Telemetry

public class BudgetService : IBudgetService
{
    private readonly TelemetryClient _telemetryClient;

    public BudgetService(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    public async Task<BudgetDto> CreateAsync(CreateBudgetDto dto)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            // Business logic
            var budget = await CreateBudgetInternalAsync(dto);

            _telemetryClient.TrackEvent("BudgetCreated", new Dictionary<string, string>
            {
                { "BudgetId", budget.Id.ToString() },
                { "Amount", budget.Amount.ToString() }
            });

            return budget;
        }
        catch (Exception ex)
        {
            _telemetryClient.TrackException(ex);
            throw;
        }
        finally
        {
            stopwatch.Stop();
            _telemetryClient.TrackMetric("BudgetCreationTime", stopwatch.ElapsedMilliseconds);
        }
    }
}

Payment Gateway (Stripe - opcjonalnie)

Konfiguracja

// appsettings.json
{
  "Stripe": {
    "SecretKey": "sk_test_xxxxxxxxxxxxxxxxxxxx",
    "PublishableKey": "pk_test_xxxxxxxxxxxxxxxxxxxx"
  }
}
// Services/PaymentService.cs
public class PaymentService : IPaymentService
{
    public PaymentService(IConfiguration configuration)
    {
        StripeConfiguration.ApiKey = configuration["Stripe:SecretKey"];
    }

    public async Task<PaymentIntent> CreatePaymentIntentAsync(decimal amount, string currency)
    {
        var options = new PaymentIntentCreateOptions
        {
            Amount = (long)(amount * 100), // cents
            Currency = currency.ToLower(),
            AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions
            {
                Enabled = true,
            },
        };

        var service = new PaymentIntentService();
        return await service.CreateAsync(options);
    }
}

Webhooks

Incoming webhooks (przykład: Stripe)

// Controllers/WebhooksController.cs
[ApiController]
[Route("api/webhooks")]
public class WebhooksController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IPaymentService _paymentService;

    [HttpPost("stripe")]
    public async Task<IActionResult> StripeWebhook()
    {
        var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
        var webhookSecret = _configuration["Stripe:WebhookSecret"];

        try
        {
            var stripeEvent = EventUtility.ConstructEvent(
                json,
                Request.Headers["Stripe-Signature"],
                webhookSecret
            );

            if (stripeEvent.Type == Events.PaymentIntentSucceeded)
            {
                var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
                await _paymentService.HandleSuccessfulPaymentAsync(paymentIntent.Id);
            }

            return Ok();
        }
        catch (StripeException)
        {
            return BadRequest();
        }
    }
}

Dalsze zasoby