@polymorphism-tech/morph-spec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -0
- package/bin/morph-spec.js +53 -0
- package/content/.claude/commands/morph-apply.md +66 -0
- package/content/.claude/commands/morph-archive.md +79 -0
- package/content/.claude/commands/morph-costs.md +206 -0
- package/content/.claude/commands/morph-infra.md +209 -0
- package/content/.claude/commands/morph-proposal.md +60 -0
- package/content/.claude/commands/morph-status.md +71 -0
- package/content/.claude/settings.local.json +15 -0
- package/content/.claude/skills/infra/bicep-architect.md +419 -0
- package/content/.claude/skills/infra/container-specialist.md +437 -0
- package/content/.claude/skills/infra/devops-engineer.md +405 -0
- package/content/.claude/skills/integrations/asaas-financial.md +333 -0
- package/content/.claude/skills/integrations/azure-identity.md +309 -0
- package/content/.claude/skills/integrations/clerk-auth.md +290 -0
- package/content/.claude/skills/specialists/azure-architect.md +142 -0
- package/content/.claude/skills/specialists/cost-guardian.md +110 -0
- package/content/.claude/skills/specialists/ef-modeler.md +200 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +245 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +209 -0
- package/content/.claude/skills/specialists/po-pm-advisor.md +197 -0
- package/content/.claude/skills/specialists/standards-architect.md +78 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +325 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +352 -0
- package/content/.claude/skills/stacks/dotnet-nextjs.md +402 -0
- package/content/.claude/skills/stacks/shopify.md +445 -0
- package/content/.morph/archive/.gitkeep +25 -0
- package/content/.morph/config/agents.json +149 -0
- package/content/.morph/config/config.template.json +96 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -0
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -0
- package/content/.morph/examples/api-nextjs/spec.md +399 -0
- package/content/.morph/examples/api-nextjs/tasks.md +168 -0
- package/content/.morph/examples/micro-saas/README.md +125 -0
- package/content/.morph/examples/micro-saas/contracts.cs +358 -0
- package/content/.morph/examples/micro-saas/decisions.md +246 -0
- package/content/.morph/examples/micro-saas/spec.md +236 -0
- package/content/.morph/examples/micro-saas/tasks.md +150 -0
- package/content/.morph/examples/multi-agent/README.md +309 -0
- package/content/.morph/examples/multi-agent/contracts.cs +433 -0
- package/content/.morph/examples/multi-agent/spec.md +479 -0
- package/content/.morph/examples/multi-agent/tasks.md +185 -0
- package/content/.morph/features/.gitkeep +25 -0
- package/content/.morph/project.md +159 -0
- package/content/.morph/specs/.gitkeep +20 -0
- package/content/.morph/standards/architecture.md +190 -0
- package/content/.morph/standards/azure.md +184 -0
- package/content/.morph/standards/coding.md +342 -0
- package/content/.morph/templates/agent.cs +172 -0
- package/content/.morph/templates/component.razor +239 -0
- package/content/.morph/templates/contracts.cs +217 -0
- package/content/.morph/templates/decisions.md +106 -0
- package/content/.morph/templates/infra/app-insights.bicep +63 -0
- package/content/.morph/templates/infra/container-app-env.bicep +49 -0
- package/content/.morph/templates/infra/container-app.bicep +156 -0
- package/content/.morph/templates/infra/key-vault.bicep +91 -0
- package/content/.morph/templates/infra/main.bicep +155 -0
- package/content/.morph/templates/infra/parameters.dev.json +23 -0
- package/content/.morph/templates/infra/parameters.prod.json +23 -0
- package/content/.morph/templates/infra/sql-database.bicep +103 -0
- package/content/.morph/templates/infra/storage.bicep +106 -0
- package/content/.morph/templates/integrations/asaas-client.cs +387 -0
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/content/.morph/templates/integrations/clerk-config.cs +258 -0
- package/content/.morph/templates/job.cs +171 -0
- package/content/.morph/templates/migration.cs +83 -0
- package/content/.morph/templates/proposal.md +155 -0
- package/content/.morph/templates/recap.md +105 -0
- package/content/.morph/templates/repository.cs +141 -0
- package/content/.morph/templates/saas/subscription.cs +347 -0
- package/content/.morph/templates/saas/tenant.cs +338 -0
- package/content/.morph/templates/service.cs +139 -0
- package/content/.morph/templates/spec.md +147 -0
- package/content/.morph/templates/tasks.md +235 -0
- package/content/.morph/templates/test.cs +239 -0
- package/content/CLAUDE.md +318 -0
- package/package.json +50 -0
- package/src/commands/doctor.js +132 -0
- package/src/commands/init.js +121 -0
- package/src/commands/update.js +84 -0
- package/src/utils/file-copier.js +50 -0
- package/src/utils/logger.js +32 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Asaas Webhook Controller Template
|
|
3
|
+
// Controller para receber webhooks do Asaas
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
using System.Text.Json.Serialization;
|
|
7
|
+
using Microsoft.AspNetCore.Mvc;
|
|
8
|
+
using Microsoft.Extensions.Logging;
|
|
9
|
+
|
|
10
|
+
namespace {{Namespace}}.Web.Controllers;
|
|
11
|
+
|
|
12
|
+
// ==============================================================================
|
|
13
|
+
// CONTROLLER
|
|
14
|
+
// ==============================================================================
|
|
15
|
+
|
|
16
|
+
[ApiController]
|
|
17
|
+
[Route("api/webhooks/asaas")]
|
|
18
|
+
public class AsaasWebhookController : ControllerBase
|
|
19
|
+
{
|
|
20
|
+
private readonly IAsaasWebhookHandler _webhookHandler;
|
|
21
|
+
private readonly ILogger<AsaasWebhookController> _logger;
|
|
22
|
+
|
|
23
|
+
public AsaasWebhookController(
|
|
24
|
+
IAsaasWebhookHandler webhookHandler,
|
|
25
|
+
ILogger<AsaasWebhookController> logger)
|
|
26
|
+
{
|
|
27
|
+
_webhookHandler = webhookHandler;
|
|
28
|
+
_logger = logger;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[HttpPost]
|
|
32
|
+
public async Task<IActionResult> HandleWebhook([FromBody] AsaasWebhookPayload payload)
|
|
33
|
+
{
|
|
34
|
+
_logger.LogInformation("Received Asaas webhook: {Event}", payload.Event);
|
|
35
|
+
|
|
36
|
+
try
|
|
37
|
+
{
|
|
38
|
+
await _webhookHandler.HandleAsync(payload);
|
|
39
|
+
return Ok();
|
|
40
|
+
}
|
|
41
|
+
catch (Exception ex)
|
|
42
|
+
{
|
|
43
|
+
_logger.LogError(ex, "Error processing Asaas webhook: {Event}", payload.Event);
|
|
44
|
+
// Retorna 200 para evitar retentativas desnecessárias
|
|
45
|
+
// O erro será tratado internamente
|
|
46
|
+
return Ok();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ==============================================================================
|
|
52
|
+
// WEBHOOK HANDLER INTERFACE
|
|
53
|
+
// ==============================================================================
|
|
54
|
+
|
|
55
|
+
public interface IAsaasWebhookHandler
|
|
56
|
+
{
|
|
57
|
+
Task HandleAsync(AsaasWebhookPayload payload, CancellationToken ct = default);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==============================================================================
|
|
61
|
+
// WEBHOOK HANDLER IMPLEMENTATION
|
|
62
|
+
// ==============================================================================
|
|
63
|
+
|
|
64
|
+
public class AsaasWebhookHandler : IAsaasWebhookHandler
|
|
65
|
+
{
|
|
66
|
+
private readonly IPaymentService _paymentService;
|
|
67
|
+
private readonly ISubscriptionService _subscriptionService;
|
|
68
|
+
private readonly ILogger<AsaasWebhookHandler> _logger;
|
|
69
|
+
|
|
70
|
+
public AsaasWebhookHandler(
|
|
71
|
+
IPaymentService paymentService,
|
|
72
|
+
ISubscriptionService subscriptionService,
|
|
73
|
+
ILogger<AsaasWebhookHandler> logger)
|
|
74
|
+
{
|
|
75
|
+
_paymentService = paymentService;
|
|
76
|
+
_subscriptionService = subscriptionService;
|
|
77
|
+
_logger = logger;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async Task HandleAsync(AsaasWebhookPayload payload, CancellationToken ct = default)
|
|
81
|
+
{
|
|
82
|
+
_logger.LogInformation("Processing Asaas webhook: {Event} for {PaymentId}",
|
|
83
|
+
payload.Event, payload.Payment?.Id ?? "N/A");
|
|
84
|
+
|
|
85
|
+
switch (payload.Event)
|
|
86
|
+
{
|
|
87
|
+
// =========================================================
|
|
88
|
+
// PAYMENT EVENTS
|
|
89
|
+
// =========================================================
|
|
90
|
+
|
|
91
|
+
case AsaasWebhookEvents.PaymentCreated:
|
|
92
|
+
await HandlePaymentCreatedAsync(payload, ct);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case AsaasWebhookEvents.PaymentAwaitingRiskAnalysis:
|
|
96
|
+
await HandlePaymentAwaitingRiskAsync(payload, ct);
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case AsaasWebhookEvents.PaymentPending:
|
|
100
|
+
await HandlePaymentPendingAsync(payload, ct);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case AsaasWebhookEvents.PaymentConfirmed:
|
|
104
|
+
case AsaasWebhookEvents.PaymentReceived:
|
|
105
|
+
await HandlePaymentConfirmedAsync(payload, ct);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case AsaasWebhookEvents.PaymentOverdue:
|
|
109
|
+
await HandlePaymentOverdueAsync(payload, ct);
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case AsaasWebhookEvents.PaymentRefunded:
|
|
113
|
+
case AsaasWebhookEvents.PaymentRefundInProgress:
|
|
114
|
+
await HandlePaymentRefundedAsync(payload, ct);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case AsaasWebhookEvents.PaymentChargebackRequested:
|
|
118
|
+
case AsaasWebhookEvents.PaymentChargebackDispute:
|
|
119
|
+
await HandlePaymentChargebackAsync(payload, ct);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case AsaasWebhookEvents.PaymentDeleted:
|
|
123
|
+
await HandlePaymentDeletedAsync(payload, ct);
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
// =========================================================
|
|
127
|
+
// SUBSCRIPTION EVENTS
|
|
128
|
+
// =========================================================
|
|
129
|
+
|
|
130
|
+
case AsaasWebhookEvents.SubscriptionCreated:
|
|
131
|
+
await HandleSubscriptionCreatedAsync(payload, ct);
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case AsaasWebhookEvents.SubscriptionUpdated:
|
|
135
|
+
await HandleSubscriptionUpdatedAsync(payload, ct);
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case AsaasWebhookEvents.SubscriptionDeleted:
|
|
139
|
+
await HandleSubscriptionDeletedAsync(payload, ct);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
_logger.LogWarning("Unhandled Asaas webhook event: {Event}", payload.Event);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// =========================================================================
|
|
149
|
+
// PAYMENT HANDLERS
|
|
150
|
+
// =========================================================================
|
|
151
|
+
|
|
152
|
+
private async Task HandlePaymentCreatedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
153
|
+
{
|
|
154
|
+
if (payload.Payment is null) return;
|
|
155
|
+
|
|
156
|
+
_logger.LogInformation("Payment created: {PaymentId}", payload.Payment.Id);
|
|
157
|
+
await _paymentService.SyncPaymentAsync(payload.Payment.Id, ct);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async Task HandlePaymentAwaitingRiskAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
161
|
+
{
|
|
162
|
+
if (payload.Payment is null) return;
|
|
163
|
+
|
|
164
|
+
_logger.LogInformation("Payment awaiting risk analysis: {PaymentId}", payload.Payment.Id);
|
|
165
|
+
await _paymentService.UpdateStatusAsync(payload.Payment.Id, PaymentStatus.AwaitingRisk, ct);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async Task HandlePaymentPendingAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
169
|
+
{
|
|
170
|
+
if (payload.Payment is null) return;
|
|
171
|
+
|
|
172
|
+
_logger.LogInformation("Payment pending: {PaymentId}", payload.Payment.Id);
|
|
173
|
+
await _paymentService.UpdateStatusAsync(payload.Payment.Id, PaymentStatus.Pending, ct);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async Task HandlePaymentConfirmedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
177
|
+
{
|
|
178
|
+
if (payload.Payment is null) return;
|
|
179
|
+
|
|
180
|
+
_logger.LogInformation("Payment confirmed: {PaymentId}, Value: {Value}",
|
|
181
|
+
payload.Payment.Id, payload.Payment.Value);
|
|
182
|
+
|
|
183
|
+
await _paymentService.ConfirmPaymentAsync(payload.Payment.Id, ct);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async Task HandlePaymentOverdueAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
187
|
+
{
|
|
188
|
+
if (payload.Payment is null) return;
|
|
189
|
+
|
|
190
|
+
_logger.LogWarning("Payment overdue: {PaymentId}", payload.Payment.Id);
|
|
191
|
+
await _paymentService.MarkOverdueAsync(payload.Payment.Id, ct);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async Task HandlePaymentRefundedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
195
|
+
{
|
|
196
|
+
if (payload.Payment is null) return;
|
|
197
|
+
|
|
198
|
+
_logger.LogInformation("Payment refunded: {PaymentId}", payload.Payment.Id);
|
|
199
|
+
await _paymentService.RefundPaymentAsync(payload.Payment.Id, ct);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async Task HandlePaymentChargebackAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
203
|
+
{
|
|
204
|
+
if (payload.Payment is null) return;
|
|
205
|
+
|
|
206
|
+
_logger.LogWarning("Payment chargeback: {PaymentId}", payload.Payment.Id);
|
|
207
|
+
await _paymentService.HandleChargebackAsync(payload.Payment.Id, ct);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async Task HandlePaymentDeletedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
211
|
+
{
|
|
212
|
+
if (payload.Payment is null) return;
|
|
213
|
+
|
|
214
|
+
_logger.LogInformation("Payment deleted: {PaymentId}", payload.Payment.Id);
|
|
215
|
+
await _paymentService.DeletePaymentAsync(payload.Payment.Id, ct);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// SUBSCRIPTION HANDLERS
|
|
220
|
+
// =========================================================================
|
|
221
|
+
|
|
222
|
+
private async Task HandleSubscriptionCreatedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
223
|
+
{
|
|
224
|
+
// Subscription events may not have payment data
|
|
225
|
+
_logger.LogInformation("Subscription created via webhook");
|
|
226
|
+
// Implement as needed
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async Task HandleSubscriptionUpdatedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
230
|
+
{
|
|
231
|
+
_logger.LogInformation("Subscription updated via webhook");
|
|
232
|
+
// Implement as needed
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async Task HandleSubscriptionDeletedAsync(AsaasWebhookPayload payload, CancellationToken ct)
|
|
236
|
+
{
|
|
237
|
+
_logger.LogInformation("Subscription deleted via webhook");
|
|
238
|
+
// Implement as needed
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ==============================================================================
|
|
243
|
+
// WEBHOOK EVENTS
|
|
244
|
+
// ==============================================================================
|
|
245
|
+
|
|
246
|
+
public static class AsaasWebhookEvents
|
|
247
|
+
{
|
|
248
|
+
// Payment events
|
|
249
|
+
public const string PaymentCreated = "PAYMENT_CREATED";
|
|
250
|
+
public const string PaymentAwaitingRiskAnalysis = "PAYMENT_AWAITING_RISK_ANALYSIS";
|
|
251
|
+
public const string PaymentPending = "PAYMENT_PENDING";
|
|
252
|
+
public const string PaymentConfirmed = "PAYMENT_CONFIRMED";
|
|
253
|
+
public const string PaymentReceived = "PAYMENT_RECEIVED";
|
|
254
|
+
public const string PaymentOverdue = "PAYMENT_OVERDUE";
|
|
255
|
+
public const string PaymentRefunded = "PAYMENT_REFUNDED";
|
|
256
|
+
public const string PaymentRefundInProgress = "PAYMENT_REFUND_IN_PROGRESS";
|
|
257
|
+
public const string PaymentChargebackRequested = "PAYMENT_CHARGEBACK_REQUESTED";
|
|
258
|
+
public const string PaymentChargebackDispute = "PAYMENT_CHARGEBACK_DISPUTE";
|
|
259
|
+
public const string PaymentDeleted = "PAYMENT_DELETED";
|
|
260
|
+
|
|
261
|
+
// Subscription events
|
|
262
|
+
public const string SubscriptionCreated = "SUBSCRIPTION_CREATED";
|
|
263
|
+
public const string SubscriptionUpdated = "SUBSCRIPTION_UPDATED";
|
|
264
|
+
public const string SubscriptionDeleted = "SUBSCRIPTION_DELETED";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ==============================================================================
|
|
268
|
+
// WEBHOOK PAYLOAD
|
|
269
|
+
// ==============================================================================
|
|
270
|
+
|
|
271
|
+
public record AsaasWebhookPayload
|
|
272
|
+
{
|
|
273
|
+
[JsonPropertyName("event")]
|
|
274
|
+
public string Event { get; init; } = string.Empty;
|
|
275
|
+
|
|
276
|
+
[JsonPropertyName("payment")]
|
|
277
|
+
public AsaasWebhookPayment? Payment { get; init; }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public record AsaasWebhookPayment
|
|
281
|
+
{
|
|
282
|
+
[JsonPropertyName("id")]
|
|
283
|
+
public string Id { get; init; } = string.Empty;
|
|
284
|
+
|
|
285
|
+
[JsonPropertyName("customer")]
|
|
286
|
+
public string Customer { get; init; } = string.Empty;
|
|
287
|
+
|
|
288
|
+
[JsonPropertyName("value")]
|
|
289
|
+
public decimal Value { get; init; }
|
|
290
|
+
|
|
291
|
+
[JsonPropertyName("status")]
|
|
292
|
+
public string Status { get; init; } = string.Empty;
|
|
293
|
+
|
|
294
|
+
[JsonPropertyName("billingType")]
|
|
295
|
+
public string BillingType { get; init; } = string.Empty;
|
|
296
|
+
|
|
297
|
+
[JsonPropertyName("externalReference")]
|
|
298
|
+
public string? ExternalReference { get; init; }
|
|
299
|
+
|
|
300
|
+
[JsonPropertyName("subscription")]
|
|
301
|
+
public string? Subscription { get; init; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ==============================================================================
|
|
305
|
+
// PAYMENT STATUS ENUM
|
|
306
|
+
// ==============================================================================
|
|
307
|
+
|
|
308
|
+
public enum PaymentStatus
|
|
309
|
+
{
|
|
310
|
+
Pending,
|
|
311
|
+
AwaitingRisk,
|
|
312
|
+
Confirmed,
|
|
313
|
+
Received,
|
|
314
|
+
Overdue,
|
|
315
|
+
Refunded,
|
|
316
|
+
Chargeback,
|
|
317
|
+
Deleted
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ==============================================================================
|
|
321
|
+
// SERVICE INTERFACES (to be implemented)
|
|
322
|
+
// ==============================================================================
|
|
323
|
+
|
|
324
|
+
public interface IPaymentService
|
|
325
|
+
{
|
|
326
|
+
Task SyncPaymentAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
327
|
+
Task UpdateStatusAsync(string asaasPaymentId, PaymentStatus status, CancellationToken ct = default);
|
|
328
|
+
Task ConfirmPaymentAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
329
|
+
Task MarkOverdueAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
330
|
+
Task RefundPaymentAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
331
|
+
Task HandleChargebackAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
332
|
+
Task DeletePaymentAsync(string asaasPaymentId, CancellationToken ct = default);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public interface ISubscriptionService
|
|
336
|
+
{
|
|
337
|
+
// Implement as needed
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ==============================================================================
|
|
341
|
+
// DEPENDENCY INJECTION
|
|
342
|
+
// ==============================================================================
|
|
343
|
+
|
|
344
|
+
public static class AsaasWebhookServiceExtensions
|
|
345
|
+
{
|
|
346
|
+
public static IServiceCollection AddAsaasWebhook(this IServiceCollection services)
|
|
347
|
+
{
|
|
348
|
+
services.AddScoped<IAsaasWebhookHandler, AsaasWebhookHandler>();
|
|
349
|
+
return services;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Azure Identity (Microsoft Identity) Configuration Template
|
|
3
|
+
// Configuração de autenticação com Microsoft Identity Platform
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
using Microsoft.AspNetCore.Authentication;
|
|
7
|
+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
|
8
|
+
using Microsoft.AspNetCore.Components.Authorization;
|
|
9
|
+
using Microsoft.Identity.Web;
|
|
10
|
+
using Microsoft.Identity.Web.UI;
|
|
11
|
+
using System.Security.Claims;
|
|
12
|
+
|
|
13
|
+
namespace {{Namespace}}.Infrastructure.Auth;
|
|
14
|
+
|
|
15
|
+
// ==============================================================================
|
|
16
|
+
// SERVICE EXTENSIONS
|
|
17
|
+
// ==============================================================================
|
|
18
|
+
|
|
19
|
+
public static class AzureIdentityServiceExtensions
|
|
20
|
+
{
|
|
21
|
+
/// <summary>
|
|
22
|
+
/// Adiciona autenticação Microsoft Identity para Web Apps
|
|
23
|
+
/// Requer: Microsoft.Identity.Web, Microsoft.Identity.Web.UI
|
|
24
|
+
/// </summary>
|
|
25
|
+
public static IServiceCollection AddAzureIdentityAuthentication(
|
|
26
|
+
this IServiceCollection services,
|
|
27
|
+
IConfiguration configuration)
|
|
28
|
+
{
|
|
29
|
+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
30
|
+
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"));
|
|
31
|
+
|
|
32
|
+
services.AddControllersWithViews()
|
|
33
|
+
.AddMicrosoftIdentityUI();
|
|
34
|
+
|
|
35
|
+
services.AddAuthorization(options =>
|
|
36
|
+
{
|
|
37
|
+
// Default policy - requires authenticated user
|
|
38
|
+
options.FallbackPolicy = options.DefaultPolicy;
|
|
39
|
+
|
|
40
|
+
// Custom policies
|
|
41
|
+
options.AddPolicy("RequireAdmin", policy =>
|
|
42
|
+
policy.RequireRole("Admin"));
|
|
43
|
+
|
|
44
|
+
options.AddPolicy("RequireManager", policy =>
|
|
45
|
+
policy.RequireAssertion(context =>
|
|
46
|
+
context.User.IsInRole("Admin") ||
|
|
47
|
+
context.User.IsInRole("Manager")));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return services;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// Adiciona autenticação Microsoft Identity para Blazor Server
|
|
55
|
+
/// </summary>
|
|
56
|
+
public static IServiceCollection AddAzureIdentityBlazor(
|
|
57
|
+
this IServiceCollection services,
|
|
58
|
+
IConfiguration configuration)
|
|
59
|
+
{
|
|
60
|
+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
61
|
+
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"));
|
|
62
|
+
|
|
63
|
+
services.AddControllersWithViews()
|
|
64
|
+
.AddMicrosoftIdentityUI();
|
|
65
|
+
|
|
66
|
+
services.AddRazorPages();
|
|
67
|
+
services.AddServerSideBlazor()
|
|
68
|
+
.AddMicrosoftIdentityConsentHandler();
|
|
69
|
+
|
|
70
|
+
services.AddAuthorization();
|
|
71
|
+
|
|
72
|
+
return services;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// <summary>
|
|
76
|
+
/// Adiciona autenticação Microsoft Identity para APIs (JWT Bearer)
|
|
77
|
+
/// </summary>
|
|
78
|
+
public static IServiceCollection AddAzureIdentityApi(
|
|
79
|
+
this IServiceCollection services,
|
|
80
|
+
IConfiguration configuration)
|
|
81
|
+
{
|
|
82
|
+
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
83
|
+
.AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"));
|
|
84
|
+
|
|
85
|
+
services.AddAuthorization();
|
|
86
|
+
|
|
87
|
+
return services;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// <summary>
|
|
91
|
+
/// Adiciona suporte para chamar APIs downstream (Graph, custom APIs)
|
|
92
|
+
/// </summary>
|
|
93
|
+
public static IServiceCollection AddAzureIdentityWithDownstreamApi(
|
|
94
|
+
this IServiceCollection services,
|
|
95
|
+
IConfiguration configuration)
|
|
96
|
+
{
|
|
97
|
+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
98
|
+
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"))
|
|
99
|
+
.EnableTokenAcquisitionToCallDownstreamApi()
|
|
100
|
+
.AddMicrosoftGraph(configuration.GetSection("Graph"))
|
|
101
|
+
.AddInMemoryTokenCaches();
|
|
102
|
+
|
|
103
|
+
return services;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ==============================================================================
|
|
108
|
+
// USER SERVICE
|
|
109
|
+
// ==============================================================================
|
|
110
|
+
|
|
111
|
+
public interface IAzureIdentityUserService
|
|
112
|
+
{
|
|
113
|
+
string? GetUserId();
|
|
114
|
+
string? GetUserEmail();
|
|
115
|
+
string? GetUserName();
|
|
116
|
+
IEnumerable<string> GetUserRoles();
|
|
117
|
+
bool IsAuthenticated();
|
|
118
|
+
bool IsInRole(string role);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public class AzureIdentityUserService : IAzureIdentityUserService
|
|
122
|
+
{
|
|
123
|
+
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
124
|
+
|
|
125
|
+
public AzureIdentityUserService(IHttpContextAccessor httpContextAccessor)
|
|
126
|
+
{
|
|
127
|
+
_httpContextAccessor = httpContextAccessor;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User;
|
|
131
|
+
|
|
132
|
+
public string? GetUserId() =>
|
|
133
|
+
User?.FindFirstValue(ClaimTypes.NameIdentifier) ??
|
|
134
|
+
User?.FindFirstValue("oid"); // Azure AD Object ID
|
|
135
|
+
|
|
136
|
+
public string? GetUserEmail() =>
|
|
137
|
+
User?.FindFirstValue(ClaimTypes.Email) ??
|
|
138
|
+
User?.FindFirstValue("preferred_username");
|
|
139
|
+
|
|
140
|
+
public string? GetUserName() =>
|
|
141
|
+
User?.FindFirstValue(ClaimTypes.Name) ??
|
|
142
|
+
User?.FindFirstValue("name");
|
|
143
|
+
|
|
144
|
+
public IEnumerable<string> GetUserRoles() =>
|
|
145
|
+
User?.FindAll(ClaimTypes.Role).Select(c => c.Value) ??
|
|
146
|
+
Enumerable.Empty<string>();
|
|
147
|
+
|
|
148
|
+
public bool IsAuthenticated() =>
|
|
149
|
+
User?.Identity?.IsAuthenticated ?? false;
|
|
150
|
+
|
|
151
|
+
public bool IsInRole(string role) =>
|
|
152
|
+
User?.IsInRole(role) ?? false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ==============================================================================
|
|
156
|
+
// MIDDLEWARE PIPELINE
|
|
157
|
+
// ==============================================================================
|
|
158
|
+
|
|
159
|
+
public static class AzureIdentityMiddlewareExtensions
|
|
160
|
+
{
|
|
161
|
+
public static IApplicationBuilder UseAzureIdentityAuthentication(this IApplicationBuilder app)
|
|
162
|
+
{
|
|
163
|
+
app.UseAuthentication();
|
|
164
|
+
app.UseAuthorization();
|
|
165
|
+
return app;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ==============================================================================
|
|
170
|
+
// BLAZOR COMPONENTS HELPER
|
|
171
|
+
// ==============================================================================
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
<!-- App.razor -->
|
|
175
|
+
<CascadingAuthenticationState>
|
|
176
|
+
<Router AppAssembly="@typeof(App).Assembly">
|
|
177
|
+
<Found Context="routeData">
|
|
178
|
+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
179
|
+
<NotAuthorized>
|
|
180
|
+
@if (!context.User.Identity?.IsAuthenticated ?? true)
|
|
181
|
+
{
|
|
182
|
+
<RedirectToLogin />
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
{
|
|
186
|
+
<p>Você não tem permissão para acessar este recurso.</p>
|
|
187
|
+
}
|
|
188
|
+
</NotAuthorized>
|
|
189
|
+
</AuthorizeRouteView>
|
|
190
|
+
</Found>
|
|
191
|
+
</Router>
|
|
192
|
+
</CascadingAuthenticationState>
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
// ==============================================================================
|
|
196
|
+
// REDIRECT TO LOGIN COMPONENT
|
|
197
|
+
// ==============================================================================
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
<!-- Components/RedirectToLogin.razor -->
|
|
201
|
+
@inject NavigationManager Navigation
|
|
202
|
+
|
|
203
|
+
@code {
|
|
204
|
+
protected override void OnInitialized()
|
|
205
|
+
{
|
|
206
|
+
var returnUrl = Uri.EscapeDataString(Navigation.Uri);
|
|
207
|
+
Navigation.NavigateTo($"MicrosoftIdentity/Account/SignIn?redirectUri={returnUrl}", forceLoad: true);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
// ==============================================================================
|
|
213
|
+
// APPSETTINGS EXAMPLE
|
|
214
|
+
// ==============================================================================
|
|
215
|
+
|
|
216
|
+
/*
|
|
217
|
+
{
|
|
218
|
+
"AzureAd": {
|
|
219
|
+
"Instance": "https://login.microsoftonline.com/",
|
|
220
|
+
"Domain": "yourdomain.onmicrosoft.com",
|
|
221
|
+
"TenantId": "your-tenant-id",
|
|
222
|
+
"ClientId": "your-client-id",
|
|
223
|
+
"ClientSecret": "your-client-secret",
|
|
224
|
+
"CallbackPath": "/signin-oidc"
|
|
225
|
+
},
|
|
226
|
+
"Graph": {
|
|
227
|
+
"BaseUrl": "https://graph.microsoft.com/v1.0",
|
|
228
|
+
"Scopes": "User.Read"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
// ==============================================================================
|
|
234
|
+
// PROGRAM.CS EXAMPLES
|
|
235
|
+
// ==============================================================================
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
// Web App (MVC/Razor Pages)
|
|
239
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
240
|
+
builder.Services.AddAzureIdentityAuthentication(builder.Configuration);
|
|
241
|
+
|
|
242
|
+
var app = builder.Build();
|
|
243
|
+
app.UseAzureIdentityAuthentication();
|
|
244
|
+
app.MapControllers();
|
|
245
|
+
app.Run();
|
|
246
|
+
|
|
247
|
+
// Blazor Server
|
|
248
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
249
|
+
builder.Services.AddAzureIdentityBlazor(builder.Configuration);
|
|
250
|
+
|
|
251
|
+
var app = builder.Build();
|
|
252
|
+
app.UseAzureIdentityAuthentication();
|
|
253
|
+
app.MapBlazorHub();
|
|
254
|
+
app.MapFallbackToPage("/_Host");
|
|
255
|
+
app.Run();
|
|
256
|
+
|
|
257
|
+
// API (JWT Bearer)
|
|
258
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
259
|
+
builder.Services.AddAzureIdentityApi(builder.Configuration);
|
|
260
|
+
|
|
261
|
+
var app = builder.Build();
|
|
262
|
+
app.UseAzureIdentityAuthentication();
|
|
263
|
+
app.MapControllers().RequireAuthorization();
|
|
264
|
+
app.Run();
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
// ==============================================================================
|
|
268
|
+
// AZURE AD B2C CONFIGURATION
|
|
269
|
+
// ==============================================================================
|
|
270
|
+
|
|
271
|
+
/*
|
|
272
|
+
// For Azure AD B2C, use this configuration:
|
|
273
|
+
{
|
|
274
|
+
"AzureAdB2C": {
|
|
275
|
+
"Instance": "https://yourtenant.b2clogin.com",
|
|
276
|
+
"Domain": "yourtenant.onmicrosoft.com",
|
|
277
|
+
"TenantId": "your-tenant-id",
|
|
278
|
+
"ClientId": "your-client-id",
|
|
279
|
+
"SignUpSignInPolicyId": "B2C_1_signupsignin",
|
|
280
|
+
"ResetPasswordPolicyId": "B2C_1_passwordreset",
|
|
281
|
+
"EditProfilePolicyId": "B2C_1_editprofile"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// And use:
|
|
286
|
+
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
287
|
+
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAdB2C"));
|
|
288
|
+
*/
|