From 140419a1ca494fe26bb8c9a294443659f4b5d062 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 12:08:53 -0500 Subject: [PATCH 1/6] feat: rate limiter lets api v1 requests through --- Foxnouns.Backend/Controllers/InternalController.cs | 2 ++ rate/handler.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 85bc774..3954547 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -38,6 +38,8 @@ public partial class InternalController(DatabaseContext db) : ControllerBase { if (template.StartsWith("api/v2")) template = template["api/v2".Length..]; + else if (template.StartsWith("api/v1")) + template = template["api/v1".Length..]; template = PathVarRegex() .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` diff --git a/rate/handler.go b/rate/handler.go index 7ab0b59..311b5b8 100644 --- a/rate/handler.go +++ b/rate/handler.go @@ -38,7 +38,7 @@ func (hn *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // all public api endpoints are prefixed with this - if !strings.HasPrefix(r.URL.Path, "/api/v2") { + if !strings.HasPrefix(r.URL.Path, "/api/v2") && !strings.HasPrefix(r.URL.Path, "/api/v1") { w.WriteHeader(http.StatusNotFound) return } From 2281b3e478bb5220c1711900af9c956106e3bc80 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 14:03:15 -0500 Subject: [PATCH 2/6] fix: replace port 5000 in example docs with port 6000 macOS runs a service on port 5000 by default. this doesn't actually prevent the backend server from *starting*, or the rate limiter proxy from working, but it *does* mean that when the backend restarts, if the proxy sends a request, it will stop working until it's restarted. the easiest way to work around this is by just changing the port the backend listens on. this does not change the ports used in the docker configuration. --- Foxnouns.Backend/config.example.ini | 2 +- Foxnouns.Frontend/.env.example | 4 ++-- rate/proxy-config.example.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 7522cba..9c6097e 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -1,7 +1,7 @@ ; The host the server will listen on Host = localhost ; The port the server will listen on -Port = 5000 +Port = 6000 ; The base *external* URL BaseUrl = https://pronouns.localhost ; The base URL for media, without a trailing slash. This must be publicly accessible. diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example index d79c672..2931832 100644 --- a/Foxnouns.Frontend/.env.example +++ b/Foxnouns.Frontend/.env.example @@ -1,7 +1,7 @@ -# Example .env file--DO NOT EDIT +# Example .env file--DO NOT EDIT, copy to .env or .env.local then edit PUBLIC_LANGUAGE=en PUBLIC_BASE_URL=https://pronouns.cc PUBLIC_SHORT_URL=https://prns.cc PUBLIC_API_BASE=https://pronouns.cc/api PRIVATE_API_HOST=http://localhost:5003/api -PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api +PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api diff --git a/rate/proxy-config.example.json b/rate/proxy-config.example.json index 427acef..1ec9e59 100644 --- a/rate/proxy-config.example.json +++ b/rate/proxy-config.example.json @@ -1,6 +1,6 @@ { "port": 5003, - "proxy_target": "http://localhost:5000", + "proxy_target": "http://localhost:6000", "debug": true, "powered_by": "5 gay rats" } From d182b074828cffaa328dc6e5a1b38c15dc268369 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 14:23:16 -0500 Subject: [PATCH 3/6] feat: GET /api/v1/members/{id}, api v1 flags --- .../Controllers/V1/UsersV1Controller.cs | 31 +++++- Foxnouns.Backend/Dto/V1/Member.cs | 44 +++++++++ Foxnouns.Backend/Dto/V1/User.cs | 4 + .../Extensions/WebApplicationExtensions.cs | 3 +- .../Services/V1/MembersV1Service.cs | 72 ++++++++++++++ .../Services/V1/UsersV1Service.cs | 97 +++++++++++++++---- 6 files changed, 229 insertions(+), 22 deletions(-) create mode 100644 Foxnouns.Backend/Dto/V1/Member.cs create mode 100644 Foxnouns.Backend/Services/V1/MembersV1Service.cs diff --git a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs index e11e490..75fd7b9 100644 --- a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs +++ b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs @@ -4,13 +4,36 @@ using Microsoft.AspNetCore.Mvc; namespace Foxnouns.Backend.Controllers.V1; -[Route("/api/v1/users")] -public class UsersV1Controller(UsersV1Service usersV1Service) : ApiControllerBase +[Route("/api/v1")] +public class UsersV1Controller(UsersV1Service usersV1Service, MembersV1Service membersV1Service) + : ApiControllerBase { - [HttpGet("{userRef}")] + [HttpGet("users/{userRef}")] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok(await usersV1Service.RenderUserAsync(user)); + return Ok( + await usersV1Service.RenderUserAsync( + user, + CurrentToken, + renderMembers: true, + renderFlags: true, + ct: ct + ) + ); + } + + [HttpGet("members/{id}")] + public async Task GetMemberAsync(string id, CancellationToken ct = default) + { + Member member = await membersV1Service.ResolveMemberAsync(id, ct); + return Ok( + await membersV1Service.RenderMemberAsync( + member, + CurrentToken, + renderFlags: true, + ct: ct + ) + ); } } diff --git a/Foxnouns.Backend/Dto/V1/Member.cs b/Foxnouns.Backend/Dto/V1/Member.cs new file mode 100644 index 0000000..955e9af --- /dev/null +++ b/Foxnouns.Backend/Dto/V1/Member.cs @@ -0,0 +1,44 @@ +// ReSharper disable NotAccessedPositionalProperty.Global +using Foxnouns.Backend.Database; +using Newtonsoft.Json; + +namespace Foxnouns.Backend.Dto.V1; + +public record PartialMember( + string Id, + Snowflake IdNew, + string Sid, + string Name, + string? DisplayName, + string? Bio, + string? Avatar, + string[] Links, + FieldEntry[] Names, + PronounEntry[] Pronouns +); + +public record MemberResponse( + string Id, + Snowflake IdNew, + string Sid, + string Name, + string? DisplayName, + string? Bio, + string? Avatar, + string[] Links, + FieldEntry[] Names, + PronounEntry[] Pronouns, + ProfileField[] Fields, + PrideFlag[] Flags, + PartialUser User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted +); + +public record PartialUser( + string Id, + Snowflake IdNew, + string Name, + string? DisplayName, + string? Avatar, + Dictionary CustomPreferences +); diff --git a/Foxnouns.Backend/Dto/V1/User.cs b/Foxnouns.Backend/Dto/V1/User.cs index eab4c29..11ff066 100644 --- a/Foxnouns.Backend/Dto/V1/User.cs +++ b/Foxnouns.Backend/Dto/V1/User.cs @@ -21,6 +21,8 @@ public record UserResponse( FieldEntry[] Names, PronounEntry[] Pronouns, ProfileField[] Fields, + PrideFlag[] Flags, + PartialMember[] Members, int? UtcOffset, Dictionary CustomPreferences ); @@ -75,3 +77,5 @@ public record PronounEntry(string Pronouns, string? DisplayText, string Status) )) .ToArray(); } + +public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description); diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 86b4a82..426ec12 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -130,7 +130,8 @@ public static class WebApplicationExtensions .AddTransient() .AddTransient() // Legacy services - .AddScoped(); + .AddScoped() + .AddScoped(); if (!config.Logging.EnableMetrics) services.AddHostedService(); diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs new file mode 100644 index 0000000..521a924 --- /dev/null +++ b/Foxnouns.Backend/Services/V1/MembersV1Service.cs @@ -0,0 +1,72 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Dto.V1; +using Microsoft.EntityFrameworkCore; +using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; +using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag; + +namespace Foxnouns.Backend.Services.V1; + +public class MembersV1Service(DatabaseContext db) +{ + public async Task ResolveMemberAsync(string id, CancellationToken ct = default) + { + Member? member; + if (Snowflake.TryParse(id, out Snowflake? sf)) + { + member = await db + .Members.Include(m => m.User) + .FirstOrDefaultAsync(m => m.Id == sf && !m.User.Deleted, ct); + if (member != null) + return member; + } + + member = await db + .Members.Include(m => m.User) + .FirstOrDefaultAsync(m => m.LegacyId == id && !m.User.Deleted, ct); + if (member != null) + return member; + + throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound); + } + + public async Task RenderMemberAsync( + Member m, + Token? token = default, + bool renderFlags = true, + CancellationToken ct = default + ) + { + bool renderUnlisted = m.UserId == token?.UserId; + + List flags = renderFlags + ? await db.MemberFlags.Where(f => f.MemberId == m.Id).OrderBy(f => f.Id).ToListAsync(ct) + : []; + + return new MemberResponse( + m.LegacyId, + m.Id, + m.Sid, + m.Name, + m.DisplayName, + m.Bio, + m.Avatar, + m.Links, + Names: FieldEntry.FromEntries(m.Names, m.User.CustomPreferences), + Pronouns: PronounEntry.FromPronouns(m.Pronouns, m.User.CustomPreferences), + Fields: ProfileField.FromFields(m.Fields, m.User.CustomPreferences), + Flags: flags + .Where(f => f.PrideFlag.Hash != null) + .Select(f => new PrideFlag( + f.PrideFlag.LegacyId, + f.PrideFlag.Id, + f.PrideFlag.Hash!, + f.PrideFlag.Name, + f.PrideFlag.Description + )) + .ToArray(), + User: UsersV1Service.RenderPartialUser(m.User), + Unlisted: renderUnlisted ? m.Unlisted : null + ); + } +} diff --git a/Foxnouns.Backend/Services/V1/UsersV1Service.cs b/Foxnouns.Backend/Services/V1/UsersV1Service.cs index 7492256..990812e 100644 --- a/Foxnouns.Backend/Services/V1/UsersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/UsersV1Service.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto.V1; using Microsoft.EntityFrameworkCore; using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; +using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag; namespace Foxnouns.Backend.Services.V1; @@ -49,8 +50,26 @@ public class UsersV1Service(DatabaseContext db) ); } - public async Task RenderUserAsync(User user) + public async Task RenderUserAsync( + User user, + Token? token = null, + bool renderMembers = true, + bool renderFlags = true, + CancellationToken ct = default + ) { + bool isSelfUser = user.Id == token?.UserId; + renderMembers = renderMembers && (isSelfUser || !user.ListHidden); + + // Only fetch members if we're rendering members (duh) + List members = renderMembers + ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) + : []; + + List flags = renderFlags + ? await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct) + : []; + int? utcOffset = null; if ( user.Timezone != null @@ -70,23 +89,67 @@ public class UsersV1Service(DatabaseContext db) user.MemberTitle, user.Avatar, user.Links, - FieldEntry.FromEntries(user.Names, user.CustomPreferences), - PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), - ProfileField.FromFields(user.Fields, user.CustomPreferences), + Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences), + Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), + Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences), + Flags: flags + .Where(f => f.PrideFlag.Hash != null) + .Select(f => new PrideFlag( + f.PrideFlag.LegacyId, + f.PrideFlag.Id, + f.PrideFlag.Hash!, + f.PrideFlag.Name, + f.PrideFlag.Description + )) + .ToArray(), + Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(), utcOffset, - user.CustomPreferences.Select(x => - ( - x.Value.LegacyId, - new CustomPreference( - x.Value.Icon, - x.Value.Tooltip, - x.Value.Size, - x.Value.Muted, - x.Value.Favourite - ) - ) - ) - .ToDictionary() + CustomPreferences: RenderCustomPreferences(user.CustomPreferences) ); } + + private static Dictionary RenderCustomPreferences( + Dictionary customPreferences + ) => + customPreferences + .Select(x => + ( + x.Value.LegacyId, + new CustomPreference( + x.Value.Icon, + x.Value.Tooltip, + x.Value.Size, + x.Value.Muted, + x.Value.Favourite + ) + ) + ) + .ToDictionary(); + + private static PartialMember RenderPartialMember( + Member m, + Dictionary customPreferences + ) => + new( + m.LegacyId, + m.Id, + m.Sid, + m.Name, + m.DisplayName, + m.Bio, + m.Avatar, + m.Links, + Names: FieldEntry.FromEntries(m.Names, customPreferences), + Pronouns: PronounEntry.FromPronouns(m.Pronouns, customPreferences) + ); + + public static PartialUser RenderPartialUser(User user) => + new( + user.LegacyId, + user.Id, + user.Username, + user.DisplayName, + user.Avatar, + CustomPreferences: RenderCustomPreferences(user.CustomPreferences) + ); } From e908e67ca6458f9df1b82566c00d22a56659ee4d Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 14:24:18 -0500 Subject: [PATCH 4/6] chore: license headers --- .../Controllers/DeleteUserController.cs | 14 ++++++++++++++ .../Controllers/NotificationsController.cs | 14 ++++++++++++++ .../Controllers/V1/UsersV1Controller.cs | 14 ++++++++++++++ Foxnouns.Backend/Dto/V1/Member.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/V1/User.cs | 15 +++++++++++++++ Foxnouns.Backend/Services/V1/MembersV1Service.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/V1/UsersV1Service.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/V1/V1Utils.cs | 14 ++++++++++++++ 8 files changed, 114 insertions(+) diff --git a/Foxnouns.Backend/Controllers/DeleteUserController.cs b/Foxnouns.Backend/Controllers/DeleteUserController.cs index b611c35..d1c8e62 100644 --- a/Foxnouns.Backend/Controllers/DeleteUserController.cs +++ b/Foxnouns.Backend/Controllers/DeleteUserController.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc; diff --git a/Foxnouns.Backend/Controllers/NotificationsController.cs b/Foxnouns.Backend/Controllers/NotificationsController.cs index f258b3c..873344c 100644 --- a/Foxnouns.Backend/Controllers/NotificationsController.cs +++ b/Foxnouns.Backend/Controllers/NotificationsController.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs index 75fd7b9..8433689 100644 --- a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs +++ b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Services.V1; using Microsoft.AspNetCore.Mvc; diff --git a/Foxnouns.Backend/Dto/V1/Member.cs b/Foxnouns.Backend/Dto/V1/Member.cs index 955e9af..c745187 100644 --- a/Foxnouns.Backend/Dto/V1/Member.cs +++ b/Foxnouns.Backend/Dto/V1/Member.cs @@ -1,3 +1,18 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; using Newtonsoft.Json; diff --git a/Foxnouns.Backend/Dto/V1/User.cs b/Foxnouns.Backend/Dto/V1/User.cs index 11ff066..c212d97 100644 --- a/Foxnouns.Backend/Dto/V1/User.cs +++ b/Foxnouns.Backend/Dto/V1/User.cs @@ -1,3 +1,18 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs index 521a924..5033e7f 100644 --- a/Foxnouns.Backend/Services/V1/MembersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/MembersV1Service.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto.V1; diff --git a/Foxnouns.Backend/Services/V1/UsersV1Service.cs b/Foxnouns.Backend/Services/V1/UsersV1Service.cs index 990812e..34163a6 100644 --- a/Foxnouns.Backend/Services/V1/UsersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/UsersV1Service.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto.V1; diff --git a/Foxnouns.Backend/Services/V1/V1Utils.cs b/Foxnouns.Backend/Services/V1/V1Utils.cs index eb8d9c0..2e52316 100644 --- a/Foxnouns.Backend/Services/V1/V1Utils.cs +++ b/Foxnouns.Backend/Services/V1/V1Utils.cs @@ -1,3 +1,17 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; From 78afb8b9c463097745d754796e0fbf2e0c5c4fa3 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 14:33:42 -0500 Subject: [PATCH 5/6] feat: GET /api/v1/users/{userRef}/members --- .../Controllers/V1/UsersV1Controller.cs | 39 ++++++++++++++++++- .../Services/V1/MembersV1Service.cs | 10 +++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs index 8433689..51e2b17 100644 --- a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs +++ b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs @@ -12,15 +12,21 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Dto.V1; using Foxnouns.Backend.Services.V1; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers.V1; [Route("/api/v1")] -public class UsersV1Controller(UsersV1Service usersV1Service, MembersV1Service membersV1Service) - : ApiControllerBase +public class UsersV1Controller( + UsersV1Service usersV1Service, + MembersV1Service membersV1Service, + DatabaseContext db +) : ApiControllerBase { [HttpGet("users/{userRef}")] public async Task GetUserAsync(string userRef, CancellationToken ct = default) @@ -50,4 +56,33 @@ public class UsersV1Controller(UsersV1Service usersV1Service, MembersV1Service m ) ); } + + [HttpGet("users/{userRef}/members")] + public async Task GetUserMembersAsync( + string userRef, + CancellationToken ct = default + ) + { + User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); + List members = await db + .Members.Where(m => m.UserId == user.Id) + .OrderBy(m => m.Name) + .ToListAsync(ct); + + List responses = []; + foreach (Member member in members) + { + responses.Add( + await membersV1Service.RenderMemberAsync( + member, + CurrentToken, + user, + renderFlags: true, + ct: ct + ) + ); + } + + return Ok(responses); + } } diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs index 5033e7f..b11a510 100644 --- a/Foxnouns.Backend/Services/V1/MembersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/MembersV1Service.cs @@ -47,10 +47,12 @@ public class MembersV1Service(DatabaseContext db) public async Task RenderMemberAsync( Member m, Token? token = default, + User? user = null, bool renderFlags = true, CancellationToken ct = default ) { + user ??= m.User; bool renderUnlisted = m.UserId == token?.UserId; List flags = renderFlags @@ -66,9 +68,9 @@ public class MembersV1Service(DatabaseContext db) m.Bio, m.Avatar, m.Links, - Names: FieldEntry.FromEntries(m.Names, m.User.CustomPreferences), - Pronouns: PronounEntry.FromPronouns(m.Pronouns, m.User.CustomPreferences), - Fields: ProfileField.FromFields(m.Fields, m.User.CustomPreferences), + Names: FieldEntry.FromEntries(m.Names, user.CustomPreferences), + Pronouns: PronounEntry.FromPronouns(m.Pronouns, user.CustomPreferences), + Fields: ProfileField.FromFields(m.Fields, user.CustomPreferences), Flags: flags .Where(f => f.PrideFlag.Hash != null) .Select(f => new PrideFlag( @@ -79,7 +81,7 @@ public class MembersV1Service(DatabaseContext db) f.PrideFlag.Description )) .ToArray(), - User: UsersV1Service.RenderPartialUser(m.User), + User: UsersV1Service.RenderPartialUser(user), Unlisted: renderUnlisted ? m.Unlisted : null ); } From 478ba2a4065f1aa18e2dc5c5f8f9db761ece6e16 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Dec 2024 14:53:36 -0500 Subject: [PATCH 6/6] feat: GET /api/v1/users/{userRef}/members/{memberRef} --- ...ersV1Controller.cs => V1ReadController.cs} | 25 +++++++++++- .../Services/V1/MembersV1Service.cs | 39 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) rename Foxnouns.Backend/Controllers/V1/{UsersV1Controller.cs => V1ReadController.cs} (82%) diff --git a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs b/Foxnouns.Backend/Controllers/V1/V1ReadController.cs similarity index 82% rename from Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs rename to Foxnouns.Backend/Controllers/V1/V1ReadController.cs index 51e2b17..5f69c20 100644 --- a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs +++ b/Foxnouns.Backend/Controllers/V1/V1ReadController.cs @@ -22,7 +22,7 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers.V1; [Route("/api/v1")] -public class UsersV1Controller( +public class V1ReadController( UsersV1Service usersV1Service, MembersV1Service membersV1Service, DatabaseContext db @@ -85,4 +85,27 @@ public class UsersV1Controller( return Ok(responses); } + + [HttpGet("users/{userRef}/members/{memberRef}")] + public async Task GetUserMemberAsync( + string userRef, + string memberRef, + CancellationToken ct = default + ) + { + Member member = await membersV1Service.ResolveMemberAsync( + userRef, + memberRef, + CurrentToken, + ct + ); + return Ok( + await membersV1Service.RenderMemberAsync( + member, + CurrentToken, + renderFlags: true, + ct: ct + ) + ); + } } diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs index b11a510..632226c 100644 --- a/Foxnouns.Backend/Services/V1/MembersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/MembersV1Service.cs @@ -21,7 +21,7 @@ using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag; namespace Foxnouns.Backend.Services.V1; -public class MembersV1Service(DatabaseContext db) +public class MembersV1Service(DatabaseContext db, UsersV1Service usersV1Service) { public async Task ResolveMemberAsync(string id, CancellationToken ct = default) { @@ -44,6 +44,43 @@ public class MembersV1Service(DatabaseContext db) throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound); } + public async Task ResolveMemberAsync( + string userRef, + string memberRef, + Token? token, + CancellationToken ct = default + ) + { + User user = await usersV1Service.ResolveUserAsync(userRef, token, ct); + + Member? member; + if (Snowflake.TryParse(memberRef, out Snowflake? sf)) + { + member = await db + .Members.Include(m => m.User) + .FirstOrDefaultAsync(m => m.Id == sf && m.UserId == user.Id, ct); + if (member != null) + return member; + } + + member = await db + .Members.Include(m => m.User) + .FirstOrDefaultAsync(m => m.LegacyId == memberRef && m.UserId == user.Id, ct); + if (member != null) + return member; + + member = await db + .Members.Include(m => m.User) + .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == user.Id, ct); + if (member != null) + return member; + + throw new ApiError.NotFound( + "No member with that ID or name found.", + ErrorCode.MemberNotFound + ); + } + public async Task RenderMemberAsync( Member m, Token? token = default,