Compare commits
2 commits
e95e0a79ff
...
16f230b97d
Author | SHA1 | Date | |
---|---|---|---|
16f230b97d | |||
fa49030b06 |
20 changed files with 1307 additions and 58 deletions
45
ENDPOINTS.md
Normal file
45
ENDPOINTS.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# List of API endpoints and scopes
|
||||
|
||||
## Scopes
|
||||
|
||||
- `identify`: `@me` will refer to token user (always granted)
|
||||
- `user.read_privileged`: can read privileged information such as authentication methods
|
||||
- `user.update`: can update the user's profile.
|
||||
**cannot** update anything locked behind `user.read_privileged`
|
||||
- `member.read`: can view member list if it's hidden and enumerate unlisted members
|
||||
- `member.create`: can create new members
|
||||
- `member.update`: can edit and delete members
|
||||
|
||||
## Meta
|
||||
|
||||
- [ ] GET `/meta`: gets stats and server information
|
||||
|
||||
## Users
|
||||
|
||||
- [ ] GET `/users/{userRef}`: views current user.
|
||||
`identify` required to use `@me` as user reference.
|
||||
`user.read_privileged` required to view authentication methods.
|
||||
`member.read` required to view unlisted members.
|
||||
- [ ] PATCH `/users/@me`: updates current user. `user.update` required.
|
||||
- [ ] DELETE `/users/@me`: deletes current user. `*` required
|
||||
- [ ] POST `/users/@me/export`: queues new data export. `*` required
|
||||
- [ ] GET `/users/@me/export`: gets latest data export. `*` required
|
||||
- [ ] GET `/users/@me/flags`: get all the user's flags. `identify` required
|
||||
- [ ] POST `/users/@me/flags`: creates a new flag. `user.update` required
|
||||
- [ ] PATCH `/users/@me/flags/{id}`: updates an existing flag. `user.update` required
|
||||
- [ ] DELETE `/users/@me/flags/{id}`: deletes a user flag. `user.update` required
|
||||
- [ ] POST `/users/@me/reroll`: rerolls a user's short ID. `user.update` required
|
||||
|
||||
## Members
|
||||
|
||||
- [ ] GET `/users/{userRef}/members`: gets list of a user's members.
|
||||
if the user's member list is hidden,
|
||||
and it is not the authenticated user (or the token doesn't have the `member.read` scope)
|
||||
returns an empty array.
|
||||
- [ ] GET `/users/{userRef}/members/{memberRef}`: gets a single member.
|
||||
will always return a member if it exists, even if the member is unlisted.
|
||||
- [ ] POST `/users/@me/members`: creates a new member. `member.create` required
|
||||
- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required
|
||||
- [ ] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required
|
||||
- [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required.
|
||||
-
|
|
@ -10,6 +10,7 @@ public class Config
|
|||
public string MediaBaseUrl { get; set; } = null!;
|
||||
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
public string? MetricsAddress => Logging.MetricsPort != null ? $"http://{Host}:{Logging.MetricsPort}" : null;
|
||||
|
||||
public LoggingConfig Logging { get; init; } = new();
|
||||
public DatabaseConfig Database { get; init; } = new();
|
||||
|
@ -26,6 +27,8 @@ public class Config
|
|||
public string? SentryUrl { get; init; }
|
||||
public bool SentryTracing { get; init; } = false;
|
||||
public double SentryTracesSampleRate { get; init; } = 0.0;
|
||||
public bool LogQueries { get; init; } = false;
|
||||
public int? MetricsPort { get; init; }
|
||||
}
|
||||
|
||||
public class DatabaseConfig
|
||||
|
|
|
@ -1,29 +1,38 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/meta")]
|
||||
public class MetaController(DatabaseContext db) : ApiControllerBase
|
||||
public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBase
|
||||
{
|
||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))]
|
||||
public async Task<IActionResult> GetMeta()
|
||||
{
|
||||
var userCount = await db.Users.CountAsync();
|
||||
var now = clock.GetCurrentInstant();
|
||||
var users = await db.Users.Select(u => u.LastActive).ToListAsync();
|
||||
var memberCount = await db.Members.CountAsync();
|
||||
|
||||
return Ok(new MetaResponse(
|
||||
BuildInfo.Version, BuildInfo.Hash, memberCount,
|
||||
new UserInfo(userCount, 0, 0, 0))
|
||||
Repository, BuildInfo.Version, BuildInfo.Hash, memberCount,
|
||||
new UserInfo(
|
||||
users.Count,
|
||||
users.Count(i => i > now - Duration.FromDays(30)),
|
||||
users.Count(i => i > now - Duration.FromDays(7)),
|
||||
users.Count(i => i > now - Duration.FromDays(1))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v2/coffee")]
|
||||
public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
|
||||
|
||||
private record MetaResponse(string Version, string Hash, int Members, UserInfo Users);
|
||||
private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users);
|
||||
|
||||
private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
|
||||
}
|
|
@ -10,6 +10,7 @@ namespace Foxnouns.Backend.Database;
|
|||
public class DatabaseContext : DbContext
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Member> Members { get; set; }
|
||||
|
@ -19,7 +20,7 @@ public class DatabaseContext : DbContext
|
|||
public DbSet<Application> Applications { get; set; }
|
||||
public DbSet<TemporaryKey> TemporaryKeys { get; set; }
|
||||
|
||||
public DatabaseContext(Config config)
|
||||
public DatabaseContext(Config config, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||
{
|
||||
|
@ -30,13 +31,15 @@ public class DatabaseContext : DbContext
|
|||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||
dataSourceBuilder.UseNodaTime();
|
||||
_dataSource = dataSourceBuilder.Build();
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder
|
||||
.ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning))
|
||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||
.UseSnakeCaseNamingConvention();
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseLoggerFactory(_loggerFactory);
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
|
@ -73,6 +76,6 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory<Data
|
|||
// Get the configuration as our config class
|
||||
.Get<Config>() ?? new();
|
||||
|
||||
return new DatabaseContext(config);
|
||||
return new DatabaseContext(config, null);
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@ public static class DatabaseQueryExtensions
|
|||
if (Snowflake.TryParse(userRef, out var snowflake))
|
||||
{
|
||||
user = await context.Users
|
||||
.Where(u => !u.Deleted)
|
||||
.FirstOrDefaultAsync(u => u.Id == snowflake);
|
||||
if (user != null) return user;
|
||||
}
|
||||
|
||||
user = await context.Users
|
||||
.Where(u => !u.Deleted)
|
||||
.FirstOrDefaultAsync(u => u.Username == userRef);
|
||||
if (user != null) return user;
|
||||
throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound);
|
||||
|
@ -28,6 +30,7 @@ public static class DatabaseQueryExtensions
|
|||
public static async Task<User> ResolveUserAsync(this DatabaseContext context, Snowflake id)
|
||||
{
|
||||
var user = await context.Users
|
||||
.Where(u => !u.Deleted)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user != null) return user;
|
||||
throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound);
|
||||
|
@ -37,6 +40,7 @@ public static class DatabaseQueryExtensions
|
|||
{
|
||||
var member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.Where(m => !m.User.Deleted)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
if (member != null) return member;
|
||||
throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound);
|
||||
|
@ -56,12 +60,14 @@ public static class DatabaseQueryExtensions
|
|||
{
|
||||
member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.Where(m => !m.User.Deleted)
|
||||
.FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId);
|
||||
if (member != null) return member;
|
||||
}
|
||||
|
||||
member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.Where(m => !m.User.Deleted)
|
||||
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId);
|
||||
if (member != null) return member;
|
||||
throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound);
|
||||
|
|
515
Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs
generated
Normal file
515
Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs
generated
Normal file
|
@ -0,0 +1,515 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240712233806_AddUserLastActive")]
|
||||
partial class AddUserLastActive
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<Instant>("Expires")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("value");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_temporary_keys");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_temporary_keys_key");
|
||||
|
||||
b.ToTable("temporary_keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserLastActive : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "last_active",
|
||||
table: "users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValueSql: "now()");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "last_active",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
528
Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs
generated
Normal file
528
Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs
generated
Normal file
|
@ -0,0 +1,528 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240713000719_AddDeleted")]
|
||||
partial class AddDeleted
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<Instant>("Expires")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("value");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_temporary_keys");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_temporary_keys_key");
|
||||
|
||||
b.ToTable("temporary_keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeleted : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deleted",
|
||||
table: "users",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "deleted_at",
|
||||
table: "users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "deleted_by",
|
||||
table: "users",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deleted",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deleted_at",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deleted_by",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -267,10 +267,26 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
|
@ -335,7 +351,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Fields#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -345,7 +361,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members", (string)null);
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
|
@ -354,7 +370,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Names#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -364,7 +380,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members", (string)null);
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
|
@ -373,7 +389,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Pronouns#System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -383,7 +399,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members", (string)null);
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
|
@ -427,7 +443,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -438,7 +454,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users", (string)null);
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
|
@ -447,7 +463,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -458,7 +474,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users", (string)null);
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
|
@ -467,7 +483,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
@ -478,7 +494,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users", (string)null);
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
|
@ -21,6 +22,13 @@ public class User : BaseModel
|
|||
|
||||
public List<Member> Members { get; } = [];
|
||||
public List<AuthMethod> AuthMethods { get; } = [];
|
||||
|
||||
public required Instant LastActive { get; set; }
|
||||
|
||||
public bool Deleted { get; set; }
|
||||
public Instant? DeletedAt { get; set; }
|
||||
public Snowflake? DeletedBy { get; set; }
|
||||
[NotMapped] public bool? SelfDelete => Deleted ? DeletedBy != null : null;
|
||||
}
|
||||
|
||||
public enum UserRole
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
using App.Metrics;
|
||||
using App.Metrics.AspNetCore;
|
||||
using App.Metrics.Formatters.Prometheus;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
|
@ -6,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using NodaTime;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using IClock = NodaTime.IClock;
|
||||
|
||||
namespace Foxnouns.Backend.Extensions;
|
||||
|
||||
|
@ -24,9 +28,12 @@ public static class WebApplicationExtensions
|
|||
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
||||
// Serilog doesn't disable the built-in logs, so we do it here.
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command",
|
||||
config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Hangfire", LogEventLevel.Information)
|
||||
.WriteTo.Console();
|
||||
|
||||
if (config.Logging.SeqLogUrl != null)
|
||||
|
@ -34,10 +41,8 @@ public static class WebApplicationExtensions
|
|||
logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose);
|
||||
}
|
||||
|
||||
Log.Logger = logCfg.CreateLogger();
|
||||
|
||||
// AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually.
|
||||
builder.Services.AddSerilog().AddSingleton(Log.Logger);
|
||||
builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
@ -52,6 +57,36 @@ public static class WebApplicationExtensions
|
|||
return config;
|
||||
}
|
||||
|
||||
public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder)
|
||||
{
|
||||
var config = builder.Configuration.Get<Config>() ?? new();
|
||||
var metrics = AppMetrics.CreateDefaultBuilder()
|
||||
.OutputMetrics.AsPrometheusPlainText()
|
||||
.Build();
|
||||
|
||||
builder.Services.AddSingleton(metrics);
|
||||
builder.Services.AddSingleton<IMetrics>(metrics);
|
||||
|
||||
builder.WebHost
|
||||
.ConfigureMetrics(metrics)
|
||||
.UseMetrics(opts =>
|
||||
{
|
||||
opts.EndpointOptions = options =>
|
||||
{
|
||||
// Metrics must listen on a separate port for security reasons. If no metrics port is set, disable the endpoint entirely.
|
||||
options.MetricsEndpointEnabled = config.Logging.MetricsPort != null;
|
||||
options.EnvironmentInfoEndpointEnabled = config.Logging.MetricsPort != null;
|
||||
options.MetricsTextEndpointEnabled = false;
|
||||
options.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters
|
||||
.OfType<MetricsPrometheusTextOutputFormatter>().First();
|
||||
};
|
||||
})
|
||||
.UseMetricsWebTracking()
|
||||
.ConfigureAppMetricsHostingConfiguration(opts => { opts.AllEndpointsPort = config.Logging.MetricsPort; });
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder)
|
||||
{
|
||||
var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini";
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="App.Metrics" Version="4.3.0" />
|
||||
<PackageReference Include="App.Metrics.AspNetCore.All" Version="4.3.0" />
|
||||
<PackageReference Include="App.Metrics.Prometheus" Version="4.3.0" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.14" />
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.14" />
|
||||
|
|
6
Foxnouns.Backend/Metrics.cs
Normal file
6
Foxnouns.Backend/Metrics.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Foxnouns.Backend;
|
||||
|
||||
public static class Metrics
|
||||
{
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ var builder = WebApplication.CreateBuilder(args);
|
|||
|
||||
var config = builder.AddConfiguration();
|
||||
|
||||
builder.AddSerilog();
|
||||
builder.AddSerilog().AddMetrics();
|
||||
|
||||
builder.WebHost
|
||||
.UseSentry(opts =>
|
||||
|
@ -103,6 +103,7 @@ app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|||
|
||||
app.Urls.Clear();
|
||||
app.Urls.Add(config.Address);
|
||||
if (config.MetricsAddress != null) app.Urls.Add(config.MetricsAddress);
|
||||
|
||||
// Fire off the periodic tasks loop in the background
|
||||
_ = new Timer(_ =>
|
||||
|
|
|
@ -8,7 +8,7 @@ using NodaTime;
|
|||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator)
|
||||
public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator)
|
||||
{
|
||||
private readonly PasswordHasher<User> _passwordHasher = new();
|
||||
|
||||
|
@ -26,7 +26,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator
|
|||
{
|
||||
new AuthMethod
|
||||
{ Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email }
|
||||
}
|
||||
},
|
||||
LastActive = clock.GetCurrentInstant()
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
@ -59,7 +60,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator
|
|||
Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId,
|
||||
RemoteUsername = remoteUsername, FediverseApplication = instance
|
||||
}
|
||||
}
|
||||
},
|
||||
LastActive = clock.GetCurrentInstant()
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
|
|
@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
|
||||
|
@ -14,12 +15,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
|||
bool renderAuthMethods = false)
|
||||
{
|
||||
var isSelfUser = selfUser?.Id == user.Id;
|
||||
var tokenCanReadHiddenMembers = token.HasScope("member.read");
|
||||
var tokenCanReadAuth = token.HasScope("user.read_privileged");
|
||||
var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser;
|
||||
var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser;
|
||||
|
||||
renderMembers = renderMembers &&
|
||||
(!user.ListHidden || (isSelfUser && tokenCanReadHiddenMembers));
|
||||
renderAuthMethods = renderAuthMethods && isSelfUser && tokenCanReadAuth;
|
||||
(!user.ListHidden || tokenCanReadHiddenMembers);
|
||||
renderAuthMethods = renderAuthMethods && tokenPrivileged;
|
||||
|
||||
IEnumerable<Member> members =
|
||||
renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : [];
|
||||
|
@ -34,7 +35,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
|||
: [];
|
||||
|
||||
return new UserResponse(
|
||||
user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names,
|
||||
user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links,
|
||||
user.Names,
|
||||
user.Pronouns, user.Fields,
|
||||
renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null,
|
||||
renderAuthMethods
|
||||
|
@ -42,7 +44,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
|||
a.Id, a.AuthType, a.RemoteId,
|
||||
a.RemoteUsername, a.FediverseApplication?.Domain
|
||||
))
|
||||
: null
|
||||
: null,
|
||||
tokenPrivileged ? user.LastActive : null
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -63,7 +66,9 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
IEnumerable<MemberRendererService.PartialMember>? Members,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
IEnumerable<AuthenticationMethodResponse>? AuthMethods
|
||||
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
Instant? LastActive
|
||||
);
|
||||
|
||||
public record AuthenticationMethodResponse(
|
||||
|
|
|
@ -75,7 +75,6 @@ public static class AuthUtils
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error converting string: {e}");
|
||||
bytes = [];
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -18,11 +18,14 @@ SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0
|
|||
SentryTracing = true
|
||||
; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all)
|
||||
SentryTracesSampleRate = 1.0
|
||||
; Whether to log SQL queries. Note that this is very verbose. Defaults to false.
|
||||
LogQueries = false
|
||||
; The port the /metrics endpoint will listen on. If not set, metrics will be disabled.
|
||||
MetricsPort = 5001
|
||||
|
||||
[Database]
|
||||
; The database URL in ADO.NET format.
|
||||
Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns"
|
||||
|
||||
; The timeout for opening new connections. Defaults to 5.
|
||||
Timeout = 5
|
||||
; The maximum number of open connections. Defaults to 50.
|
||||
|
|
18
SCOPES.md
18
SCOPES.md
|
@ -1,18 +0,0 @@
|
|||
# List of API endpoints and scopes
|
||||
|
||||
## Scopes
|
||||
|
||||
- `identify`: `@me` will refer to token user (always granted)
|
||||
- `user.read_privileged`: can read privileged information such as authentication methods
|
||||
- `user.update`: can update the user's profile.
|
||||
**cannot** update anything locked behind `user.read_privileged`
|
||||
- `member.read`: can view member list if it's hidden and enumerate unlisted members
|
||||
- `member.create`: can create new members
|
||||
- `member.update`: can edit and delete members
|
||||
|
||||
## Users
|
||||
|
||||
- GET `/users/{userRef}`: `identify` required to use `@me` as user reference.
|
||||
`user.read_privileged` required to view authentication methods.
|
||||
`member.read` required to view unlisted members.
|
||||
- PATCH `/users/@me`: `user.update` required.
|
Loading…
Reference in a new issue