Skip to content

Instantly share code, notes, and snippets.

@jhoerr
Last active July 23, 2020 16:09
Show Gist options
  • Save jhoerr/fb7a7dd2de2e4e1bc752d3f1073beafc to your computer and use it in GitHub Desktop.
Save jhoerr/fb7a7dd2de2e4e1bc752d3f1073beafc to your computer and use it in GitHub Desktop.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Database;
using System.Security.Claims;
using System.Linq;
using System.Collections.Generic;
using Models;
using System.ComponentModel.DataAnnotations;
using CSharpFunctionalExtensions;
using System;
using System.Net;
namespace Functions
{
public class Devices
{
private readonly PrintContext _printContext;
public Devices(PrintContext printContext)
{
_printContext = printContext;
}
[FunctionName("GetDevices")]
public async Task<IActionResult> GetDevices(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "devices")] HttpRequest req,
ClaimsPrincipal principal,
ILogger log)
=> await ApiRequest
.Authorize(req, principal, new[] { "Read.All", "ReadWrite.All", "Bill.All" })
.Bind(_ => Database.Get(_printContext.Devices))
.Finally(ApiResponse.Ok);
[FunctionName("PostDevices")]
public async Task<IActionResult> PostDevice(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "devices")] HttpRequest req,
ClaimsPrincipal principal,
ILogger log)
=> await ApiRequest
.Authorize(req, principal, new[] { "ReadWrite.All" })
.Bind(req => ApiRequest.ValidateBody<Device>(req))
.Bind(device => Database.Add(_printContext.Devices, device))
.Bind(device => Database.SaveChanges(_printContext, device))
.Finally(ApiResponse.Created);
}
public static class ApiRequest
{
/// <summary>Confirms that the requestor is authenticated and in at least one of the specified roles.</summary>
public static Result<HttpRequest, ApiError> Authorize(HttpRequest req, ClaimsPrincipal principal, IEnumerable<string> allowedRoles)
{
if (!principal.Identity.IsAuthenticated)
return Result.Failure<HttpRequest, ApiError>(ApiError.Unauthorized());
else if (!principal.Claims.Where(c => c.Type == "roles").Select(c => c.Value).Intersect(allowedRoles).Any())
return Result.Failure<HttpRequest, ApiError>(ApiError.Forbidden());
else
return Result.Success<HttpRequest, ApiError>(req);
}
/// <summary>Deserialize the request body to an instance of specified type and validate all properties. If valid, the instance is returned.</summary>
public static async Task<Result<T, ApiError>> ValidateBody<T>(HttpRequest req)
{
var bodyString = await req.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<T>(bodyString);
var validationContext = new ValidationContext(obj, null, null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(obj, validationContext, results);
return results.Count > 0
? Result.Failure<T, ApiError>(ApiError.BadRequest(results.First().ErrorMessage))
: Result.Success<T, ApiError>(obj);
}
}
public static class Database
{
/// <summary>Add an entity to the specified database set.</summary>
public static async Task<Result<T, ApiError>> Add<T>(DbSet<T> set, T obj) where T : class
{
try
{
await set.AddAsync(obj);
return Result.Success<T, ApiError>(obj);
}
catch (Exception e)
{
return Result.Failure<T, ApiError>(ApiError.InternalServerError($"Failed to add record: {e.Message}"));
}
}
/// <summary>Get all entities from the specified database set.</summary>
public static async Task<Result<List<T>, ApiError>> Get<T>(DbSet<T> set) where T : class
{
try
{
var records = await set.ToListAsync();
return Result.Success<List<T>, ApiError>(records);
}
catch (Exception e)
{
return Result.Failure<List<T>, ApiError>(ApiError.InternalServerError($"Failed to fetch records: {e.Message}"));
}
}
/// <summary>Save all outstanding changes to the database.</summary>
public static async Task<Result<T, ApiError>> SaveChanges<T>(DbContext dbContext, T obj)
{
try
{
await dbContext.SaveChangesAsync();
return Result.Success<T, ApiError>(obj);
}
catch (Exception e)
{
return Result.Failure<T, ApiError>(ApiError.InternalServerError($"Failed to save changes: {e.Message}"));
}
}
}
public static class ApiResponse
{
/// <summary>Return an HTTP 200 response with content, or an appropriate HTTP error response.</summary>
public static IActionResult Ok<T>(Result<T, ApiError> result)
=> result.IsSuccess
? new OkObjectResult(result.Value)
: result.Error.ToActionResult();
/// <summary>Return an HTTP 204 response with content, or an appropriate HTTP error response.</summary>
public static IActionResult Created<T>(Result<T, ApiError> result)
=> result.IsSuccess
? new CreatedResult("location", result.Value)
: result.Error.ToActionResult();
}
public class ApiError
{
internal ApiError(HttpStatusCode statusCode, string message = null)
{
StatusCode = statusCode;
Message = message;
}
public HttpStatusCode StatusCode { get; set; }
public string Message { get; set; }
public IActionResult ToActionResult()
{
switch (StatusCode)
{
case HttpStatusCode.Unauthorized: return new StatusCodeResult(401);
case HttpStatusCode.Forbidden: return new ForbidResult();
case HttpStatusCode.BadRequest: return new BadRequestObjectResult(Message);
default: return new StatusCodeResult(500);
}
}
public static ApiError Unauthorized()
=> new ApiError(HttpStatusCode.Unauthorized);
public static ApiError Forbidden()
=> new ApiError(HttpStatusCode.Forbidden);
public static ApiError BadRequest(string message)
=> new ApiError(HttpStatusCode.BadRequest, message);
public static ApiError InternalServerError(string message)
=> new ApiError(HttpStatusCode.InternalServerError, message);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment