summaryrefslogtreecommitdiffstats
path: root/src/server/Api/Internal
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/Api/Internal')
-rw-r--r--src/server/Api/Internal/Account/CreateSessionRequest.cs8
-rw-r--r--src/server/Api/Internal/Account/CreateSessionRoute.cs41
-rw-r--r--src/server/Api/Internal/Account/CreateTokenRequest.cs6
-rw-r--r--src/server/Api/Internal/Account/CreateTokenRoute.cs34
-rw-r--r--src/server/Api/Internal/Account/DeleteTokenRoute.cs24
-rw-r--r--src/server/Api/Internal/Account/EndSessionRoute.cs13
-rw-r--r--src/server/Api/Internal/Account/GetArchiveRoute.cs36
-rw-r--r--src/server/Api/Internal/Account/GetProfileDataRoute.cs11
-rw-r--r--src/server/Api/Internal/Account/GetTokensRoute.cs17
-rw-r--r--src/server/Api/Internal/Account/UpdatePasswordRequest.cs6
-rw-r--r--src/server/Api/Internal/Account/UpdatePasswordRoute.cs35
-rw-r--r--src/server/Api/Internal/BaseInternalRoute.cs15
-rw-r--r--src/server/Api/Internal/Dtos/SiteReportDto.cs8
-rw-r--r--src/server/Api/Internal/Dtos/UserArchiveDto.cs32
-rw-r--r--src/server/Api/Internal/GetSiteReportRequest.cs6
-rw-r--r--src/server/Api/Internal/GetSiteReportRoute.cs54
-rw-r--r--src/server/Api/Internal/LoggedInInternalUser.cs7
-rw-r--r--src/server/Api/Internal/RouteBaseAsync.cs73
-rw-r--r--src/server/Api/Internal/RouteBaseSync.cs53
19 files changed, 479 insertions, 0 deletions
diff --git a/src/server/Api/Internal/Account/CreateSessionRequest.cs b/src/server/Api/Internal/Account/CreateSessionRequest.cs
new file mode 100644
index 0000000..24c28b4
--- /dev/null
+++ b/src/server/Api/Internal/Account/CreateSessionRequest.cs
@@ -0,0 +1,8 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class CreateSessionRequest
+{
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public bool Persist { get; set; }
+}
diff --git a/src/server/Api/Internal/Account/CreateSessionRoute.cs b/src/server/Api/Internal/Account/CreateSessionRoute.cs
new file mode 100644
index 0000000..09e05b6
--- /dev/null
+++ b/src/server/Api/Internal/Account/CreateSessionRoute.cs
@@ -0,0 +1,41 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class CreateSessionRoute : RouteBaseInternalAsync.WithRequest<CreateSessionRequest>.WithActionResult
+{
+ private readonly AppDbContext _context;
+
+ public CreateSessionRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [AllowAnonymous]
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpPost("~/v{version:apiVersion}/account/create-session")]
+ public override async Task<ActionResult> HandleAsync(CreateSessionRequest payload, CancellationToken cancellationToken = default) {
+ var user = _context.Users.SingleOrDefault(u => u.Username == payload.Username);
+ if (user == default || !user.VerifyPassword(payload.Password)) {
+ return BadRequest(new ErrorResult("Invalid username or password"));
+ }
+
+ var claims = user.DefaultClaims();
+ var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
+ var principal = new ClaimsPrincipal(identity);
+ var authenticationProperties = new AuthenticationProperties {
+ AllowRefresh = true,
+ IssuedUtc = DateTimeOffset.UtcNow,
+ };
+
+ if (payload.Persist) {
+ authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddYears(100);
+ authenticationProperties.IsPersistent = true;
+ }
+
+ await HttpContext.SignInAsync(principal, authenticationProperties);
+ // Return new LoggedInInternalUser here, because it is not materialised in AppControllerBase yet.
+ return Ok(new LoggedInInternalUser {
+ Id = user.Id,
+ Username = user.Username
+ });
+ }
+}
diff --git a/src/server/Api/Internal/Account/CreateTokenRequest.cs b/src/server/Api/Internal/Account/CreateTokenRequest.cs
new file mode 100644
index 0000000..399bdfc
--- /dev/null
+++ b/src/server/Api/Internal/Account/CreateTokenRequest.cs
@@ -0,0 +1,6 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class CreateTokenRequest
+{
+ public string Name { get; set; }
+}
diff --git a/src/server/Api/Internal/Account/CreateTokenRoute.cs b/src/server/Api/Internal/Account/CreateTokenRoute.cs
new file mode 100644
index 0000000..ea0e01f
--- /dev/null
+++ b/src/server/Api/Internal/Account/CreateTokenRoute.cs
@@ -0,0 +1,34 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class CreateTokenRoute : RouteBaseInternalSync.WithRequest<CreateTokenRequest>.WithActionResult
+{
+ private readonly AppDbContext _context;
+
+ public CreateTokenRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpPost("~/v{version:apiVersion}/account/create-token")]
+ public override ActionResult Handle(CreateTokenRequest request) {
+ var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ return NotFound(new ErrorResult("User does not exist"));
+ }
+
+ if (request.Name.IsNullOrWhiteSpace()) {
+ return BadRequest(new ErrorResult("Token name is required"));
+ }
+
+ var token = new AccessToken {
+ Id = Guid.NewGuid(),
+ Name = request.Name,
+ User = user
+ };
+
+ _context.AccessTokens.Add(token);
+ _context.SaveChanges();
+ return Ok(token);
+ }
+}
diff --git a/src/server/Api/Internal/Account/DeleteTokenRoute.cs b/src/server/Api/Internal/Account/DeleteTokenRoute.cs
new file mode 100644
index 0000000..f423b6f
--- /dev/null
+++ b/src/server/Api/Internal/Account/DeleteTokenRoute.cs
@@ -0,0 +1,24 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class DeleteTokenRoute : RouteBaseInternalSync.WithRequest<Guid>.WithActionResult
+{
+ private readonly AppDbContext _context;
+
+ public DeleteTokenRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpDelete("~/v{version:apiVersion}/account/delete-token")]
+ public override ActionResult Handle(Guid id) {
+ var token = _context.AccessTokens.SingleOrDefault(c => c.Id == id);
+ if (token == default) {
+ return NotFound();
+ }
+
+ _context.AccessTokens.Remove(token);
+ _context.SaveChanges();
+ return Ok();
+ }
+}
diff --git a/src/server/Api/Internal/Account/EndSessionRoute.cs b/src/server/Api/Internal/Account/EndSessionRoute.cs
new file mode 100644
index 0000000..4f32168
--- /dev/null
+++ b/src/server/Api/Internal/Account/EndSessionRoute.cs
@@ -0,0 +1,13 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class EndSessionRoute : RouteBaseInternalAsync.WithoutRequest.WithActionResult
+{
+ [AllowAnonymous]
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpGet("~/v{version:apiVersion}/account/end-session")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ await HttpContext.SignOutAsync();
+ return Ok();
+ }
+}
diff --git a/src/server/Api/Internal/Account/GetArchiveRoute.cs b/src/server/Api/Internal/Account/GetArchiveRoute.cs
new file mode 100644
index 0000000..5dc006e
--- /dev/null
+++ b/src/server/Api/Internal/Account/GetArchiveRoute.cs
@@ -0,0 +1,36 @@
+using IOL.BookmarkThing.Server.Api.Internal.Dtos;
+
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class GetArchiveRoute : RouteBaseInternalSync.WithoutRequest.WithActionResult
+{
+ private readonly AppDbContext _context;
+
+ public GetArchiveRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpGet("~/v{version:apiVersion}/account/archive")]
+ public override ActionResult Handle() {
+ var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ return NotFound();
+ }
+
+ var entries = _context.Entries.Where(c => c.UserId == user.Id);
+ var archive = new UserArchiveDto() {
+ Created = DateTime.UtcNow,
+ User = new UserArchiveDto.UserArchiveUser(user),
+ Entries = entries.Select(c => new UserArchiveDto.UserArchiveEntry(c)).ToList()
+ };
+ var jsonOptions = new JsonSerializerOptions {
+ WriteIndented = true
+ };
+ var archiveBytes = JsonSerializer.SerializeToUtf8Bytes(archive, jsonOptions);
+ return File(archiveBytes,
+ "application/json",
+ "bookmark-thing-archive-" + user.Username + "-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json");
+ }
+}
diff --git a/src/server/Api/Internal/Account/GetProfileDataRoute.cs b/src/server/Api/Internal/Account/GetProfileDataRoute.cs
new file mode 100644
index 0000000..adf1cba
--- /dev/null
+++ b/src/server/Api/Internal/Account/GetProfileDataRoute.cs
@@ -0,0 +1,11 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class GetProfileDataRoute : RouteBaseInternalSync.WithoutRequest.WithActionResult<LoggedInInternalUser>
+{
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpGet("~/v{version:apiVersion}/account/profile-data")]
+ public override ActionResult<LoggedInInternalUser> Handle() {
+ return Ok(LoggedInUser);
+ }
+}
diff --git a/src/server/Api/Internal/Account/GetTokensRoute.cs b/src/server/Api/Internal/Account/GetTokensRoute.cs
new file mode 100644
index 0000000..7e87bc7
--- /dev/null
+++ b/src/server/Api/Internal/Account/GetTokensRoute.cs
@@ -0,0 +1,17 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class GetTokensRoute : RouteBaseInternalSync.WithoutRequest.WithResult<ActionResult<IList<AccessToken>>>
+{
+ private readonly AppDbContext _context;
+
+ public GetTokensRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpGet("~/v{version:apiVersion}/account/tokens")]
+ public override ActionResult<IList<AccessToken>> Handle() {
+ return Ok(_context.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id));
+ }
+}
diff --git a/src/server/Api/Internal/Account/UpdatePasswordRequest.cs b/src/server/Api/Internal/Account/UpdatePasswordRequest.cs
new file mode 100644
index 0000000..1773bb0
--- /dev/null
+++ b/src/server/Api/Internal/Account/UpdatePasswordRequest.cs
@@ -0,0 +1,6 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class UpdatePasswordRequest
+{
+ public string NewPassword { get; set; }
+}
diff --git a/src/server/Api/Internal/Account/UpdatePasswordRoute.cs b/src/server/Api/Internal/Account/UpdatePasswordRoute.cs
new file mode 100644
index 0000000..d06e850
--- /dev/null
+++ b/src/server/Api/Internal/Account/UpdatePasswordRoute.cs
@@ -0,0 +1,35 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Account;
+
+public class UpdatePasswordRoute : RouteBaseInternalSync.WithRequest<UpdatePasswordRequest>.WithActionResult
+{
+ private readonly AppDbContext _context;
+
+ public UpdatePasswordRoute(AppDbContext context) {
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpPost("~/v{version:apiVersion}/account/update-password")]
+ public override ActionResult Handle(UpdatePasswordRequest payload) {
+ if (payload.NewPassword.IsNullOrWhiteSpace()) {
+ return BadRequest(new ErrorResult("Invalid request",
+ "The new password field is required"));
+ }
+
+ if (payload.NewPassword.Length < 6) {
+ return BadRequest(new ErrorResult("Invalid request",
+ "The new password must contain atleast 6 characters"));
+ }
+
+ var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ HttpContext.SignOutAsync();
+ return StatusCode(403);
+ }
+
+ user.HashAndSetPassword(payload.NewPassword);
+ _context.SaveChanges();
+ return Ok();
+ }
+}
diff --git a/src/server/Api/Internal/BaseInternalRoute.cs b/src/server/Api/Internal/BaseInternalRoute.cs
new file mode 100644
index 0000000..2f92c8e
--- /dev/null
+++ b/src/server/Api/Internal/BaseInternalRoute.cs
@@ -0,0 +1,15 @@
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+/// <inheritdoc />
+[Authorize]
+[ApiController]
+public class BaseInternalRoute : ControllerBase
+{
+ /// <summary>
+ /// User data for the currently logged on user.
+ /// </summary>
+ protected LoggedInInternalUser LoggedInUser => new() {
+ Username = User.Identity?.Name,
+ Id = User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value.ToGuid() ?? default
+ };
+}
diff --git a/src/server/Api/Internal/Dtos/SiteReportDto.cs b/src/server/Api/Internal/Dtos/SiteReportDto.cs
new file mode 100644
index 0000000..9c4aba8
--- /dev/null
+++ b/src/server/Api/Internal/Dtos/SiteReportDto.cs
@@ -0,0 +1,8 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Dtos;
+
+public class SiteReportDto
+{
+ public string Description { get; set; }
+ public bool Duplicate { get; set; }
+ public bool Unreachable { get; set; }
+}
diff --git a/src/server/Api/Internal/Dtos/UserArchiveDto.cs b/src/server/Api/Internal/Dtos/UserArchiveDto.cs
new file mode 100644
index 0000000..ec691b3
--- /dev/null
+++ b/src/server/Api/Internal/Dtos/UserArchiveDto.cs
@@ -0,0 +1,32 @@
+namespace IOL.BookmarkThing.Server.Api.Internal.Dtos;
+
+public class UserArchiveDto
+{
+ public DateTime Created { get; set; }
+ public UserArchiveUser User { get; set; }
+ public List<UserArchiveEntry> Entries { get; set; }
+
+ public class UserArchiveUser
+ {
+ public UserArchiveUser(User user) {
+ Created = user.Created;
+ Username = user.Username;
+ }
+
+ public DateTime Created { get; set; }
+ public string Username { get; set; }
+ }
+
+ public class UserArchiveEntry
+ {
+ public UserArchiveEntry(Entry entry) {
+ Url = entry.Url;
+ Description = entry.Description;
+ Tags = entry.Tags;
+ }
+
+ public Uri Url { get; set; }
+ public string Description { get; set; }
+ public string Tags { get; set; }
+ }
+}
diff --git a/src/server/Api/Internal/GetSiteReportRequest.cs b/src/server/Api/Internal/GetSiteReportRequest.cs
new file mode 100644
index 0000000..52fbfe8
--- /dev/null
+++ b/src/server/Api/Internal/GetSiteReportRequest.cs
@@ -0,0 +1,6 @@
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+public class GetSiteReportRequest
+{
+ public Uri Url { get; set; }
+}
diff --git a/src/server/Api/Internal/GetSiteReportRoute.cs b/src/server/Api/Internal/GetSiteReportRoute.cs
new file mode 100644
index 0000000..58d6637
--- /dev/null
+++ b/src/server/Api/Internal/GetSiteReportRoute.cs
@@ -0,0 +1,54 @@
+using HtmlAgilityPack;
+using IOL.BookmarkThing.Server.Api.Internal.Dtos;
+
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+public class GetSiteReportRoute : RouteBaseInternalAsync.WithRequest<GetSiteReportRequest>.WithActionResult
+{
+ private readonly HttpClient _client;
+ private readonly ILogger<GetSiteReportRequest> _logger;
+ private readonly AppDbContext _context;
+
+ public GetSiteReportRoute(HttpClient client, ILogger<GetSiteReportRequest> logger, AppDbContext context) {
+ _client = client;
+ _logger = logger;
+ _context = context;
+ }
+
+ [ApiVersionNeutral]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ [HttpPost("~/v{version:apiVersion}/site-report")]
+ public override async Task<ActionResult> HandleAsync(GetSiteReportRequest request, CancellationToken cancellationToken = default) {
+ var report = new SiteReportDto();
+ var exists = _context.Entries.Any(c => c.Url == request.Url && c.UserId == LoggedInUser.Id);
+ if (exists) {
+ report.Duplicate = true;
+ return Ok(report);
+ }
+
+ try {
+ var http_request = await _client.GetAsync(request.Url, cancellationToken);
+ if (http_request.IsSuccessStatusCode) {
+ try {
+ var document = new HtmlDocument();
+ document.Load(await http_request.Content.ReadAsStreamAsync(cancellationToken));
+ var htmlNodes = document.DocumentNode.Descendants("meta")
+ .Where(p => p.GetAttributeValue("name", string.Empty).Equals("description", StringComparison.InvariantCultureIgnoreCase));
+ report.Description = htmlNodes.FirstOrDefault()?.GetAttributeValue("content", string.Empty);
+ } catch (Exception e) {
+ _logger.LogWarning(e, "An error occured when parsing site for site report");
+ }
+ } else {
+ report.Unreachable = true;
+ }
+ } catch (Exception e) {
+ if (e is HttpRequestException) {
+ report.Unreachable = true;
+ }
+
+ _logger.LogError(e, "An error occured when getting external site for site report");
+ }
+
+ return Ok(report);
+ }
+}
diff --git a/src/server/Api/Internal/LoggedInInternalUser.cs b/src/server/Api/Internal/LoggedInInternalUser.cs
new file mode 100644
index 0000000..e08dd51
--- /dev/null
+++ b/src/server/Api/Internal/LoggedInInternalUser.cs
@@ -0,0 +1,7 @@
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+public class LoggedInInternalUser
+{
+ public Guid Id { get; set; }
+ public string Username { get; set; }
+}
diff --git a/src/server/Api/Internal/RouteBaseAsync.cs b/src/server/Api/Internal/RouteBaseAsync.cs
new file mode 100644
index 0000000..b5ad709
--- /dev/null
+++ b/src/server/Api/Internal/RouteBaseAsync.cs
@@ -0,0 +1,73 @@
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public static class RouteBaseInternalAsync
+{
+ public static class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : BaseInternalRoute
+ {
+ public abstract Task<TResponse> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : BaseInternalRoute
+ {
+ public abstract Task HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : BaseInternalRoute
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : BaseInternalRoute
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : BaseInternalRoute
+ {
+ public abstract Task<TResponse> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : BaseInternalRoute
+ {
+ public abstract Task HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : BaseInternalRoute
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : BaseInternalRoute
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+}
diff --git a/src/server/Api/Internal/RouteBaseSync.cs b/src/server/Api/Internal/RouteBaseSync.cs
new file mode 100644
index 0000000..128a3a9
--- /dev/null
+++ b/src/server/Api/Internal/RouteBaseSync.cs
@@ -0,0 +1,53 @@
+namespace IOL.BookmarkThing.Server.Api.Internal;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public static class RouteBaseInternalSync
+{
+ public static class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : BaseInternalRoute
+ {
+ public abstract TResponse Handle(TRequest request);
+ }
+
+ public abstract class WithoutResult : BaseInternalRoute
+ {
+ public abstract void Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult<TResponse> : BaseInternalRoute
+ {
+ public abstract ActionResult<TResponse> Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult : BaseInternalRoute
+ {
+ public abstract ActionResult Handle(TRequest entry);
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : BaseInternalRoute
+ {
+ public abstract TResponse Handle();
+ }
+
+ public abstract class WithoutResult : BaseInternalRoute
+ {
+ public abstract void Handle();
+ }
+
+ public abstract class WithActionResult<TResponse> : BaseInternalRoute
+ {
+ public abstract ActionResult<TResponse> Handle();
+ }
+
+ public abstract class WithActionResult : BaseInternalRoute
+ {
+ public abstract ActionResult Handle();
+ }
+ }
+}