chore: add csharpier to husky, format backend with csharpier

This commit is contained in:
sam 2024-10-02 00:28:07 +02:00
parent 5fab66444f
commit 7f971e8549
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
73 changed files with 2098 additions and 1048 deletions

View file

@ -3,4 +3,4 @@ namespace Foxnouns.Backend.Database;
public abstract class BaseModel
{
public required Snowflake Id { get; init; } = SnowflakeGenerator.Instance.GenerateSnowflake();
}
}

View file

@ -45,11 +45,12 @@ public class DatabaseContext : DbContext
_loggerFactory = loggerFactory;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder
.ConfigureWarnings(c =>
c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)
.Ignore(CoreEventId.SaveChangesFailed))
.Ignore(CoreEventId.SaveChangesFailed)
)
.UseNpgsql(_dataSource, o => o.UseNodaTime())
.UseSnakeCaseNamingConvention()
.UseLoggerFactory(_loggerFactory)
@ -76,7 +77,10 @@ public class DatabaseContext : DbContext
modelBuilder.Entity<User>().Property(u => u.CustomPreferences).HasColumnType("jsonb");
modelBuilder.Entity<User>().Property(u => u.Settings).HasColumnType("jsonb");
modelBuilder.Entity<Member>().Property(m => m.Sid).HasDefaultValueSql("find_free_member_sid()");
modelBuilder
.Entity<Member>()
.Property(m => m.Sid)
.HasDefaultValueSql("find_free_member_sid()");
modelBuilder.Entity<Member>().Property(m => m.Fields).HasColumnType("jsonb");
modelBuilder.Entity<Member>().Property(m => m.Names).HasColumnType("jsonb");
modelBuilder.Entity<Member>().Property(m => m.Pronouns).HasColumnType("jsonb");
@ -84,10 +88,12 @@ public class DatabaseContext : DbContext
modelBuilder.Entity<UserFlag>().Navigation(f => f.PrideFlag).AutoInclude();
modelBuilder.Entity<MemberFlag>().Navigation(f => f.PrideFlag).AutoInclude();
modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!)
modelBuilder
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!)
.HasName("find_free_user_sid");
modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!)
modelBuilder
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!)
.HasName("find_free_member_sid");
}
@ -102,18 +108,23 @@ public class DatabaseContext : DbContext
public string FindFreeMemberSid() => throw new NotSupportedException();
}
[SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Used by EF Core's migration generator")]
[SuppressMessage(
"ReSharper",
"UnusedType.Global",
Justification = "Used by EF Core's migration generator"
)]
public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory<DatabaseContext>
{
public DatabaseContext CreateDbContext(string[] args)
{
// Read the configuration file
var config = new ConfigurationBuilder()
.AddConfiguration()
.Build()
// Get the configuration as our config class
.Get<Config>() ?? new();
var config =
new ConfigurationBuilder()
.AddConfiguration()
.Build()
// Get the configuration as our config class
.Get<Config>() ?? new();
return new DatabaseContext(config, null);
}
}
}

View file

@ -8,89 +8,128 @@ namespace Foxnouns.Backend.Database;
public static class DatabaseQueryExtensions
{
public static async Task<User> ResolveUserAsync(this DatabaseContext context, string userRef, Token? token,
CancellationToken ct = default)
public static async Task<User> ResolveUserAsync(
this DatabaseContext context,
string userRef,
Token? token,
CancellationToken ct = default
)
{
if (userRef == "@me")
{
return token != null
? await context.Users.FirstAsync(u => u.Id == token.UserId, ct)
: throw new ApiError.Unauthorized("This endpoint requires an authenticated user.",
ErrorCode.AuthenticationRequired);
: throw new ApiError.Unauthorized(
"This endpoint requires an authenticated user.",
ErrorCode.AuthenticationRequired
);
}
User? user;
if (Snowflake.TryParse(userRef, out var snowflake))
{
user = await context.Users
.Where(u => !u.Deleted)
user = await context
.Users.Where(u => !u.Deleted)
.FirstOrDefaultAsync(u => u.Id == snowflake, ct);
if (user != null) return user;
if (user != null)
return user;
}
user = await context.Users
.Where(u => !u.Deleted)
user = await context
.Users.Where(u => !u.Deleted)
.FirstOrDefaultAsync(u => u.Username == userRef, ct);
if (user != null) return user;
throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound);
if (user != null)
return user;
throw new ApiError.NotFound(
"No user with that ID or username found.",
code: ErrorCode.UserNotFound
);
}
public static async Task<User> ResolveUserAsync(this DatabaseContext context, Snowflake id,
CancellationToken ct = default)
public static async Task<User> ResolveUserAsync(
this DatabaseContext context,
Snowflake id,
CancellationToken ct = default
)
{
var user = await context.Users
.Where(u => !u.Deleted)
var user = await context
.Users.Where(u => !u.Deleted)
.FirstOrDefaultAsync(u => u.Id == id, ct);
if (user != null) return user;
if (user != null)
return user;
throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound);
}
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, Snowflake id,
CancellationToken ct = default)
public static async Task<Member> ResolveMemberAsync(
this DatabaseContext context,
Snowflake id,
CancellationToken ct = default
)
{
var member = await context.Members
.Include(m => m.User)
var member = await context
.Members.Include(m => m.User)
.Where(m => !m.User.Deleted)
.FirstOrDefaultAsync(m => m.Id == id, ct);
if (member != null) return member;
throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound);
if (member != null)
return member;
throw new ApiError.NotFound(
"No member with that ID found.",
code: ErrorCode.MemberNotFound
);
}
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef,
Token? token, CancellationToken ct = default)
public static async Task<Member> ResolveMemberAsync(
this DatabaseContext context,
string userRef,
string memberRef,
Token? token,
CancellationToken ct = default
)
{
var user = await context.ResolveUserAsync(userRef, token, ct);
return await context.ResolveMemberAsync(user.Id, memberRef, ct);
}
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, Snowflake userId,
string memberRef, CancellationToken ct = default)
public static async Task<Member> ResolveMemberAsync(
this DatabaseContext context,
Snowflake userId,
string memberRef,
CancellationToken ct = default
)
{
Member? member;
if (Snowflake.TryParse(memberRef, out var snowflake))
{
member = await context.Members
.Include(m => m.User)
member = await context
.Members.Include(m => m.User)
.Include(m => m.ProfileFlags)
.Where(m => !m.User.Deleted)
.FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct);
if (member != null) return member;
if (member != null)
return member;
}
member = await context.Members
.Include(m => m.User)
member = await context
.Members.Include(m => m.User)
.Include(m => m.ProfileFlags)
.Where(m => !m.User.Deleted)
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct);
if (member != null) return member;
throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound);
if (member != null)
return member;
throw new ApiError.NotFound(
"No member with that ID or name found.",
code: ErrorCode.MemberNotFound
);
}
public static async Task<Application> GetFrontendApplicationAsync(this DatabaseContext context,
CancellationToken ct = default)
public static async Task<Application> GetFrontendApplicationAsync(
this DatabaseContext context,
CancellationToken ct = default
)
{
var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0), ct);
if (app != null) return app;
if (app != null)
return app;
app = new Application
{
@ -107,27 +146,42 @@ public static class DatabaseQueryExtensions
return app;
}
public static async Task<Token?> GetToken(this DatabaseContext context, byte[] rawToken,
CancellationToken ct = default)
public static async Task<Token?> GetToken(
this DatabaseContext context,
byte[] rawToken,
CancellationToken ct = default
)
{
var hash = SHA512.HashData(rawToken);
var oauthToken = await context.Tokens
.Include(t => t.Application)
var oauthToken = await context
.Tokens.Include(t => t.Application)
.Include(t => t.User)
.FirstOrDefaultAsync(
t => t.Hash == hash && t.ExpiresAt > SystemClock.Instance.GetCurrentInstant() && !t.ManuallyExpired,
ct);
t =>
t.Hash == hash
&& t.ExpiresAt > SystemClock.Instance.GetCurrentInstant()
&& !t.ManuallyExpired,
ct
);
return oauthToken;
}
public static async Task<Snowflake?> GetTokenUserId(this DatabaseContext context, byte[] rawToken,
CancellationToken ct = default)
public static async Task<Snowflake?> GetTokenUserId(
this DatabaseContext context,
byte[] rawToken,
CancellationToken ct = default
)
{
var hash = SHA512.HashData(rawToken);
return await context.Tokens
.Where(t => t.Hash == hash && t.ExpiresAt > SystemClock.Instance.GetCurrentInstant() && !t.ManuallyExpired)
.Select(t => t.UserId).FirstOrDefaultAsync(ct);
return await context
.Tokens.Where(t =>
t.Hash == hash
&& t.ExpiresAt > SystemClock.Instance.GetCurrentInstant()
&& !t.ManuallyExpired
)
.Select(t => t.UserId)
.FirstOrDefaultAsync(ct);
}
}
}

View file

@ -5,23 +5,30 @@ namespace Foxnouns.Backend.Database;
public static class FlagQueryExtensions
{
private static async Task<List<PrideFlag>> GetFlagsAsync(this DatabaseContext db, Snowflake userId) =>
await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync();
private static async Task<List<PrideFlag>> GetFlagsAsync(
this DatabaseContext db,
Snowflake userId
) => await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync();
/// <summary>
/// Sets the user's profile flags to the given IDs. Returns a validation error if any of the flag IDs are unknown
/// or if too many IDs are given. Duplicates are allowed.
/// </summary>
public static async Task<ValidationError?> SetUserFlagsAsync(this DatabaseContext db, Snowflake userId,
Snowflake[] flagIds)
public static async Task<ValidationError?> SetUserFlagsAsync(
this DatabaseContext db,
Snowflake userId,
Snowflake[] flagIds
)
{
var currentFlags = await db.UserFlags.Where(f => f.UserId == userId).ToListAsync();
foreach (var flag in currentFlags)
db.UserFlags.Remove(flag);
// If there's no new flags to set, we're done
if (flagIds.Length == 0) return null;
if (flagIds.Length > 100) return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
if (flagIds.Length == 0)
return null;
if (flagIds.Length > 100)
return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
var flags = await db.GetFlagsAsync(userId);
var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray();
@ -34,24 +41,34 @@ public static class FlagQueryExtensions
return null;
}
public static async Task<ValidationError?> SetMemberFlagsAsync(this DatabaseContext db, Snowflake userId,
Snowflake memberId, Snowflake[] flagIds)
public static async Task<ValidationError?> SetMemberFlagsAsync(
this DatabaseContext db,
Snowflake userId,
Snowflake memberId,
Snowflake[] flagIds
)
{
var currentFlags = await db.MemberFlags.Where(f => f.MemberId == memberId).ToListAsync();
foreach (var flag in currentFlags)
db.MemberFlags.Remove(flag);
if (flagIds.Length == 0) return null;
if (flagIds.Length > 100) return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
if (flagIds.Length == 0)
return null;
if (flagIds.Length > 100)
return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
var flags = await db.GetFlagsAsync(userId);
var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray();
if (unknownFlagIds.Length != 0)
return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds);
var memberFlags = flagIds.Select(id => new MemberFlag { PrideFlagId = id, MemberId = memberId });
var memberFlags = flagIds.Select(id => new MemberFlag
{
PrideFlagId = id,
MemberId = memberId,
});
db.MemberFlags.AddRange(memberFlags);
return null;
}
}
}

View file

@ -5,4 +5,4 @@ namespace Foxnouns.Backend.Database;
public interface ISnowflakeGenerator
{
Snowflake GenerateSnowflake(Instant? time = null);
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
@ -22,12 +22,13 @@ namespace Foxnouns.Backend.Database.Migrations
domain = table.Column<string>(type: "text", nullable: false),
client_id = table.Column<string>(type: "text", nullable: false),
client_secret = table.Column<string>(type: "text", nullable: false),
instance_type = table.Column<int>(type: "integer", nullable: false)
instance_type = table.Column<int>(type: "integer", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("pk_fediverse_applications", x => x.id);
});
}
);
migrationBuilder.CreateTable(
name: "users",
@ -43,12 +44,13 @@ namespace Foxnouns.Backend.Database.Migrations
role = table.Column<int>(type: "integer", nullable: false),
fields = table.Column<string>(type: "jsonb", nullable: false),
names = table.Column<string>(type: "jsonb", nullable: false),
pronouns = table.Column<string>(type: "jsonb", nullable: false)
pronouns = table.Column<string>(type: "jsonb", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("pk_users", x => x.id);
});
}
);
migrationBuilder.CreateTable(
name: "auth_methods",
@ -59,7 +61,7 @@ namespace Foxnouns.Backend.Database.Migrations
remote_id = table.Column<string>(type: "text", nullable: false),
remote_username = table.Column<string>(type: "text", nullable: true),
user_id = table.Column<long>(type: "bigint", nullable: false),
fediverse_application_id = table.Column<long>(type: "bigint", nullable: true)
fediverse_application_id = table.Column<long>(type: "bigint", nullable: true),
},
constraints: table =>
{
@ -68,14 +70,17 @@ namespace Foxnouns.Backend.Database.Migrations
name: "fk_auth_methods_fediverse_applications_fediverse_application_id",
column: x => x.fediverse_application_id,
principalTable: "fediverse_applications",
principalColumn: "id");
principalColumn: "id"
);
table.ForeignKey(
name: "fk_auth_methods_users_user_id",
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateTable(
name: "members",
@ -91,7 +96,7 @@ namespace Foxnouns.Backend.Database.Migrations
user_id = table.Column<long>(type: "bigint", nullable: false),
fields = table.Column<string>(type: "jsonb", nullable: false),
names = table.Column<string>(type: "jsonb", nullable: false),
pronouns = table.Column<string>(type: "jsonb", nullable: false)
pronouns = table.Column<string>(type: "jsonb", nullable: false),
},
constraints: table =>
{
@ -101,18 +106,23 @@ namespace Foxnouns.Backend.Database.Migrations
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateTable(
name: "tokens",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false),
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
expires_at = table.Column<Instant>(
type: "timestamp with time zone",
nullable: false
),
scopes = table.Column<string[]>(type: "text[]", nullable: false),
manually_expired = table.Column<bool>(type: "boolean", nullable: false),
user_id = table.Column<long>(type: "bigint", nullable: false)
user_id = table.Column<long>(type: "bigint", nullable: false),
},
constraints: table =>
{
@ -122,53 +132,56 @@ namespace Foxnouns.Backend.Database.Migrations
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateIndex(
name: "ix_auth_methods_fediverse_application_id",
table: "auth_methods",
column: "fediverse_application_id");
column: "fediverse_application_id"
);
migrationBuilder.CreateIndex(
name: "ix_auth_methods_user_id",
table: "auth_methods",
column: "user_id");
column: "user_id"
);
// EF Core doesn't support creating indexes on arbitrary expressions, so we have to create it manually.
// Due to historical reasons (I made a mistake while writing the initial migration for the Go version)
// only members have case-insensitive names.
migrationBuilder.Sql("CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))");
migrationBuilder.Sql(
"CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))"
);
migrationBuilder.CreateIndex(
name: "ix_tokens_user_id",
table: "tokens",
column: "user_id");
column: "user_id"
);
migrationBuilder.CreateIndex(
name: "ix_users_username",
table: "users",
column: "username",
unique: true);
unique: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "auth_methods");
migrationBuilder.DropTable(name: "auth_methods");
migrationBuilder.DropTable(
name: "members");
migrationBuilder.DropTable(name: "members");
migrationBuilder.DropTable(
name: "tokens");
migrationBuilder.DropTable(name: "tokens");
migrationBuilder.DropTable(
name: "fediverse_applications");
migrationBuilder.DropTable(name: "fediverse_applications");
migrationBuilder.DropTable(
name: "users");
migrationBuilder.DropTable(name: "users");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -18,14 +18,16 @@ namespace Foxnouns.Backend.Database.Migrations
table: "tokens",
type: "bigint",
nullable: false,
defaultValue: 0L);
defaultValue: 0L
);
migrationBuilder.AddColumn<byte[]>(
name: "hash",
table: "tokens",
type: "bytea",
nullable: false,
defaultValue: new byte[0]);
defaultValue: new byte[0]
);
migrationBuilder.CreateTable(
name: "applications",
@ -36,17 +38,19 @@ namespace Foxnouns.Backend.Database.Migrations
client_secret = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false),
scopes = table.Column<string[]>(type: "text[]", nullable: false),
redirect_uris = table.Column<string[]>(type: "text[]", nullable: false)
redirect_uris = table.Column<string[]>(type: "text[]", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("pk_applications", x => x.id);
});
}
);
migrationBuilder.CreateIndex(
name: "ix_tokens_application_id",
table: "tokens",
column: "application_id");
column: "application_id"
);
migrationBuilder.AddForeignKey(
name: "fk_tokens_applications_application_id",
@ -54,7 +58,8 @@ namespace Foxnouns.Backend.Database.Migrations
column: "application_id",
principalTable: "applications",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Cascade
);
}
/// <inheritdoc />
@ -62,22 +67,16 @@ namespace Foxnouns.Backend.Database.Migrations
{
migrationBuilder.DropForeignKey(
name: "fk_tokens_applications_application_id",
table: "tokens");
table: "tokens"
);
migrationBuilder.DropTable(
name: "applications");
migrationBuilder.DropTable(name: "applications");
migrationBuilder.DropIndex(
name: "ix_tokens_application_id",
table: "tokens");
migrationBuilder.DropIndex(name: "ix_tokens_application_id", table: "tokens");
migrationBuilder.DropColumn(
name: "application_id",
table: "tokens");
migrationBuilder.DropColumn(name: "application_id", table: "tokens");
migrationBuilder.DropColumn(
name: "hash",
table: "tokens");
migrationBuilder.DropColumn(name: "hash", table: "tokens");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -18,15 +18,14 @@ namespace Foxnouns.Backend.Database.Migrations
table: "users",
type: "boolean",
nullable: false,
defaultValue: false);
defaultValue: false
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "list_hidden",
table: "users");
migrationBuilder.DropColumn(name: "list_hidden", table: "users");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -17,15 +17,14 @@ namespace Foxnouns.Backend.Database.Migrations
name: "password",
table: "users",
type: "text",
nullable: true);
nullable: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "password",
table: "users");
migrationBuilder.DropColumn(name: "password", table: "users");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@ -19,29 +19,37 @@ namespace Foxnouns.Backend.Database.Migrations
name: "temporary_keys",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
id = table
.Column<long>(type: "bigint", nullable: false)
.Annotation(
"Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
),
key = table.Column<string>(type: "text", nullable: false),
value = table.Column<string>(type: "text", nullable: false),
expires = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
expires = table.Column<Instant>(
type: "timestamp with time zone",
nullable: false
),
},
constraints: table =>
{
table.PrimaryKey("pk_temporary_keys", x => x.id);
});
}
);
migrationBuilder.CreateIndex(
name: "ix_temporary_keys_key",
table: "temporary_keys",
column: "key",
unique: true);
unique: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "temporary_keys");
migrationBuilder.DropTable(name: "temporary_keys");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
@ -19,15 +19,14 @@ namespace Foxnouns.Backend.Database.Migrations
table: "users",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()");
defaultValueSql: "now()"
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "last_active",
table: "users");
migrationBuilder.DropColumn(name: "last_active", table: "users");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
@ -19,35 +19,32 @@ namespace Foxnouns.Backend.Database.Migrations
table: "users",
type: "boolean",
nullable: false,
defaultValue: false);
defaultValue: false
);
migrationBuilder.AddColumn<Instant>(
name: "deleted_at",
table: "users",
type: "timestamp with time zone",
nullable: true);
nullable: true
);
migrationBuilder.AddColumn<long>(
name: "deleted_by",
table: "users",
type: "bigint",
nullable: true);
nullable: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deleted",
table: "users");
migrationBuilder.DropColumn(name: "deleted", table: "users");
migrationBuilder.DropColumn(
name: "deleted_at",
table: "users");
migrationBuilder.DropColumn(name: "deleted_at", table: "users");
migrationBuilder.DropColumn(
name: "deleted_by",
table: "users");
migrationBuilder.DropColumn(name: "deleted_by", table: "users");
}
}
}

View file

@ -1,7 +1,7 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Collections.Generic;
using Foxnouns.Backend.Database.Models;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -21,15 +21,14 @@ namespace Foxnouns.Backend.Database.Migrations
table: "users",
type: "jsonb",
nullable: false,
defaultValueSql: "'{}'");
defaultValueSql: "'{}'"
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "custom_preferences",
table: "users");
migrationBuilder.DropColumn(name: "custom_preferences", table: "users");
}
}
}

View file

@ -19,15 +19,14 @@ namespace Foxnouns.Backend.Database.Migrations
table: "users",
type: "jsonb",
nullable: false,
defaultValueSql: "'{}'");
defaultValueSql: "'{}'"
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "settings",
table: "users");
migrationBuilder.DropColumn(name: "settings", table: "users");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
@ -18,38 +18,46 @@ namespace Foxnouns.Backend.Database.Migrations
name: "sid",
table: "users",
type: "text",
nullable: true);
nullable: true
);
migrationBuilder.AddColumn<Instant>(
name: "last_sid_reroll",
table: "users",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now() - '1 hour'::interval");
defaultValueSql: "now() - '1 hour'::interval"
);
migrationBuilder.AddColumn<string>(
name: "sid",
table: "members",
type: "text",
nullable: true);
nullable: true
);
migrationBuilder.CreateIndex(
name: "ix_users_sid",
table: "users",
column: "sid",
unique: true);
unique: true
);
migrationBuilder.CreateIndex(
name: "ix_members_sid",
table: "members",
column: "sid",
unique: true);
unique: true
);
migrationBuilder.Sql(@"create function generate_sid(len int) returns text as $$
migrationBuilder.Sql(
@"create function generate_sid(len int) returns text as $$
select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, len)
$$ language sql volatile;
");
migrationBuilder.Sql(@"create function find_free_user_sid() returns text as $$
"
);
migrationBuilder.Sql(
@"create function find_free_user_sid() returns text as $$
declare new_sid text;
begin
loop
@ -58,8 +66,10 @@ begin
end loop;
end
$$ language plpgsql volatile;
");
migrationBuilder.Sql(@"create function find_free_member_sid() returns text as $$
"
);
migrationBuilder.Sql(
@"create function find_free_member_sid() returns text as $$
declare new_sid text;
begin
loop
@ -67,7 +77,8 @@ begin
if not exists (select 1 from members where sid = new_sid) then return new_sid; end if;
end loop;
end
$$ language plpgsql volatile;");
$$ language plpgsql volatile;"
);
}
/// <inheritdoc />
@ -77,25 +88,15 @@ $$ language plpgsql volatile;");
migrationBuilder.Sql("drop function find_free_user_sid;");
migrationBuilder.Sql("drop function generate_sid;");
migrationBuilder.DropIndex(
name: "ix_users_sid",
table: "users");
migrationBuilder.DropIndex(name: "ix_users_sid", table: "users");
migrationBuilder.DropIndex(
name: "ix_members_sid",
table: "members");
migrationBuilder.DropIndex(name: "ix_members_sid", table: "members");
migrationBuilder.DropColumn(
name: "sid",
table: "users");
migrationBuilder.DropColumn(name: "sid", table: "users");
migrationBuilder.DropColumn(
name: "last_sid_reroll",
table: "users");
migrationBuilder.DropColumn(name: "last_sid_reroll", table: "users");
migrationBuilder.DropColumn(
name: "sid",
table: "members");
migrationBuilder.DropColumn(name: "sid", table: "members");
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
@ -22,7 +22,8 @@ namespace Foxnouns.Backend.Database.Migrations
defaultValueSql: "find_free_user_sid()",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
oldNullable: true
);
migrationBuilder.AlterColumn<string>(
name: "sid",
@ -32,7 +33,8 @@ namespace Foxnouns.Backend.Database.Migrations
defaultValueSql: "find_free_member_sid()",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
oldNullable: true
);
}
/// <inheritdoc />
@ -45,7 +47,8 @@ namespace Foxnouns.Backend.Database.Migrations
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldDefaultValueSql: "find_free_user_sid()");
oldDefaultValueSql: "find_free_user_sid()"
);
migrationBuilder.AlterColumn<string>(
name: "sid",
@ -54,7 +57,8 @@ namespace Foxnouns.Backend.Database.Migrations
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldDefaultValueSql: "find_free_member_sid()");
oldDefaultValueSql: "find_free_member_sid()"
);
}
}
}

View file

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
@ -22,7 +22,7 @@ namespace Foxnouns.Backend.Database.Migrations
user_id = table.Column<long>(type: "bigint", nullable: false),
hash = table.Column<string>(type: "text", nullable: false),
name = table.Column<string>(type: "text", nullable: false),
description = table.Column<string>(type: "text", nullable: true)
description = table.Column<string>(type: "text", nullable: true),
},
constraints: table =>
{
@ -32,17 +32,23 @@ namespace Foxnouns.Backend.Database.Migrations
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateTable(
name: "member_flags",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
id = table
.Column<long>(type: "bigint", nullable: false)
.Annotation(
"Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
),
member_id = table.Column<long>(type: "bigint", nullable: false),
pride_flag_id = table.Column<long>(type: "bigint", nullable: false)
pride_flag_id = table.Column<long>(type: "bigint", nullable: false),
},
constraints: table =>
{
@ -52,23 +58,30 @@ namespace Foxnouns.Backend.Database.Migrations
column: x => x.member_id,
principalTable: "members",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Cascade
);
table.ForeignKey(
name: "fk_member_flags_pride_flags_pride_flag_id",
column: x => x.pride_flag_id,
principalTable: "pride_flags",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateTable(
name: "user_flags",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
id = table
.Column<long>(type: "bigint", nullable: false)
.Annotation(
"Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
),
user_id = table.Column<long>(type: "bigint", nullable: false),
pride_flag_id = table.Column<long>(type: "bigint", nullable: false)
pride_flag_id = table.Column<long>(type: "bigint", nullable: false),
},
constraints: table =>
{
@ -78,52 +91,57 @@ namespace Foxnouns.Backend.Database.Migrations
column: x => x.pride_flag_id,
principalTable: "pride_flags",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Cascade
);
table.ForeignKey(
name: "fk_user_flags_users_user_id",
column: x => x.user_id,
principalTable: "users",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
onDelete: ReferentialAction.Cascade
);
}
);
migrationBuilder.CreateIndex(
name: "ix_member_flags_member_id",
table: "member_flags",
column: "member_id");
column: "member_id"
);
migrationBuilder.CreateIndex(
name: "ix_member_flags_pride_flag_id",
table: "member_flags",
column: "pride_flag_id");
column: "pride_flag_id"
);
migrationBuilder.CreateIndex(
name: "ix_pride_flags_user_id",
table: "pride_flags",
column: "user_id");
column: "user_id"
);
migrationBuilder.CreateIndex(
name: "ix_user_flags_pride_flag_id",
table: "user_flags",
column: "pride_flag_id");
column: "pride_flag_id"
);
migrationBuilder.CreateIndex(
name: "ix_user_flags_user_id",
table: "user_flags",
column: "user_id");
column: "user_id"
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "member_flags");
migrationBuilder.DropTable(name: "member_flags");
migrationBuilder.DropTable(
name: "user_flags");
migrationBuilder.DropTable(name: "user_flags");
migrationBuilder.DropTable(
name: "pride_flags");
migrationBuilder.DropTable(name: "pride_flags");
}
}
}

View file

@ -11,20 +11,30 @@ public class Application : BaseModel
public required string[] Scopes { get; init; }
public required string[] RedirectUris { get; init; }
public static Application Create(ISnowflakeGenerator snowflakeGenerator, string name, string[] scopes,
string[] redirectUrls)
public static Application Create(
ISnowflakeGenerator snowflakeGenerator,
string name,
string[] scopes,
string[] redirectUrls
)
{
var clientId = RandomNumberGenerator.GetHexString(32, true);
var clientSecret = AuthUtils.RandomToken();
if (scopes.Except(AuthUtils.ApplicationScopes).Any())
{
throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes));
throw new ArgumentException(
"Invalid scopes passed to Application.Create",
nameof(scopes)
);
}
if (redirectUrls.Any(s => !AuthUtils.ValidateRedirectUri(s)))
{
throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls));
throw new ArgumentException(
"Invalid redirect URLs passed to Application.Create",
nameof(redirectUrls)
);
}
return new Application
@ -34,7 +44,7 @@ public class Application : BaseModel
ClientSecret = clientSecret,
Name = name,
Scopes = scopes,
RedirectUris = redirectUrls
RedirectUris = redirectUrls,
};
}
}
}

View file

@ -20,4 +20,4 @@ public enum AuthType
Tumblr,
Fediverse,
Email,
}
}

View file

@ -11,5 +11,5 @@ public class FediverseApplication : BaseModel
public enum FediverseInstanceType
{
MastodonApi,
MisskeyApi
}
MisskeyApi,
}

View file

@ -15,4 +15,4 @@ public class FieldEntry
public class Pronoun : FieldEntry
{
public string? DisplayText { get; set; }
}
}

View file

@ -18,4 +18,4 @@ public class Member : BaseModel
public Snowflake UserId { get; init; }
public User User { get; init; } = null!;
}
}

View file

@ -22,4 +22,4 @@ public class MemberFlag
public required Snowflake MemberId { get; init; }
public required Snowflake PrideFlagId { get; init; }
public PrideFlag PrideFlag { get; init; } = null!;
}
}

View file

@ -8,4 +8,4 @@ public class TemporaryKey
public required string Key { get; init; }
public required string Value { get; set; }
public Instant Expires { get; init; }
}
}

View file

@ -14,4 +14,4 @@ public class Token : BaseModel
public Snowflake ApplicationId { get; set; }
public Application Application { get; set; } = null!;
}
}

View file

@ -37,7 +37,9 @@ public class User : BaseModel
public bool Deleted { get; set; }
public Instant? DeletedAt { get; set; }
public Snowflake? DeletedBy { get; set; }
[NotMapped] public bool? SelfDelete => Deleted ? DeletedBy != null : null;
[NotMapped]
public bool? SelfDelete => Deleted ? DeletedBy != null : null;
public class CustomPreference
{
@ -69,4 +71,4 @@ public enum PreferenceSize
public class UserSettings
{
public bool? DarkMode { get; set; } = null;
}
}

View file

@ -41,19 +41,26 @@ public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
public short Increment => (short)(Value & 0xFFF);
public static bool operator <(Snowflake arg1, Snowflake arg2) => arg1.Value < arg2.Value;
public static bool operator >(Snowflake arg1, Snowflake arg2) => arg1.Value > arg2.Value;
public static bool operator ==(Snowflake arg1, Snowflake arg2) => arg1.Value == arg2.Value;
public static bool operator !=(Snowflake arg1, Snowflake arg2) => arg1.Value != arg2.Value;
public static implicit operator ulong(Snowflake s) => s.Value;
public static implicit operator long(Snowflake s) => (long)s.Value;
public static implicit operator Snowflake(ulong n) => new(n);
public static implicit operator Snowflake(long n) => new((ulong)n);
public static bool TryParse(string input, [NotNullWhen(true)] out Snowflake? snowflake)
{
snowflake = null;
if (!ulong.TryParse(input, out var res)) return false;
if (!ulong.TryParse(input, out var res))
return false;
snowflake = new Snowflake(res);
return true;
}
@ -66,27 +73,37 @@ public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
}
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
/// <summary>
/// An Entity Framework ValueConverter for Snowflakes to longs.
/// </summary>
// ReSharper disable once ClassNeverInstantiated.Global
public class ValueConverter() : ValueConverter<Snowflake, long>(
convertToProviderExpression: x => x,
convertFromProviderExpression: x => x
);
public class ValueConverter()
: ValueConverter<Snowflake, long>(
convertToProviderExpression: x => x,
convertFromProviderExpression: x => x
);
private class JsonConverter : JsonConverter<Snowflake>
{
public override void WriteJson(JsonWriter writer, Snowflake value, JsonSerializer serializer)
public override void WriteJson(
JsonWriter writer,
Snowflake value,
JsonSerializer serializer
)
{
writer.WriteValue(value.Value.ToString());
}
public override Snowflake ReadJson(JsonReader reader, Type objectType, Snowflake existingValue,
public override Snowflake ReadJson(
JsonReader reader,
Type objectType,
Snowflake existingValue,
bool hasExistingValue,
JsonSerializer serializer)
JsonSerializer serializer
)
{
return ulong.Parse((string)reader.Value!);
}
@ -97,12 +114,18 @@ public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
sourceType == typeof(string);
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) =>
destinationType == typeof(Snowflake);
public override bool CanConvertTo(
ITypeDescriptorContext? context,
[NotNullWhen(true)] Type? destinationType
) => destinationType == typeof(Snowflake);
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
public override object? ConvertFrom(
ITypeDescriptorContext? context,
CultureInfo? culture,
object value
)
{
return TryParse((string)value, out var snowflake) ? snowflake : null;
}
}
}
}

View file

@ -32,14 +32,20 @@ public class SnowflakeGenerator : ISnowflakeGenerator
var threadId = Environment.CurrentManagedThreadId % 32;
var timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch;
return (timestamp << 22) | (uint)(_processId << 17) | (uint)(threadId << 12) | (increment % 4096);
return (timestamp << 22)
| (uint)(_processId << 17)
| (uint)(threadId << 12)
| (increment % 4096);
}
}
public static class SnowflakeGeneratorServiceExtensions
{
public static IServiceCollection AddSnowflakeGenerator(this IServiceCollection services, int? processId = null)
public static IServiceCollection AddSnowflakeGenerator(
this IServiceCollection services,
int? processId = null
)
{
return services.AddSingleton<ISnowflakeGenerator>(new SnowflakeGenerator(processId));
}
}
}