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¶
- User klika "Login" w aplikacji
- Frontend przekierowuje do Azure AD B2C
- User loguje się lub rejestruje
- Azure AD B2C zwraca JWT token
- Frontend przechowuje token i używa go w API requests
- 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¶
// 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();
}
}
}