init
This commit is contained in:
commit
f4c0a40259
27 changed files with 2188 additions and 0 deletions
24
Foxnouns.Backend/Config.cs
Normal file
24
Foxnouns.Backend/Config.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using Serilog.Events;
|
||||
|
||||
namespace Foxnouns.Backend;
|
||||
|
||||
public class Config
|
||||
{
|
||||
public string Host { get; set; } = "localhost";
|
||||
public int Port { get; set; } = 3000;
|
||||
public string BaseUrl { get; set; } = null!;
|
||||
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
|
||||
public string? SeqLogUrl { get; set; }
|
||||
public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug;
|
||||
|
||||
public DatabaseConfig Database { get; set; } = new();
|
||||
|
||||
public class DatabaseConfig
|
||||
{
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public int? Timeout { get; set; }
|
||||
public int? MaxPoolSize { get; set; }
|
||||
}
|
||||
}
|
6
Foxnouns.Backend/Database/BaseModel.cs
Normal file
6
Foxnouns.Backend/Database/BaseModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public abstract class BaseModel
|
||||
{
|
||||
public required Snowflake Id { get; init; } = SnowflakeGenerator.Instance.GenerateSnowflake();
|
||||
}
|
73
Foxnouns.Backend/Database/DatabaseContext.cs
Normal file
73
Foxnouns.Backend/Database/DatabaseContext.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Npgsql;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public class DatabaseContext : DbContext
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Member> Members { get; set; }
|
||||
public DbSet<AuthMethod> AuthMethods { get; set; }
|
||||
public DbSet<FediverseApplication> FediverseApplications { get; set; }
|
||||
public DbSet<Token> Tokens { get; set; }
|
||||
|
||||
public DatabaseContext(Config config)
|
||||
{
|
||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||
{
|
||||
Timeout = config.Database.Timeout ?? 5,
|
||||
MaxPoolSize = config.Database.MaxPoolSize ?? 50,
|
||||
}.ConnectionString;
|
||||
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||
dataSourceBuilder.UseNodaTime();
|
||||
_dataSource = dataSourceBuilder.Build();
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder
|
||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||
.UseSnakeCaseNamingConvention();
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
// Snowflakes are stored as longs
|
||||
configurationBuilder.Properties<Snowflake>().HaveConversion<Snowflake.ValueConverter>();
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.Username).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => new { m.UserId, m.Name }).IsUnique();
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.OwnsOne(u => u.Fields, f => f.ToJson())
|
||||
.OwnsOne(u => u.Names, n => n.ToJson())
|
||||
.OwnsOne(u => u.Pronouns, p => p.ToJson());
|
||||
|
||||
modelBuilder.Entity<Member>()
|
||||
.OwnsOne(m => m.Fields, f => f.ToJson())
|
||||
.OwnsOne(m => m.Names, n => n.ToJson())
|
||||
.OwnsOne(m => m.Pronouns, p => p.ToJson());
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return new DatabaseContext(config);
|
||||
}
|
||||
}
|
8
Foxnouns.Backend/Database/ISnowflakeGenerator.cs
Normal file
8
Foxnouns.Backend/Database/ISnowflakeGenerator.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public interface ISnowflakeGenerator
|
||||
{
|
||||
Snowflake GenerateSnowflake(Instant? time = null);
|
||||
}
|
412
Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs
generated
Normal file
412
Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs
generated
Normal file
|
@ -0,0 +1,412 @@
|
|||
// <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("20240527132444_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <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.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.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
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("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<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
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.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
171
Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs
Normal file
171
Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs
Normal file
|
@ -0,0 +1,171 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "fediverse_applications",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_fediverse_applications", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
username = table.Column<string>(type: "text", nullable: false),
|
||||
display_name = table.Column<string>(type: "text", nullable: true),
|
||||
bio = table.Column<string>(type: "text", nullable: true),
|
||||
member_title = table.Column<string>(type: "text", nullable: true),
|
||||
avatar = table.Column<string>(type: "text", nullable: true),
|
||||
links = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_users", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "auth_methods",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
auth_type = table.Column<int>(type: "integer", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_auth_methods", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_auth_methods_fediverse_applications_fediverse_application_id",
|
||||
column: x => x.fediverse_application_id,
|
||||
principalTable: "fediverse_applications",
|
||||
principalColumn: "id");
|
||||
table.ForeignKey(
|
||||
name: "fk_auth_methods_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "members",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: false),
|
||||
display_name = table.Column<string>(type: "text", nullable: true),
|
||||
bio = table.Column<string>(type: "text", nullable: true),
|
||||
avatar = table.Column<string>(type: "text", nullable: true),
|
||||
links = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
unlisted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_members", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_members_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
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),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tokens", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_tokens_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_fediverse_application_id",
|
||||
table: "auth_methods",
|
||||
column: "fediverse_application_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_user_id",
|
||||
table: "auth_methods",
|
||||
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.CreateIndex(
|
||||
name: "ix_tokens_user_id",
|
||||
table: "tokens",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_username",
|
||||
table: "users",
|
||||
column: "username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "auth_methods");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "members");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "fediverse_applications");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
partial class DatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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.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.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
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("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<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
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.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
23
Foxnouns.Backend/Database/Models/AuthMethod.cs
Normal file
23
Foxnouns.Backend/Database/Models/AuthMethod.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class AuthMethod : BaseModel
|
||||
{
|
||||
public required AuthType AuthType { get; init; }
|
||||
public required string RemoteId { get; init; }
|
||||
public string? RemoteUsername { get; set; }
|
||||
|
||||
public Snowflake UserId { get; init; }
|
||||
public User User { get; init; } = null!;
|
||||
|
||||
public Snowflake? FediverseApplicationId { get; init; }
|
||||
public FediverseApplication? FediverseApplication { get; init; }
|
||||
}
|
||||
|
||||
public enum AuthType
|
||||
{
|
||||
Discord,
|
||||
Google,
|
||||
Tumblr,
|
||||
Fediverse,
|
||||
Email,
|
||||
}
|
15
Foxnouns.Backend/Database/Models/FediverseApplication.cs
Normal file
15
Foxnouns.Backend/Database/Models/FediverseApplication.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class FediverseApplication : BaseModel
|
||||
{
|
||||
public required string Domain { get; init; }
|
||||
public required string ClientId { get; set; }
|
||||
public required string ClientSecret { get; set; }
|
||||
public required FediverseInstanceType InstanceType { get; set; }
|
||||
}
|
||||
|
||||
public enum FediverseInstanceType
|
||||
{
|
||||
MastodonApi,
|
||||
MisskeyApi
|
||||
}
|
18
Foxnouns.Backend/Database/Models/Field.cs
Normal file
18
Foxnouns.Backend/Database/Models/Field.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Field
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public FieldEntry[] Entries { get; set; } = [];
|
||||
}
|
||||
|
||||
public class FieldEntry
|
||||
{
|
||||
public required string Value { get; set; }
|
||||
public required string Status { get; set; }
|
||||
}
|
||||
|
||||
public class Pronoun : FieldEntry
|
||||
{
|
||||
public string? DisplayText { get; set; }
|
||||
}
|
18
Foxnouns.Backend/Database/Models/Member.cs
Normal file
18
Foxnouns.Backend/Database/Models/Member.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Member : BaseModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public string[] Links { get; set; } = [];
|
||||
public bool Unlisted { get; set; }
|
||||
|
||||
public List<FieldEntry> Names { get; set; } = [];
|
||||
public List<Pronoun> Pronouns { get; set; } = [];
|
||||
public List<Field> Fields { get; set; } = [];
|
||||
|
||||
public Snowflake UserId { get; init; }
|
||||
public User User { get; init; } = null!;
|
||||
}
|
15
Foxnouns.Backend/Database/Models/Token.cs
Normal file
15
Foxnouns.Backend/Database/Models/Token.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Token : BaseModel
|
||||
{
|
||||
public required Instant ExpiresAt { get; init; }
|
||||
public required string[] Scopes { get; init; }
|
||||
public bool ManuallyExpired { get; set; }
|
||||
|
||||
public bool IsExpired => ManuallyExpired || ExpiresAt < SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
public Snowflake UserId { get; init; }
|
||||
public User User { get; init; } = null!;
|
||||
}
|
27
Foxnouns.Backend/Database/Models/User.cs
Normal file
27
Foxnouns.Backend/Database/Models/User.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class User : BaseModel
|
||||
{
|
||||
public required string Username { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? MemberTitle { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public string[] Links { get; set; } = [];
|
||||
|
||||
public List<FieldEntry> Names { get; set; } = [];
|
||||
public List<Pronoun> Pronouns { get; set; } = [];
|
||||
public List<Field> Fields { get; set; } = [];
|
||||
|
||||
public UserRole Role { get; set; } = UserRole.User;
|
||||
|
||||
public List<Member> Members { get; } = [];
|
||||
public List<AuthMethod> AuthMethods { get; } = [];
|
||||
}
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
User,
|
||||
Moderator,
|
||||
Admin,
|
||||
}
|
57
Foxnouns.Backend/Database/Snowflake.cs
Normal file
57
Foxnouns.Backend/Database/Snowflake.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public readonly struct Snowflake(ulong value)
|
||||
{
|
||||
public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC
|
||||
public readonly ulong Value = value;
|
||||
|
||||
/// <summary>
|
||||
/// The time this snowflake was created.
|
||||
/// </summary>
|
||||
public Instant Time => Instant.FromUnixTimeMilliseconds(Timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp embedded in this snowflake, in milliseconds.
|
||||
/// </summary>
|
||||
public long Timestamp => (long)((Value >> 22) + Epoch);
|
||||
|
||||
/// <summary>
|
||||
/// The process ID embedded in this snowflake.
|
||||
/// </summary>
|
||||
public byte ProcessId => (byte)((Value & 0x3E0000) >> 17);
|
||||
|
||||
/// <summary>
|
||||
/// The thread ID embedded in this snowflake.
|
||||
/// </summary>
|
||||
public byte ThreadId => (byte)((Value & 0x1F000) >> 12);
|
||||
|
||||
/// <summary>
|
||||
/// The increment embedded in this snowflake.
|
||||
/// </summary>
|
||||
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 override bool Equals(object? obj) => obj is Snowflake other && Value == other.Value;
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
|
||||
/// <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
|
||||
);
|
||||
}
|
45
Foxnouns.Backend/Database/SnowflakeGenerator.cs
Normal file
45
Foxnouns.Backend/Database/SnowflakeGenerator.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public class SnowflakeGenerator : ISnowflakeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance of SnowflakeGenerator. Where possible, use an injected ISnowflakeGenerator instead
|
||||
/// (see IServiceCollection.AddSnowflakeGenerator).
|
||||
/// </summary>
|
||||
public static SnowflakeGenerator Instance { get; } = new();
|
||||
|
||||
private readonly byte _processId;
|
||||
private long _increment;
|
||||
|
||||
public SnowflakeGenerator(int? processId = null)
|
||||
{
|
||||
processId ??= Environment.ProcessId;
|
||||
_processId = (byte)(processId % 32);
|
||||
_increment = Random.Shared.NextInt64() % 4096;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a snowflake ID for the given timestamp.
|
||||
/// </summary>
|
||||
/// <param name="time">An optional timestamp. If null, use the current timestamp.</param>
|
||||
/// <returns>A new snowflake ID.</returns>
|
||||
public Snowflake GenerateSnowflake(Instant? time = null)
|
||||
{
|
||||
time ??= SystemClock.Instance.GetCurrentInstant();
|
||||
var increment = Interlocked.Increment(ref _increment);
|
||||
var threadId = Environment.CurrentManagedThreadId % 32;
|
||||
var timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch;
|
||||
|
||||
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)
|
||||
{
|
||||
return services.AddSingleton<ISnowflakeGenerator>(new SnowflakeGenerator(processId));
|
||||
}
|
||||
}
|
60
Foxnouns.Backend/Extensions/WebApplicationExtensions.cs
Normal file
60
Foxnouns.Backend/Extensions/WebApplicationExtensions.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
|
||||
namespace Foxnouns.Backend.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls.
|
||||
/// </summary>
|
||||
public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder, LogEventLevel level)
|
||||
{
|
||||
var config = builder.Configuration.Get<Config>() ?? new();
|
||||
|
||||
var logCfg = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.MinimumLevel.Is(level)
|
||||
// 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.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Code);
|
||||
|
||||
if (config.SeqLogUrl != null)
|
||||
{
|
||||
logCfg.WriteTo.Seq(config.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);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static Config AddConfiguration(this WebApplicationBuilder builder)
|
||||
{
|
||||
|
||||
builder.Configuration.Sources.Clear();
|
||||
builder.Configuration.AddConfiguration();
|
||||
|
||||
var config = builder.Configuration.Get<Config>() ?? new();
|
||||
builder.Services.AddSingleton(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder)
|
||||
{
|
||||
var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini";
|
||||
|
||||
return builder
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddIniFile(file, optional: false, reloadOnChange: true)
|
||||
.AddEnvironmentVariables();
|
||||
}
|
||||
}
|
29
Foxnouns.Backend/Foxnouns.Backend.csproj
Normal file
29
Foxnouns.Backend/Foxnouns.Backend.csproj
Normal file
|
@ -0,0 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.11" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
2
Foxnouns.Backend/GlobalUsing.cs
Normal file
2
Foxnouns.Backend/GlobalUsing.cs
Normal file
|
@ -0,0 +1,2 @@
|
|||
global using ILogger = Serilog.ILogger;
|
||||
global using Log = Serilog.Log;
|
40
Foxnouns.Backend/Program.cs
Normal file
40
Foxnouns.Backend/Program.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Serilog;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var config = builder.AddConfiguration();
|
||||
|
||||
builder.AddSerilog(config.LogEventLevel);
|
||||
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options =>
|
||||
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddDbContext<DatabaseContext>()
|
||||
.AddSnowflakeGenerator()
|
||||
.AddEndpointsApiExplorer()
|
||||
.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseRouting();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseCors();
|
||||
app.MapControllers();
|
||||
|
||||
app.Urls.Clear();
|
||||
app.Urls.Add(config.Address);
|
||||
|
||||
app.Run();
|
||||
|
||||
Log.CloseAndFlush();
|
20
Foxnouns.Backend/config.ini
Normal file
20
Foxnouns.Backend/config.ini
Normal file
|
@ -0,0 +1,20 @@
|
|||
; The host the server will listen on
|
||||
Host = localhost
|
||||
; The port the server will listen on
|
||||
Port = 5000
|
||||
; The base *external* URL
|
||||
BaseUrl = https://pronouns.localhost
|
||||
|
||||
; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal
|
||||
LogEventLevel = Verbose
|
||||
|
||||
SeqLogUrl = http://localhost:5341
|
||||
|
||||
[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.
|
||||
MaxPoolSize = 500
|
Loading…
Add table
Add a link
Reference in a new issue