Skip to content

Commit 114c92f

Browse files
Finalized SNS subscription support (SQS only)
1 parent 1230487 commit 114c92f

File tree

9 files changed

+274
-20
lines changed

9 files changed

+274
-20
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
using System.Net;
12+
using System.Threading.Tasks;
13+
using Amazon.SimpleNotificationService;
14+
using GuiStack.Extensions;
15+
using GuiStack.Models;
16+
using GuiStack.Repositories;
17+
using Microsoft.AspNetCore.Mvc;
18+
19+
namespace GuiStack.Controllers.SNS
20+
{
21+
[ApiController]
22+
[Route("api/" + nameof(SNS) + "/[controller]")]
23+
public class SubscriptionsController : Controller
24+
{
25+
private ISNSRepository snsRepository;
26+
27+
public SubscriptionsController(ISNSRepository snsRepository)
28+
{
29+
this.snsRepository = snsRepository;
30+
}
31+
32+
private ActionResult HandleException(Exception ex)
33+
{
34+
if(ex == null)
35+
throw new ArgumentNullException(nameof(ex));
36+
37+
if(ex is AmazonSimpleNotificationServiceException snsEx)
38+
{
39+
if(snsEx.StatusCode == HttpStatusCode.NotFound)
40+
return StatusCode((int)snsEx.StatusCode, new { error = snsEx.Message });
41+
42+
Console.Error.WriteLine(snsEx);
43+
return StatusCode((int)HttpStatusCode.InternalServerError, new { error = ex.Message });
44+
}
45+
46+
Console.Error.WriteLine(ex);
47+
return StatusCode((int)HttpStatusCode.InternalServerError);
48+
}
49+
50+
[HttpPost]
51+
[Consumes("application/json")]
52+
[Produces("application/json")]
53+
public async Task<ActionResult> CreateSubscription([FromBody] SNSCreateSubscriptionModel model)
54+
{
55+
if(string.IsNullOrWhiteSpace(model.TopicArn))
56+
return StatusCode((int)HttpStatusCode.BadRequest, new { error = "'topicArn' cannot be empty" });
57+
58+
if(string.IsNullOrWhiteSpace(model.Protocol))
59+
return StatusCode((int)HttpStatusCode.BadRequest, new { error = "'protocol' cannot be empty" });
60+
61+
if(string.IsNullOrWhiteSpace(model.Endpoint))
62+
return StatusCode((int)HttpStatusCode.BadRequest, new { error = "'endpoint' cannot be empty" });
63+
64+
if(!model.Protocol.Equals("sqs", StringComparison.OrdinalIgnoreCase))
65+
return StatusCode((int)HttpStatusCode.BadRequest, new { error = $"Protocol '{model.Protocol}' is not supported. Supported protocols are: sqs" });
66+
67+
try
68+
{
69+
await snsRepository.CreateTopicSubscriptionAsync(model.TopicArn, model.Endpoint);
70+
return Ok();
71+
}
72+
catch(Exception ex)
73+
{
74+
return HandleException(ex);
75+
}
76+
}
77+
78+
[HttpDelete("{subscriptionArn}")]
79+
public async Task<ActionResult> DeleteSubscription(string subscriptionArn)
80+
{
81+
if(string.IsNullOrWhiteSpace(subscriptionArn))
82+
return StatusCode((int)HttpStatusCode.BadRequest);
83+
84+
subscriptionArn = subscriptionArn.DecodeRouteParameter();
85+
86+
try
87+
{
88+
await snsRepository.DeleteSubscriptionAsync(subscriptionArn);
89+
return Ok();
90+
}
91+
catch(Exception ex)
92+
{
93+
return HandleException(ex);
94+
}
95+
}
96+
}
97+
}

GuiStack/Controllers/SNS/TopicsController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* License, v. 2.0. If a copy of the MPL was not distributed with this
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*
6-
* Copyright © Vincent Bengtsson & Contributors 2022-2023
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
77
* https://github.com/Visual-Vincent/GuiStack
88
*/
99

@@ -47,7 +47,7 @@ private ActionResult HandleException(Exception ex)
4747
return StatusCode((int)HttpStatusCode.InternalServerError);
4848
}
4949

50-
[HttpPut]
50+
[HttpPost]
5151
[Consumes("application/json")]
5252
public async Task<ActionResult> CreateTopic([FromBody] SNSCreateTopicModel model)
5353
{

GuiStack/Controllers/SQS/QueuesController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* License, v. 2.0. If a copy of the MPL was not distributed with this
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*
6-
* Copyright © Vincent Bengtsson & Contributors 2022
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
77
* https://github.com/Visual-Vincent/GuiStack
88
*/
99

@@ -121,7 +121,7 @@ public async Task<ActionResult> SendMessage([FromRoute] string queueName, [FromB
121121

122122
// This re-encoding to Base64 is intentional, as I want to rely on .NET's Base64 implementation to ensure that
123123
// in the event that encoding isn't properly performed by the client, we don't send incorrectly encoded messages.
124-
// (Insead, the decoding above will likely throw an error)
124+
// (Instead, the decoding above will likely throw an error)
125125

126126
if(message.Base64Encode)
127127
body = Convert.ToBase64String(protoData);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
12+
namespace GuiStack.Models
13+
{
14+
public class SNSCreateSubscriptionModel
15+
{
16+
public string TopicArn { get; set; }
17+
public string Protocol { get; set; }
18+
public string Endpoint { get; set; }
19+
}
20+
}

GuiStack/Pages/SNS/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ else
129129
var dedupe = document.getElementById("new-sns-topic-dedupe-checkbox").checked;
130130
131131
var response = await fetch("@Url.Action("CreateTopic", "Topics")", {
132-
method: "PUT",
132+
method: "POST",
133133
headers: new Headers({ "Content-Type": "application/json" }),
134134
body: JSON.stringify({
135135
topicName: topicName,

GuiStack/Pages/SNS/_TopicInfo.cshtml

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* https://github.com/Visual-Vincent/GuiStack
88
*@
99

10+
@using Amazon;
1011
@using GuiStack.Models
1112
@using GuiStack.Extensions
1213
@using GuiStack.Repositories
@@ -15,7 +16,7 @@
1516
<div id="sns-subscribe-to-sqs-modal" class="cssWindow dark backdropblur text-center">
1617
<div class="closeWindowButton"><a no-href onclick="closeParentWindow(event)">×</a></div>
1718

18-
<h2 class="title">Subscribe to SQS queue</h2>
19+
<h2 class="title">Subscribe SQS queue to Topic</h2>
1920
<div class="queue-selector-container text-left" style="margin-top: 16px; margin-bottom: 16px">
2021
Loading queues...
2122
</div>
@@ -25,6 +26,10 @@
2526
</div>
2627
</div>
2728

29+
@{
30+
await Html.RenderPartialAsync("~/Pages/Shared/_DeleteModal.cshtml", new DeleteModalModel("delete-sns-subscription-modal"));
31+
}
32+
2833
<table class="gs-info-table colored">
2934
<tbody>
3035
<tr>
@@ -85,22 +90,45 @@
8590
<table class="gs-list padded autosize-all-cols-but-first">
8691
<thead>
8792
<tr>
88-
<th>Protocol</th>
8993
<th>Endpoint</th>
94+
<th>Protocol</th>
9095
<th>Owner</th>
9196
<th>Actions</th>
9297
</tr>
9398
</thead>
9499
<tbody>
95-
@foreach(var subscription in Model.Subscriptions)
100+
@foreach(var subscription in Model.Subscriptions.OrderBy(s => s.Protocol).ThenBy(s => s.Endpoint))
96101
{
97-
<tr>
102+
<tr data-arn="@subscription.Arn" data-endpoint="@subscription.Endpoint">
103+
@if(subscription.Protocol == null)
104+
{
105+
<td>@subscription.Endpoint</td>
106+
}
107+
else
108+
{
109+
<td>
110+
@switch(subscription.Protocol.ToLower())
111+
{
112+
case "sqs":
113+
{
114+
if (Arn.TryParse(subscription.Endpoint, out var arn))
115+
{
116+
<a href="/SQS/@arn.Resource"><i class="fa-solid fa-database gs-rotate-270" style="margin-right: 2px"></i> @arn.Resource</a>
117+
}
118+
else
119+
{
120+
<td>@subscription.Endpoint</td>
121+
}
122+
break;
123+
}
124+
}
125+
</td>
126+
}
98127
<td>@subscription.Protocol</td>
99-
<td>@subscription.Endpoint</td>
100-
<td>@subscription.Owner</td>
128+
<td>@(!string.IsNullOrWhiteSpace(subscription.Owner) ? subscription.Owner : "(unknown)")</td>
101129
<td>
102130
<div class="gs-icons">
103-
<a no-href class="purple lnk-sns-sub-copyarn" title="Copy ARN">
131+
<a no-href class="purple lnk-sns-sub-copyarn" title="Copy Endpoint address/ARN">
104132
<i class="fa-solid fa-link"></i>
105133
</a>
106134
<a no-href class="red lnk-sns-sub-delete" title="Delete">
@@ -117,6 +145,14 @@
117145
var subscribeToSqsModal = document.getElementById("sns-subscribe-to-sqs-modal");
118146
var queueSelectorContainer = $("#sns-subscribe-to-sqs-modal .queue-selector-container");
119147
148+
$(".lnk-sns-sub-copyarn").click(sns_CopySubscriptionArn);
149+
$(".lnk-sns-sub-delete").click(sns_DeleteSubscriptionPrompt);
150+
$("#delete-sns-subscription-modal .yes-button").click(sns_DeleteSubscription);
151+
152+
const SNS_URL_DELETE_SUBSCRIPTION = "@Url.Action("DeleteSubscription", "Subscriptions", new { subscriptionArn = "__SUBSCRIPTIONARN__" })";
153+
154+
var prompt_SubscriptionArn;
155+
120156
subscribeToSqsModal.addEventListener("windowopened", function() {
121157
$.ajax({
122158
type: "GET",
@@ -136,7 +172,7 @@
136172
queueSelectorContainer.html("Loading queues...");
137173
});
138174
139-
function sns_CreateSubscription_Click()
175+
async function sns_CreateSubscription_Click()
140176
{
141177
var selectedItem = gs_GetSelectedTableItem(queueSelectorContainer[0]);
142178
@@ -146,7 +182,88 @@
146182
return;
147183
}
148184
149-
// TODO: Create subscription
150-
alert(selectedItem.getAttribute("data-queue-name"));
185+
var queueUrl = selectedItem.getAttribute("data-url");
186+
187+
try
188+
{
189+
var response = await fetch("@Url.Action("CreateSubscription", "Subscriptions")", {
190+
method: "POST",
191+
headers: new Headers({ "Content-Type": "application/json" }),
192+
body: JSON.stringify({
193+
topicArn: "@Model.Topic.TopicARN",
194+
protocol: "sqs",
195+
endpoint: queueUrl
196+
})
197+
});
198+
199+
if(!response.ok) {
200+
throw "Failed to create SNS subscription: Server returned HTTP status " + response.status;
201+
}
202+
203+
window.location.reload(true);
204+
}
205+
catch(error)
206+
{
207+
gs_DisplayError(error);
208+
}
209+
210+
closeWindow("sns-subscribe-to-sqs-modal");
211+
}
212+
213+
function sns_CopySubscriptionArn(event)
214+
{
215+
try
216+
{
217+
var arn = gs_GetParentTableRow(event.currentTarget, true).getAttribute("data-arn");
218+
navigator.clipboard.writeText(arn);
219+
}
220+
catch(error)
221+
{
222+
gs_DisplayError(error);
223+
}
224+
}
225+
226+
function sns_DeleteSubscriptionPrompt(event)
227+
{
228+
try
229+
{
230+
var objNameElement = document.querySelector("#delete-sns-subscription-modal .title .object-name");
231+
var parentRow = gs_GetParentTableRow(event.currentTarget, true);
232+
var endpoint = parentRow.getAttribute("data-endpoint");
233+
234+
prompt_SubscriptionArn = parentRow.getAttribute("data-arn");
235+
236+
objNameElement.innerHTML = '<span style="color: #FFFFFF">the subscription</span> ' + endpoint;
237+
showWindow("delete-sns-subscription-modal");
238+
}
239+
catch(error)
240+
{
241+
gs_DisplayError(error);
242+
}
243+
}
244+
245+
async function sns_DeleteSubscription()
246+
{
247+
try
248+
{
249+
var subscriptionArn = encodeURIComponent(prompt_SubscriptionArn);
250+
251+
var url = SNS_URL_DELETE_SUBSCRIPTION
252+
.replace("__SUBSCRIPTIONARN__", subscriptionArn);
253+
254+
var response = await fetch(url, { method: "DELETE" });
255+
256+
if(!response.ok) {
257+
throw "Failed to delete SNS subscription: Server returned HTTP status " + response.status;
258+
}
259+
260+
window.location.reload(true);
261+
}
262+
catch(error)
263+
{
264+
gs_DisplayError(error);
265+
}
266+
267+
closeWindow("delete-sns-subscription-modal");
151268
}
152269
</script>

GuiStack/Pages/SNS/_TopicsTable.cshtml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
* License, v. 2.0. If a copy of the MPL was not distributed with this
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*
6-
* Copyright © Vincent Bengtsson & Contributors 2022-2023
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
77
* https://github.com/Visual-Vincent/GuiStack
88
*@
99

1010
@using GuiStack.Models;
1111
@model IEnumerable<SNSTopic>
1212

13-
@{ await Html.RenderPartialAsync("~/Pages/Shared/_DeleteModal.cshtml", new DeleteModalModel("deleteSNSTopicModal")); }
13+
@{
14+
await Html.RenderPartialAsync("~/Pages/Shared/_DeleteModal.cshtml", new DeleteModalModel("deleteSNSTopicModal"));
15+
}
1416

1517
<table class="gs-list padded autosize-all-cols-but-first">
1618
<thead>

0 commit comments

Comments
 (0)