Skip to content

Commit

Permalink
Push notifications sample. (dotnet#492)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbritch authored Jul 15, 2024
1 parent c437fb7 commit 7adbd6b
Show file tree
Hide file tree
Showing 60 changed files with 2,034 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;

namespace PushNotificationsAPI.Authentication;

public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
{
const string ApiKeyIdentifier = "apikey";

public ApiKeyAuthHandler(IOptionsMonitor<ApiKeyAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string key = string.Empty;

if (Request.Headers[ApiKeyIdentifier].Any())
{
key = Request.Headers[ApiKeyIdentifier].FirstOrDefault();
}
else if (Request.Query.ContainsKey(ApiKeyIdentifier))
{
if (Request.Query.TryGetValue(ApiKeyIdentifier, out var queryKey))
key = queryKey;
}

if (string.IsNullOrWhiteSpace(key))
return Task.FromResult(AuthenticateResult.Fail("No api key provided"));

if (!string.Equals(key, Options.ApiKey, StringComparison.Ordinal))
return Task.FromResult(AuthenticateResult.Fail("Invalid api key."));

var identities = new List<ClaimsIdentity>
{
new ClaimsIdentity("ApiKeyIdentity")
};

var ticket = new AuthenticationTicket(new ClaimsPrincipal(identities), Options.Scheme);

return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authentication;

namespace PushNotificationsAPI.Authentication;

public class ApiKeyAuthOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public string Scheme => DefaultScheme;
public string ApiKey { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authentication;

namespace PushNotificationsAPI.Authentication;

public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeyAuth(this AuthenticationBuilder builder, Action<ApiKeyAuthOptions> configureOptions)
{
return builder
.AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>(
ApiKeyAuthOptions.DefaultScheme,
configureOptions);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PushNotificationsAPI.Models;
using PushNotificationsAPI.Services;

namespace PushNotificationsAPI.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class NotificationsController : ControllerBase
{
readonly INotificationService _notificationService;

public NotificationsController(INotificationService notificationService)
{
_notificationService = notificationService;
}

[HttpPut]
[Route("installations")]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
public async Task<IActionResult> UpdateInstallation(
[Required] DeviceInstallation deviceInstallation)
{
var success = await _notificationService
.CreateOrUpdateInstallationAsync(deviceInstallation, HttpContext.RequestAborted);

if (!success)
return new UnprocessableEntityResult();

return new OkResult();
}

[HttpDelete()]
[Route("installations/{installationId}")]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
public async Task<ActionResult> DeleteInstallation(
[Required][FromRoute] string installationId)
{
// Probably want to ensure deletion even if the connection is broken
var success = await _notificationService
.DeleteInstallationByIdAsync(installationId, CancellationToken.None);

if (!success)
return new UnprocessableEntityResult();

return new OkResult();
}

[HttpPost]
[Route("requests")]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
public async Task<IActionResult> RequestPush(
[Required] NotificationRequest notificationRequest)
{
if ((notificationRequest.Silent &&
string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
(!notificationRequest.Silent &&
string.IsNullOrWhiteSpace(notificationRequest?.Text)))
return new BadRequestResult();

var success = await _notificationService
.RequestNotificationAsync(notificationRequest, HttpContext.RequestAborted);

if (!success)
return new UnprocessableEntityResult();

return new OkResult();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace PushNotificationsAPI.Models;

public class DeviceInstallation
{
[Required]
public string InstallationId { get; set; }

[Required]
public string Platform { get; set; }

[Required]
public string PushChannel { get; set; }

public IList<string> Tags { get; set; } = Array.Empty<string>();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;

namespace PushNotificationsAPI.Models;

public class NotificationHubOptions
{
[Required]
public string Name { get; set; }

[Required]
public string ConnectionString { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PushNotificationsAPI.Models;

public class NotificationRequest
{
public string Text { get; set; }
public string Action { get; set; }
public string[] Tags { get; set; } = Array.Empty<string>();
public bool Silent { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace PushNotificationsAPI.Models;

public class PushTemplates
{
public class Generic
{
public const string Android = "{ \"message\" : { \"notification\" : { \"title\" : \"PushDemo\", \"body\" : \"$(alertMessage)\"}, \"data\" : { \"action\" : \"$(alertAction)\" } } }";
public const string iOS = "{ \"aps\" : {\"alert\" : \"$(alertMessage)\"}, \"action\" : \"$(alertAction)\" }";
}

public class Silent
{
public const string Android = "{ \"data\" : {\"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\"} }";
//public const string Android = "{ \"message\" : { \"data\" : {\"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\"} } }";
public const string iOS = "{ \"aps\" : {\"content-available\" : 1, \"apns-priority\": 5, \"sound\" : \"\", \"badge\" : 0}, \"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\" }";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using PushNotificationsAPI.Authentication;
using PushNotificationsAPI.Services;
using PushNotificationsAPI.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
}).AddApiKeyAuth(builder.Configuration.GetSection("Authentication").Bind);

builder.Services.AddSingleton<INotificationService, NotificationHubService>();
builder.Services.AddOptions<NotificationHubOptions>()
.Configure(builder.Configuration.GetSection("NotificationHub").Bind)
.ValidateDataAnnotations();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62106",
"sslPort": 44341
}
},
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/notifications",
"applicationUrl": "http://localhost:5179",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/notifications",
"applicationUrl": "https://localhost:7020;http://localhost:5179",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/notifications",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"PushNotificationsAPI": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:29493;http://localhost:60313",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>719ad32f-4587-4748-9143-4ae68b445b07</UserSecretsId>
</PropertyGroup>

<PropertyGroup Condition=" '$(RunConfiguration)' == 'https' " />
<PropertyGroup Condition=" '$(RunConfiguration)' == 'http' " />

<ItemGroup>
<Folder Include="Authentication\" />
<Folder Include="Models\" />
<Folder Include="Services\" />
<Folder Include="Controllers\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
</ItemGroup>

<ItemGroup>
<Compile Update="Models\PushTemplates.cs">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Compile>
</ItemGroup>

<ItemGroup>
<None Remove="Controllers\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using PushNotificationsAPI.Models;

namespace PushNotificationsAPI.Services;

public interface INotificationService
{
Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token);
Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token);
Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token);
}

Loading

0 comments on commit 7adbd6b

Please sign in to comment.