Skip to content

Commit 9176aeb

Browse files
committed
feat: public endpoint to list links on a reddit post id
Resolves #33
1 parent d4cf4a9 commit 9176aeb

4 files changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Data;
2+
using System.Data.Common;
3+
4+
namespace ApplicationData;
5+
6+
public static class DataReaderExtensions
7+
{
8+
public static async IAsyncEnumerable<T> SelectAsync<T>(this DbDataReader reader, Func<IDataReader, T> mapper)
9+
{
10+
await using var scopedReader = reader; // auto-dispose
11+
12+
while (await scopedReader.ReadAsync())
13+
{
14+
yield return await Task.FromResult(mapper(scopedReader));
15+
}
16+
}
17+
}

ApplicationData/Services/LinkProvider.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,27 @@ FROM app.links l
7979
);
8080
}
8181

82+
public async Task<IReadOnlyCollection<Link>> FindAllLinksByPostId(string redditPostId)
83+
{
84+
var links = (await _db.ExecuteReaderAsync(
85+
$@"SELECT l.link_id, l.reddit_post_id, l.link_url, l.link_type, l.created_at, l.owner
86+
FROM app.links l
87+
WHERE
88+
l.reddit_post_id = @redditPostId
89+
AND l.is_deleted = false",
90+
new NpgsqlParameter[] { new("@redditPostId", redditPostId) }))
91+
.SelectAsync(reader => new Link(
92+
linkId: reader.GetInt32(0),
93+
redditPostId: reader.GetString(1),
94+
linkUrl: reader.GetString(2),
95+
linkType: ParseRawLinkType(reader.GetInt16(3)),
96+
createdAt: reader.GetDateTime(4),
97+
ownerId: reader.GetInt32(5)
98+
));
99+
100+
return await links.ToArrayAsync();
101+
}
102+
82103
public async Task DeleteLinkById(int linkId)
83104
{
84105
await using var tx = await _db.CreateTransactionAsync(System.Data.IsolationLevel.Serializable);

WebApi/AuthHandlers/ApiKeyAuthHandler.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text.RegularExpressions;
66
using ApplicationData.Services;
77
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Authorization;
89
using Microsoft.Extensions.Options;
910
using WebApi.Middleware;
1011
using WebApi.Util;
@@ -40,6 +41,10 @@ UserCache userCache
4041
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() =>
4142
await JsonExceptionMiddleware.InvokeWithExceptionHandler(Request.HttpContext, async () =>
4243
{
44+
// For endpoints with IAllowAnonymous, skip the authentication check.
45+
if (Context.GetEndpoint()?.Metadata.GetMetadata<IAllowAnonymous>() != null)
46+
return AuthenticateResult.NoResult();
47+
4348
if (!Request.Headers.TryGetValue("Authorization", out var authorization))
4449
return AuthenticateResult.Fail("No authorization key provided");
4550

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Net;
2+
using ApplicationData.Services;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Newtonsoft.Json;
6+
using Swashbuckle.AspNetCore.Annotations;
7+
using WebApi.Models;
8+
using WebApi.Util;
9+
10+
namespace WebApi.Controllers;
11+
12+
/// <summary>
13+
/// PublicController
14+
/// </summary>
15+
[ApiController]
16+
[Route("v1/[controller]")]
17+
[AllowAnonymous]
18+
public class PublicController : ApiController
19+
{
20+
private readonly LinkProvider _linkProvider;
21+
private readonly UserCache _userCache;
22+
23+
/// <summary>
24+
/// C'tor
25+
/// </summary>
26+
public PublicController(IServiceProvider serviceProvider, LinkProvider linkProvider, UserCache userCache)
27+
: base(serviceProvider)
28+
{
29+
_linkProvider = linkProvider;
30+
_userCache = userCache;
31+
}
32+
33+
/// <summary>
34+
/// Find links available for the specified reddit post ID.
35+
/// </summary>
36+
/// <param name="redditPostId">The unique reddit post ID. For example, use "sf8kp8" from the permalink "https://www.reddit.com/r/aww/comments/sf8kp8/treats_you_say/".</param>
37+
[HttpGet]
38+
[Route("Link/By-Reddit-Post-Id/{redditPostId}")]
39+
[SwaggerResponse((int)HttpStatusCode.OK,
40+
description: "OK. A list of links available for this reddit post ID.",
41+
type: typeof(IReadOnlyCollection<PublicLinkListingResult>))]
42+
public async Task<IReadOnlyCollection<PublicLinkListingResult>> FindLinksByPostId(string redditPostId)
43+
{
44+
var linksByPostId = await _linkProvider.FindAllLinksByPostId(redditPostId);
45+
return await linksByPostId.ToAsyncEnumerable()
46+
.SelectAwait(async link => new PublicLinkListingResult(
47+
link.LinkUrl,
48+
LinkTypeHelpers.ParseToSerializableLinkType(link.LinkType.RawValue),
49+
ProviderUsername: (await _userCache.FindUser(link.OwnerId)).Map(x => x.DisplayUsername).Value))
50+
.ToArrayAsync();
51+
}
52+
53+
/// <summary>
54+
/// A link listing result accessible to the public.
55+
/// </summary>
56+
public record PublicLinkListingResult(
57+
[JsonProperty("linkUrl")] string LinkUrl,
58+
[JsonProperty("linkType")] SerializableLinkType LinkType,
59+
[JsonProperty("providerUsername")] string ProviderUsername
60+
);
61+
}

0 commit comments

Comments
 (0)