Skip to content

Commit 70c2026

Browse files
authored
Merge pull request #701 from VocaDB/feat/598-web-core-add-controllers
Copy `Controllers` from VocaDbWeb to VocaDbWeb.Core
2 parents c5f58c4 + e36623f commit 70c2026

File tree

126 files changed

+14714
-118
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+14714
-118
lines changed

VocaDbModel/Domain/Globalization/RegionCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace VocaDb.Model.Domain.Globalization
88
{
99
public class RegionCollection
1010
{
11+
// FIXME
1112
public static readonly string[] RegionCodes = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
1213
.Select(culture => new RegionInfo(culture.Name).TwoLetterISORegionName)
1314
.OrderBy(c => c)

VocaDbWeb.Core/Code/ErrorLogger.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#nullable disable
2+
3+
using System;
4+
using System.Net;
5+
using AngleSharp.Io;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Extensions;
8+
using NLog;
9+
10+
namespace VocaDb.Web.Code
11+
{
12+
public static class ErrorLogger
13+
{
14+
public const int Code_BadRequest = (int)HttpStatusCode.BadRequest;
15+
public const int Code_Forbidden = (int)HttpStatusCode.Forbidden;
16+
public const int Code_NotFound = (int)HttpStatusCode.NotFound;
17+
public const int Code_InternalServerError = (int)HttpStatusCode.InternalServerError;
18+
19+
private static readonly Logger s_log = LogManager.GetCurrentClassLogger();
20+
21+
/// <summary>
22+
/// Logs HTTP error code sent to a client.
23+
/// This method is mostly for client errors (status code 4xx).
24+
///
25+
/// Client info and error summary will be logged.
26+
/// Full exceptions should be logged separately using <see cref="LogException"/>.
27+
/// </summary>
28+
/// <param name="request">HTTP request. Cannot be null.</param>
29+
/// <param name="code">HTTP response code.</param>
30+
/// <param name="msg">Optional simple message, usually exception message.</param>
31+
/// <param name="level">Logging level, optional.</param>
32+
public static void LogHttpError(HttpRequest request, int code, string msg = null, LogLevel level = null)
33+
{
34+
if (string.IsNullOrEmpty(msg))
35+
s_log.Log(level ?? LogLevel.Warn, RequestInfo($"HTTP error code {code} for", request));
36+
else
37+
s_log.Log(level ?? LogLevel.Warn, RequestInfo($"HTTP error code {code} ({msg}) for", request));
38+
}
39+
40+
public static void LogException(HttpRequest request, Exception ex, LogLevel level = null)
41+
{
42+
s_log.Log(level ?? LogLevel.Error, ex, RequestInfo("Exception for", request));
43+
}
44+
45+
public static void LogMessage(HttpRequest request, string msg, LogLevel level = null)
46+
{
47+
s_log.Log(level ?? LogLevel.Error, RequestInfo(msg + " for", request));
48+
}
49+
50+
public static string RequestInfo(string msg, HttpRequest request)
51+
{
52+
var userHostAddress = request.HttpContext.Connection.RemoteIpAddress;
53+
var userHostName = request.GetTypedHeaders().Host;
54+
var httpMethod = request.Method;
55+
var pathAndQuery = request.GetEncodedPathAndQuery();
56+
var userAgent = request.Headers[HeaderNames.UserAgent];
57+
var urlReferrer = request.GetTypedHeaders().Referer;
58+
return $"{msg} '{userHostAddress}' [{userHostName}], URL {httpMethod} '{pathAndQuery}', UA '{userAgent}', referrer '{urlReferrer}'";
59+
}
60+
}
61+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Routing;
3+
4+
namespace VocaDb.Web.Code
5+
{
6+
public class IdNotNumberConstraint : IRouteConstraint
7+
{
8+
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
9+
{
10+
var val = values[routeKey]?.ToString();
11+
return !int.TryParse(val, out _);
12+
}
13+
}
14+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using Newtonsoft.Json;
5+
using NLog;
6+
7+
namespace VocaDb.Web.Code
8+
{
9+
public class JsonModelBinder : IModelBinder
10+
{
11+
private static readonly ILogger s_log = LogManager.GetCurrentClassLogger();
12+
13+
// Code from: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0
14+
public Task BindModelAsync(ModelBindingContext bindingContext)
15+
{
16+
if (bindingContext == null)
17+
throw new ArgumentNullException(nameof(bindingContext));
18+
19+
var modelName = bindingContext.ModelName;
20+
21+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
22+
if (valueProviderResult == ValueProviderResult.None)
23+
return Task.CompletedTask;
24+
25+
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
26+
27+
var value = valueProviderResult.FirstValue;
28+
if (string.IsNullOrEmpty(value))
29+
return Task.CompletedTask;
30+
31+
object obj;
32+
33+
try
34+
{
35+
obj = JsonConvert.DeserializeObject(value, bindingContext.ModelMetadata.ModelType, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
36+
}
37+
catch (JsonReaderException x)
38+
{
39+
s_log.Error(x, "Unable to process JSON, content is " + value);
40+
throw;
41+
}
42+
43+
bindingContext.Result = ModelBindingResult.Success(obj);
44+
45+
return Task.CompletedTask;
46+
}
47+
}
48+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.IO;
2+
using Microsoft.AspNetCore.Http;
3+
using VocaDb.Model.Domain.Web;
4+
5+
namespace VocaDb.Web
6+
{
7+
public class AspNetCoreHttpPostedFile : IHttpPostedFile
8+
{
9+
public AspNetCoreHttpPostedFile(IFormFile file)
10+
{
11+
_file = file;
12+
}
13+
14+
private readonly IFormFile _file;
15+
16+
public string ContentType => _file.ContentType;
17+
18+
public string FileName => _file.FileName;
19+
20+
public void SaveAs(string path) => _file.CopyTo(new FileStream(path, FileMode.Create));
21+
}
22+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Cors.Infrastructure;
4+
using Microsoft.AspNetCore.Http;
5+
using VocaDb.Model.Utils;
6+
7+
namespace VocaDb.Web.Code.WebApi
8+
{
9+
// Code from: https://github.com/aspnet/AspNetWebStack/blob/ba26cfbfbf958d548e4c0a96e853250f13450dc6/src/System.Web.Mvc/HttpVerbs.cs
10+
[Flags]
11+
public enum HttpVerbs
12+
{
13+
Get = 1 << 0,
14+
Post = 1 << 1,
15+
Put = 1 << 2,
16+
Delete = 1 << 3,
17+
Head = 1 << 4,
18+
Patch = 1 << 5,
19+
Options = 1 << 6,
20+
}
21+
22+
/// <summary>
23+
/// CORS policy for APIs that require authentication. Origins are restricted.
24+
/// </summary>
25+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
26+
public class AuthenticatedCorsApiAttribute : Attribute, ICorsPolicyProvider
27+
{
28+
private readonly CorsPolicy _policy;
29+
30+
public AuthenticatedCorsApiAttribute(HttpVerbs verbs)
31+
{
32+
_policy = new CorsPolicy
33+
{
34+
SupportsCredentials = true
35+
};
36+
37+
// Verbs are case sensitive
38+
if (verbs.HasFlag(HttpVerbs.Get))
39+
_policy.Methods.Add("GET");
40+
if (verbs.HasFlag(HttpVerbs.Post))
41+
_policy.Methods.Add("POST");
42+
if (verbs.HasFlag(HttpVerbs.Put))
43+
_policy.Methods.Add("PUT");
44+
if (verbs.HasFlag(HttpVerbs.Options))
45+
_policy.Methods.Add("OPTIONS");
46+
47+
var origins = AppConfig.AllowedCorsOrigins;
48+
if (!string.IsNullOrEmpty(origins))
49+
{
50+
if (origins != "*")
51+
Array.ForEach(origins.Split(','), _policy.Origins.Add);
52+
}
53+
}
54+
55+
public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName) => Task.FromResult(_policy);
56+
}
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Http.Extensions;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.Filters;
6+
7+
namespace VocaDb.Web.Code.WebApi
8+
{
9+
[Obsolete]
10+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
11+
public class RequireSslAttribute : ActionFilterAttribute
12+
{
13+
// Code from: https://stackoverflow.com/questions/31617345/what-is-the-asp-net-core-mvc-equivalent-to-request-requesturi/40721652#40721652
14+
private static bool IsSSL(HttpRequest request) => request is not null && new Uri(request.GetDisplayUrl()) is Uri requestUri && requestUri.Scheme == Uri.UriSchemeHttps;
15+
16+
public override void OnActionExecuting(ActionExecutingContext context)
17+
{
18+
#if DEBUG
19+
return;
20+
#endif
21+
22+
if (!IsSSL(context.HttpContext.Request))
23+
{
24+
// 403.4 - SSL required.
25+
context.Result = new ForbidResult("This API requires SSL");
26+
}
27+
}
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using Microsoft.AspNetCore.Http.Extensions;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using NLog;
7+
using VocaDb.Model.Service.Security;
8+
using VocaDb.Web.Helpers;
9+
10+
namespace VocaDb.Web.Code.WebApi
11+
{
12+
public class RestrictBannedIPAttribute : ActionFilterAttribute
13+
{
14+
private static readonly Logger s_log = LogManager.GetCurrentClassLogger();
15+
16+
public override void OnActionExecuting(ActionExecutingContext context)
17+
{
18+
var ipRules = context.HttpContext.RequestServices.GetRequiredService<IPRuleManager>();
19+
20+
var host = WebHelper.GetRealHost(context.HttpContext.Request);
21+
22+
if (!ipRules.IsAllowed(host))
23+
{
24+
// Code from: https://stackoverflow.com/questions/31617345/what-is-the-asp-net-core-mvc-equivalent-to-request-requesturi/40721652#40721652
25+
s_log.Warn($"Restricting banned host '{host}' for '{new Uri(context.HttpContext.Request.GetDisplayUrl())}'.");
26+
27+
context.Result = new ForbidResult();
28+
}
29+
}
30+
}
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Code from: https://stackoverflow.com/questions/50325969/how-do-i-make-an-asp-net-core-void-task-action-method-return-204-no-content/60563876#60563876
2+
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc.Controllers;
6+
using Microsoft.AspNetCore.Mvc.Filters;
7+
8+
namespace VocaDb.Web.Code.WebApi
9+
{
10+
/// <summary>
11+
/// A filter that transforms http status code 200 OK to 204 No Content for controller actions that return nothing,
12+
/// i.e. <see cref="System.Void"/> or <see cref="Task"/>.
13+
/// </summary>
14+
internal class VoidAndTaskTo204NoContentFilter : IResultFilter
15+
{
16+
/// <inheritdoc/>
17+
public void OnResultExecuting(ResultExecutingContext context)
18+
{
19+
if (context.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
20+
{
21+
var returnType = actionDescriptor.MethodInfo.ReturnType;
22+
if (returnType == typeof(void) || returnType == typeof(Task))
23+
context.HttpContext.Response.StatusCode = StatusCodes.Status204NoContent;
24+
}
25+
}
26+
27+
/// <inheritdoc/>
28+
public void OnResultExecuted(ResultExecutedContext context)
29+
{
30+
}
31+
}
32+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#nullable disable
2+
3+
using System;
4+
using Microsoft.AspNetCore.Mvc;
5+
using VocaDb.Model.Service;
6+
7+
namespace VocaDb.Web.Controllers
8+
{
9+
public class ActivityEntryController : ControllerBase
10+
{
11+
private new const int EntriesPerPage = 50;
12+
13+
private readonly ActivityFeedService _service;
14+
15+
private ActivityFeedService Service => _service;
16+
17+
public ActivityEntryController(ActivityFeedService service)
18+
{
19+
_service = service;
20+
}
21+
22+
public ActionResult FollowedArtistActivity()
23+
{
24+
var result = Service.GetFollowedArtistActivity(EntriesPerPage);
25+
return View(result.Items);
26+
}
27+
28+
//
29+
// GET: /ActivityEntry/
30+
31+
public ActionResult Index(DateTime? before)
32+
{
33+
ViewBag.Before = before;
34+
35+
return View("Index");
36+
}
37+
}
38+
39+
public class DetailedPageResult
40+
{
41+
public DateTime? LastEntryDate { get; set; }
42+
43+
public string ViewHtml { get; set; }
44+
}
45+
}

0 commit comments

Comments
 (0)