diff --git a/404.html b/404.html index 9a4b0db..8de6e0c 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Other/login-template.js b/Other/login-template.js index 8792883..d39825a 100644 --- a/Other/login-template.js +++ b/Other/login-template.js @@ -29,7 +29,7 @@ class LoginSignup extends HTMLElement { this.shadowThis.validateUsername(this)"> -
@@ -85,7 +85,7 @@ class LoginSignup extends HTMLElement {
- +
@@ -97,7 +97,7 @@ class LoginSignup extends HTMLElement { display: flex; flex-direction: column; max-width: 400px; - height: 230px; + height: 236px; transition: .2s height; } @@ -145,7 +145,7 @@ class LoginSignup extends HTMLElement { } #login[currentpage="signin"] > #loginSignin, #login[currentpage="signup"] > #loginSignup, - #login[currentpage="loginerror"] > #loginError, #login[currentpage="code"] > #loginCode { + #login[currentpage="error"] > #loginError, #login[currentpage="code"] > #loginCode { display: flex !important; } @@ -184,7 +184,7 @@ class LoginSignup extends HTMLElement { async signup(username, email) { try { - const response = await fetch(serverBaseAddress + "/Signup", { + const response = await fetch(serverBaseAddress + "/auth/signup", { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Username: username, Email: email }) // Convert to JSON format @@ -206,7 +206,7 @@ class LoginSignup extends HTMLElement { //Impossible to log in without code, GUID can be retrieved though async signin(username, email) { try { - const signinResponse = await fetch(serverBaseAddress + "/Signin", { + const signinResponse = await fetch(serverBaseAddress + "/auth/signin", { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Username: username, Email: email }) @@ -249,7 +249,7 @@ class LoginSignup extends HTMLElement { confirmFail(err) { this.errorMessage.textContent = err - this.login.setAttribute("currentPage", "loginerror") + this.login.setAttribute("currentpage", "error") } validateLoginSignup() { diff --git a/Other/purgatory-entry-template.js b/Other/purgatory-entry-template.js index f161de9..7610518 100644 --- a/Other/purgatory-entry-template.js +++ b/Other/purgatory-entry-template.js @@ -104,6 +104,7 @@ class PurgatoryEntry extends HTMLElement { ` this.shadowRoot.append(style) defineAndInject(this, this.shadowRoot) + this.setAttribute("tabindex", "0") if (this.getAttribute("tooltip")) { @@ -114,7 +115,7 @@ class PurgatoryEntry extends HTMLElement { this.approves.textContent = this.getAttribute("approves") this.vetoes.textContent = this.getAttribute("vetoes") - if (this.getAttribute('new')) { + if (this.getAttribute("new")) { setTimeout(() => { this.classList.add("entry-new") this.setAttribute("style", ` @@ -131,8 +132,12 @@ class PurgatoryEntry extends HTMLElement { this.classList.remove("entry-new") }, 1600) } - this.onclick = () => - window.location.href = './purgatory-poem?guid=' + this.getAttribute('guid') + this.onclick = function() { + const guid = this.getAttribute("guid") + if (guid) { + window.location.href = "./purgatory-poem?guid=" + guid + } + } } } diff --git a/Other/server.js b/Other/server.js index 1840eaa..d2d0aff 100644 --- a/Other/server.js +++ b/Other/server.js @@ -1,5 +1,6 @@ const serverBaseAddress = localStorage.server || "https://server.poemanthology.org/subliminal" const editServerAddress = - localStorage.editServer || - "https://server.poemanthology.org/subliminaledit" + localStorage.editServer || "https://server.poemanthology.org/subliminal/edit" +const soundsServerAddress = + localStorage.soundsServer || "https://server.poemanthology.org/subliminal/sounds" diff --git a/SubliminalServer/DataModel/Api/LoginDetails.cs b/SubliminalServer/ApiModel/LoginDetails.cs similarity index 58% rename from SubliminalServer/DataModel/Api/LoginDetails.cs rename to SubliminalServer/ApiModel/LoginDetails.cs index f0ba00d..cb170f1 100644 --- a/SubliminalServer/DataModel/Api/LoginDetails.cs +++ b/SubliminalServer/ApiModel/LoginDetails.cs @@ -1,3 +1,3 @@ -namespace SubliminalServer.DataModel.Api; +namespace SubliminalServer.ApiModel; public record LoginDetails(string Username, string Email); \ No newline at end of file diff --git a/SubliminalServer/DataModel/Api/UploadableEntry.cs b/SubliminalServer/ApiModel/UploadableEntry.cs similarity index 80% rename from SubliminalServer/DataModel/Api/UploadableEntry.cs rename to SubliminalServer/ApiModel/UploadableEntry.cs index 2e2233e..8e105bb 100644 --- a/SubliminalServer/DataModel/Api/UploadableEntry.cs +++ b/SubliminalServer/ApiModel/UploadableEntry.cs @@ -1,6 +1,6 @@ using SubliminalServer.DataModel.Purgatory; -namespace SubliminalServer.DataModel.Api; +namespace SubliminalServer.ApiModel; public class UploadableEntry { @@ -8,8 +8,8 @@ public class UploadableEntry public bool ContentWarning { get; set; } public PageStyle PageStyle { get; set; } public string? Background { get; set; } - - public IReadOnlyList PoemTags { get; set; } + + public List PoemTags { get; set; } public string PoemName { get; set; } public string PoemContent { get; set; } diff --git a/SubliminalServer/DataModel/Api/UploadableProfile.cs b/SubliminalServer/ApiModel/UploadableProfile.cs similarity index 81% rename from SubliminalServer/DataModel/Api/UploadableProfile.cs rename to SubliminalServer/ApiModel/UploadableProfile.cs index ddf6897..bbed92f 100644 --- a/SubliminalServer/DataModel/Api/UploadableProfile.cs +++ b/SubliminalServer/ApiModel/UploadableProfile.cs @@ -1,6 +1,6 @@ using SubliminalServer.DataModel.Account; -namespace SubliminalServer.DataModel.Api; +namespace SubliminalServer.ApiModel; public class UploadableProfile { @@ -27,17 +27,17 @@ public class UploadableProfile public UploadableProfile(AccountData account) { - AccountKey = account.AccountKey; + AccountKey = account.Id; Username = account.Username; PenName = account.PenName; Biography = account.Biography; Location = account.Location; Role = account.Role; AvatarUrl = account.AvatarUrl; - Badges = account.Badges.Select(badge => badge.BadgeKey).ToList(); - PinnedPoems = account.PinnedPoems.Select(entry => entry.EntryKey).ToList(); - Poems = account.Poems.Select(entry => entry.EntryKey).ToList(); - Following = account.Following.Select(profile => profile.AccountKey).ToList(); + Badges = account.Badges.Select(badge => badge.Id).ToList(); + PinnedPoems = account.PinnedPoems.Select(entry => entry.Id).ToList(); + Poems = account.Poems.Select(entry => entry.Id).ToList(); + Following = account.Following.Select(profile => profile.Id).ToList(); JoinDate = account.JoinDate; } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Api/UploadableRating.cs b/SubliminalServer/ApiModel/UploadableRating.cs similarity index 55% rename from SubliminalServer/DataModel/Api/UploadableRating.cs rename to SubliminalServer/ApiModel/UploadableRating.cs index 85b0ad2..2f92dcc 100644 --- a/SubliminalServer/DataModel/Api/UploadableRating.cs +++ b/SubliminalServer/ApiModel/UploadableRating.cs @@ -1,6 +1,5 @@ -using SubliminalServer.DataModel.Purgatory; using SubliminalServer.DataModel.Rating; -namespace SubliminalServer.DataModel.Api; +namespace SubliminalServer.ApiModel; public record UploadableRating(int EntryKey, RatingType Rating); \ No newline at end of file diff --git a/SubliminalServer/DataModel/Api/UploadableReport.cs b/SubliminalServer/ApiModel/UploadableReport.cs similarity index 87% rename from SubliminalServer/DataModel/Api/UploadableReport.cs rename to SubliminalServer/ApiModel/UploadableReport.cs index 2ba6488..ba0bfc5 100644 --- a/SubliminalServer/DataModel/Api/UploadableReport.cs +++ b/SubliminalServer/ApiModel/UploadableReport.cs @@ -1,6 +1,6 @@ using SubliminalServer.DataModel.Report; -namespace SubliminalServer.DataModel.Api; +namespace SubliminalServer.ApiModel; public class UploadableReport { diff --git a/SubliminalServer/Config.cs b/SubliminalServer/Config.cs deleted file mode 100644 index 0718598..0000000 --- a/SubliminalServer/Config.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SubliminalServer; - -public enum Config -{ - Certificate, - Key, - Port, - UseHttps, -} \ No newline at end of file diff --git a/SubliminalServer/DataModel/Account/AccountAddress.cs b/SubliminalServer/DataModel/Account/AccountAddress.cs index 49057b2..c94172d 100644 --- a/SubliminalServer/DataModel/Account/AccountAddress.cs +++ b/SubliminalServer/DataModel/Account/AccountAddress.cs @@ -4,14 +4,15 @@ namespace SubliminalServer.DataModel.Account; -[PrimaryKey(nameof(AddressKey))] +[PrimaryKey(nameof(Id))] public class AccountAddress { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int AddressKey { get; set; } + public int Id { get; set; } public string IpAddress { get; set; } + public DateTime LastUsed { get; set; } // Foreign key AccountData public int AccountKey { get; set; } diff --git a/SubliminalServer/DataModel/Account/AccountBadge.cs b/SubliminalServer/DataModel/Account/AccountBadge.cs index 7135a06..15f9696 100644 --- a/SubliminalServer/DataModel/Account/AccountBadge.cs +++ b/SubliminalServer/DataModel/Account/AccountBadge.cs @@ -4,13 +4,13 @@ namespace SubliminalServer.DataModel.Account; -[PrimaryKey(nameof(BadgeKey))] +[PrimaryKey(nameof(Id))] public class AccountBadge { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int BadgeKey { get; set; } + public int Id { get; set; } public BadgeType Badge { get; set; } public DateTime DateAwarded { get; set; } diff --git a/SubliminalServer/DataModel/Account/AccountData.cs b/SubliminalServer/DataModel/Account/AccountData.cs index c16024b..b22caf0 100644 --- a/SubliminalServer/DataModel/Account/AccountData.cs +++ b/SubliminalServer/DataModel/Account/AccountData.cs @@ -8,7 +8,7 @@ namespace SubliminalServer.DataModel.Account; /// /// Constructor for account data, contains the private info for an account, and inherits from account profile /// -[PrimaryKey(nameof(AccountKey))] +[PrimaryKey(nameof(Id))] public class AccountData : AccountProfile { // Unique diff --git a/SubliminalServer/DataModel/Account/AccountProfile.cs b/SubliminalServer/DataModel/Account/AccountProfile.cs index 89377d2..67c69d4 100644 --- a/SubliminalServer/DataModel/Account/AccountProfile.cs +++ b/SubliminalServer/DataModel/Account/AccountProfile.cs @@ -18,7 +18,7 @@ public class AccountProfile // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int AccountKey { get; set; } + public int Id { get; set; } // Unique [Required] diff --git a/SubliminalServer/DataModel/Configurations/AccountAddressConfiguration.cs b/SubliminalServer/DataModel/Configurations/AccountAddressConfiguration.cs index 31e65bf..f061dc5 100644 --- a/SubliminalServer/DataModel/Configurations/AccountAddressConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/AccountAddressConfiguration.cs @@ -8,13 +8,13 @@ public class AccountAddressConfiguration : IEntityTypeConfiguration builder) { - builder.HasKey(address => address.AddressKey); - builder.Property(address => address.AddressKey) + builder.HasKey(address => address.Id); + builder.Property(address => address.Id) .ValueGeneratedOnAdd(); // One to many (AccountData) builder.HasOne(address => address.Account) .WithMany(account => account.KnownIPs) - .HasForeignKey(address => address.AddressKey); + .HasForeignKey(address => address.Id); } } diff --git a/SubliminalServer/DataModel/Configurations/AccountBadgeConfiguration.cs b/SubliminalServer/DataModel/Configurations/AccountBadgeConfiguration.cs index 1c200e0..07a0b6f 100644 --- a/SubliminalServer/DataModel/Configurations/AccountBadgeConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/AccountBadgeConfiguration.cs @@ -8,7 +8,7 @@ public class AccountBadgeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.HasKey(badge => badge.BadgeKey); + builder.HasKey(badge => badge.Id); // One to many (AccountData) builder.HasOne(badge => badge.Account) diff --git a/SubliminalServer/DataModel/Configurations/AccountDataConfiguration.cs b/SubliminalServer/DataModel/Configurations/AccountDataConfiguration.cs index 9360362..579f347 100644 --- a/SubliminalServer/DataModel/Configurations/AccountDataConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/AccountDataConfiguration.cs @@ -10,7 +10,7 @@ public class AccountDataConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { // Define the primary key - builder.HasKey(account => account.AccountKey); + builder.HasKey(account => account.Id); // Unique email builder.HasIndex(account => account.Username).IsUnique(); @@ -19,12 +19,12 @@ public void Configure(EntityTypeBuilder builder) // One to many Poems (PurgatoryEntry) builder.HasMany(account => account.Poems) .WithOne(entry => entry.Author) - .HasForeignKey(entry => entry.AuthorKey); + .HasForeignKey(entry => entry.AuthorId); // One to many Drafts (PurgatoryEntry) builder.HasMany(account => account.Drafts) .WithOne(draft => draft.Author) - .HasForeignKey(draft => draft.AuthorKey); + .HasForeignKey(draft => draft.AuthorId); // One to many Badges (AccountBadge) builder.HasMany(account => account.Badges) diff --git a/SubliminalServer/DataModel/Configurations/PurgatoryAnnotationRating.cs b/SubliminalServer/DataModel/Configurations/PurgatoryAnnotationRating.cs index 11b9209..8c3cdda 100644 --- a/SubliminalServer/DataModel/Configurations/PurgatoryAnnotationRating.cs +++ b/SubliminalServer/DataModel/Configurations/PurgatoryAnnotationRating.cs @@ -8,6 +8,6 @@ public class PurgatoryAnnotationRatingConfiguration : IEntityTypeConfiguration

builder) { - builder.HasKey(rating => rating.RatingKey); + builder.HasKey(rating => rating.Id); } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Configurations/PurgatoryDraftConfiguration.cs b/SubliminalServer/DataModel/Configurations/PurgatoryDraftConfiguration.cs index 7c8ad89..7f75622 100644 --- a/SubliminalServer/DataModel/Configurations/PurgatoryDraftConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/PurgatoryDraftConfiguration.cs @@ -9,21 +9,21 @@ public class PurgatoryDraftConfiguration : IEntityTypeConfiguration builder) { // Define the primary key - builder.HasKey(entry => entry.DraftKey); + builder.HasKey(entry => entry.Id); // Many to one (AccountData) builder.HasOne(draft => draft.Author) .WithMany(account => account.Drafts) // Use the correct navigation property - .HasForeignKey(draft => draft.AuthorKey); + .HasForeignKey(draft => draft.AuthorId); // One to many PurgatoryEntry (Amends), PurgatoryEntry (AmendedBy) builder.HasOne(draft => draft.Amends) .WithMany() - .HasForeignKey(draft => draft.AmendsKey); + .HasForeignKey(draft => draft.AmendsId); // One to many PurgatoryEntry (Edits), PurgatoryEntry (AmendedBy) builder.HasOne(draft => draft.Edits) .WithMany() - .HasForeignKey(draft => draft.EditsKey); + .HasForeignKey(draft => draft.EditsId); } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Configurations/PurgatoryEntryConfiguration.cs b/SubliminalServer/DataModel/Configurations/PurgatoryEntryConfiguration.cs index 211bb57..716c214 100644 --- a/SubliminalServer/DataModel/Configurations/PurgatoryEntryConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/PurgatoryEntryConfiguration.cs @@ -9,25 +9,25 @@ public class PurgatoryEntryConfiguration : IEntityTypeConfiguration builder) { // Define the primary key - builder.HasKey(entry => entry.EntryKey); + builder.HasKey(entry => entry.Id); builder.HasOne(entry => entry.Author) .WithMany(account => account.Poems) // Use the correct navigation property - .HasForeignKey(entry => entry.AuthorKey); + .HasForeignKey(entry => entry.AuthorId); // One to many PurgatoryEntry (Amends), PurgatoryEntry (AmendedBy) builder.HasOne(entry => entry.Amends) .WithMany() - .HasForeignKey(entry => entry.AmendsKey); + .HasForeignKey(entry => entry.AmendsId); // One to many PurgatoryEntry (Edits), PurgatoryEntry (AmendedBy) builder.HasOne(entry => entry.Edits) .WithMany() - .HasForeignKey(entry => entry.EditsKey); + .HasForeignKey(entry => entry.EditsId); // Many to one (PoemTags) builder.HasMany(entry => entry.Tags) .WithOne(tag => tag.PurgatoryEntry) - .HasForeignKey(tag => tag.EntryKey); + .HasForeignKey(tag => tag.EntryId); } } diff --git a/SubliminalServer/DataModel/Configurations/PurgatoryRatingConfiguration.cs b/SubliminalServer/DataModel/Configurations/PurgatoryRatingConfiguration.cs index 220cab6..5725112 100644 --- a/SubliminalServer/DataModel/Configurations/PurgatoryRatingConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/PurgatoryRatingConfiguration.cs @@ -8,6 +8,6 @@ public class PurgatoryRatingConfiguration : IEntityTypeConfiguration builder) { - builder.HasKey(rating => rating.RatingKey); + builder.HasKey(rating => rating.Id); } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Configurations/PurgatoryTagConfiguration.cs b/SubliminalServer/DataModel/Configurations/PurgatoryTagConfiguration.cs index 5034eb1..d8a40e5 100644 --- a/SubliminalServer/DataModel/Configurations/PurgatoryTagConfiguration.cs +++ b/SubliminalServer/DataModel/Configurations/PurgatoryTagConfiguration.cs @@ -8,11 +8,11 @@ public class PurgatoryTagConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.HasKey(tag => tag.TagKey); + builder.HasKey(tag => tag.Id); // One to many (PurgatoryEntry) builder.HasOne(tag => tag.PurgatoryEntry) .WithMany(entry => entry.Tags) - .HasForeignKey(tag => tag.EntryKey); + .HasForeignKey(tag => tag.EntryId); } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Purgatory/IDatabasePoem.cs b/SubliminalServer/DataModel/Purgatory/IDatabasePoem.cs index ebfaf2e..4b66663 100644 --- a/SubliminalServer/DataModel/Purgatory/IDatabasePoem.cs +++ b/SubliminalServer/DataModel/Purgatory/IDatabasePoem.cs @@ -12,15 +12,15 @@ public interface IDatabasePoem public string PoemContent { get; set; } // Foreign key AccountData - public int? AuthorKey { get; set; } + public int? AuthorId { get; set; } public AccountData? Author { get; set; } // Foreign key PurgatoryEntry - public int? AmendsKey { get; set; } + public int? AmendsId { get; set; } public PurgatoryEntry? Amends { get; set; } // Foreign key PurgatoryEntry - public int? EditsKey { get; set; } + public int? EditsId { get; set; } public PurgatoryEntry Edits { get; set; } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Purgatory/PurgatoryAnnotation.cs b/SubliminalServer/DataModel/Purgatory/PurgatoryAnnotation.cs index 1a48e4f..828ab6c 100644 --- a/SubliminalServer/DataModel/Purgatory/PurgatoryAnnotation.cs +++ b/SubliminalServer/DataModel/Purgatory/PurgatoryAnnotation.cs @@ -5,13 +5,13 @@ namespace SubliminalServer.DataModel.Purgatory; -[PrimaryKey(nameof(AnnotationKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryAnnotation { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int AnnotationKey { get; set; } + public int Id { get; set; } // Foreign key PurgatoryEntry [Required] diff --git a/SubliminalServer/DataModel/Purgatory/PurgatoryDraft.cs b/SubliminalServer/DataModel/Purgatory/PurgatoryDraft.cs index 356de92..9dbed18 100644 --- a/SubliminalServer/DataModel/Purgatory/PurgatoryDraft.cs +++ b/SubliminalServer/DataModel/Purgatory/PurgatoryDraft.cs @@ -2,18 +2,18 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; +using SubliminalServer.ApiModel; using SubliminalServer.DataModel.Account; -using SubliminalServer.DataModel.Api; namespace SubliminalServer.DataModel.Purgatory; -[PrimaryKey(nameof(DraftKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryDraft : UploadableEntry, IDatabasePoem { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int DraftKey { get; set; } + public int Id { get; set; } // Client submitted [MaxLength(300)] @@ -32,14 +32,14 @@ public class PurgatoryDraft : UploadableEntry, IDatabasePoem // Foreign key AccountData [ForeignKey(nameof(Author))] - public int? AuthorKey { get; set; } + public int? AuthorId { get; set; } [JsonIgnore] [JsonPropertyName("Author")] public AccountData? Author { get; set; } // Foreign key PurgatoryEntry [ForeignKey(nameof(Amends))] - public int? AmendsKey { get; set; } + public int? AmendsId { get; set; } [JsonIgnore] [JsonPropertyName("Amends")] public PurgatoryEntry? Amends { get; set; } @@ -47,7 +47,7 @@ public class PurgatoryDraft : UploadableEntry, IDatabasePoem // Foreign key PurgatoryEntry [ForeignKey(nameof(Edits))] [JsonPropertyName("Edits")] - public int? EditsKey { get; set; } + public int? EditsId { get; set; } [JsonIgnore] public PurgatoryEntry Edits { get; set; } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Purgatory/PurgatoryEntry.cs b/SubliminalServer/DataModel/Purgatory/PurgatoryEntry.cs index ed267b4..fedd3cc 100644 --- a/SubliminalServer/DataModel/Purgatory/PurgatoryEntry.cs +++ b/SubliminalServer/DataModel/Purgatory/PurgatoryEntry.cs @@ -3,17 +3,16 @@ using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using SubliminalServer.DataModel.Account; -using SubliminalServer.DataModel.Api; namespace SubliminalServer.DataModel.Purgatory; -[PrimaryKey(nameof(EntryKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryEntry : IDatabasePoem { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int EntryKey { get; set; } + public int Id { get; set; } // Client submitted [MaxLength(300)] @@ -35,21 +34,21 @@ public class PurgatoryEntry : IDatabasePoem // Foreign key AccountData [ForeignKey(nameof(Author))] [JsonPropertyName("Author")] - public int? AuthorKey { get; set; } + public int? AuthorId { get; set; } [JsonIgnore] public AccountData? Author { get; set; } // Foreign key PurgatoryEntry [ForeignKey(nameof(Amends))] [JsonPropertyName("Amends")] - public int? AmendsKey { get; set; } + public int? AmendsId { get; set; } [JsonIgnore] public PurgatoryEntry? Amends { get; set; } // Foreign key PurgatoryEntry [ForeignKey(nameof(Edits))] [JsonPropertyName("Edits")] - public int? EditsKey { get; set; } + public int? EditsId { get; set; } [JsonIgnore] public PurgatoryEntry? Edits { get; set; } diff --git a/SubliminalServer/DataModel/Purgatory/PurgatoryTag.cs b/SubliminalServer/DataModel/Purgatory/PurgatoryTag.cs index 48a06f3..d5789aa 100644 --- a/SubliminalServer/DataModel/Purgatory/PurgatoryTag.cs +++ b/SubliminalServer/DataModel/Purgatory/PurgatoryTag.cs @@ -4,18 +4,18 @@ namespace SubliminalServer.DataModel.Purgatory; -[PrimaryKey(nameof(TagKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryTag { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int TagKey { get; set; } + public int Id { get; set; } public string TagName { get; set; } // Foreign key [ForeignKey(nameof(PurgatoryEntry))] - public int EntryKey { get; set; } + public int EntryId { get; set; } // Navigation property to the parent PurgatoryEntry public PurgatoryEntry PurgatoryEntry { get; set; } diff --git a/SubliminalServer/DataModel/Rating/PurgatoryAnnotationRating.cs b/SubliminalServer/DataModel/Rating/PurgatoryAnnotationRating.cs index 82d9f67..a59145c 100644 --- a/SubliminalServer/DataModel/Rating/PurgatoryAnnotationRating.cs +++ b/SubliminalServer/DataModel/Rating/PurgatoryAnnotationRating.cs @@ -6,12 +6,12 @@ namespace SubliminalServer.DataModel.Rating; -[PrimaryKey(nameof(RatingKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryAnnotationRating { [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int RatingKey { get; set; } + public int Id { get; set; } // This should only be Approve | Veto public RatingType RatingType { get; set; } diff --git a/SubliminalServer/DataModel/Rating/PurgatoryRating.cs b/SubliminalServer/DataModel/Rating/PurgatoryRating.cs index 0b8af44..af1bcf6 100644 --- a/SubliminalServer/DataModel/Rating/PurgatoryRating.cs +++ b/SubliminalServer/DataModel/Rating/PurgatoryRating.cs @@ -6,12 +6,12 @@ namespace SubliminalServer.DataModel.Rating; -[PrimaryKey(nameof(RatingKey))] +[PrimaryKey(nameof(Id))] public class PurgatoryRating { [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int RatingKey { get; set; } + public int Id { get; set; } // This should only be Approve | Veto public RatingType RatingType { get; set; } diff --git a/SubliminalServer/DataModel/Report/AccountReport.cs b/SubliminalServer/DataModel/Report/AccountReport.cs index f8965a4..c52ecf4 100644 --- a/SubliminalServer/DataModel/Report/AccountReport.cs +++ b/SubliminalServer/DataModel/Report/AccountReport.cs @@ -8,7 +8,7 @@ public class AccountReport : Report { [Required] [ForeignKey(nameof(Account))] - public int AccountKey { get; set; } + public int AccountId { get; set; } public AccountData Account { get; set; } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Report/PurgatoryReport.cs b/SubliminalServer/DataModel/Report/PurgatoryReport.cs index db5a34b..916b65e 100644 --- a/SubliminalServer/DataModel/Report/PurgatoryReport.cs +++ b/SubliminalServer/DataModel/Report/PurgatoryReport.cs @@ -8,6 +8,6 @@ public class PurgatoryReport : Report { [Required] [ForeignKey(nameof(Poem))] - public int PoemKey { get; set; } + public int PoemId { get; set; } public PurgatoryEntry Poem { get; set; } } \ No newline at end of file diff --git a/SubliminalServer/DataModel/Report/Report.cs b/SubliminalServer/DataModel/Report/Report.cs index 4414216..141cb05 100644 --- a/SubliminalServer/DataModel/Report/Report.cs +++ b/SubliminalServer/DataModel/Report/Report.cs @@ -5,13 +5,13 @@ namespace SubliminalServer.DataModel.Report; -[PrimaryKey(nameof(ReportKey))] +[PrimaryKey(nameof(ReportId))] public class Report { // Unique, Primary key [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ReportKey { get; set; } + public string ReportId { get; set; } // Foreign key Account Data [Required] diff --git a/SubliminalServer/DatabaseContext.cs b/SubliminalServer/DatabaseContext.cs index 4531774..7e6160b 100644 --- a/SubliminalServer/DatabaseContext.cs +++ b/SubliminalServer/DatabaseContext.cs @@ -11,7 +11,7 @@ public class DatabaseContext : DbContext { public DbSet Accounts { get; set; } public DbSet AccountBadges { get; set; } - public DbSet AccountAddresses { get; set; } + public DbSet AccountAddressInfos { get; set; } public DbSet PurgatoryDrafts { get; set; } public DbSet PurgatoryEntries { get; set; } diff --git a/SubliminalServer/Models.cs b/SubliminalServer/Models.cs deleted file mode 100644 index 6d56789..0000000 --- a/SubliminalServer/Models.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SubliminalServer; - -///

-/// Date should ideally be a UTC (Universal Time Zone) date -/// -public record PurgatoryBeforeAfter(DateTime Date, int Count); \ No newline at end of file diff --git a/SubliminalServer/Program.Accounts.cs b/SubliminalServer/Program.Accounts.cs new file mode 100644 index 0000000..8685100 --- /dev/null +++ b/SubliminalServer/Program.Accounts.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Mvc; +using SubliminalServer.ApiModel; +using SubliminalServer.DataModel.Account; +using SubliminalServer.DataModel.Purgatory; +using SubliminalServer.DataModel.Report; + +namespace SubliminalServer; + +public static partial class Program +{ + private static void AddAccountEndpoints() + { + // Account action endpoints + httpServer.MapPost("/accounts/{accountId}/block", ([FromBody] int accountId, [FromServices] DatabaseContext database, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/{accountId}/unblock", ([FromBody] int accountId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/{accountId}/follow", ([FromBody] int accountId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/{accountId}/report", (int accountId, [FromBody] UploadableReport reportUpload, [FromServices] DatabaseContext database, HttpContext context) => + { + var validationIssues = new Dictionary(); + if (reportUpload.Reason.Length > 300) + { + validationIssues.Add(nameof(reportUpload.Reason), ValidationFails.ReportReasonTooLong); + } + var validKey = reportUpload.TargetType switch + { + ReportTargetType.Entry => database.PurgatoryEntries + .Any(entry => entry.Id == reportUpload.TargetKey), + ReportTargetType.Account => database.Accounts + .Any(account => account.Id == reportUpload.TargetKey), + ReportTargetType.Annotation => database.PurgatoryAnnotations + .Any(entry => entry.Id == reportUpload.TargetKey), + _ => throw new ArgumentOutOfRangeException(nameof(reportUpload.TargetType)) + }; + if (!validKey) + { + validationIssues.Add(nameof(reportUpload.TargetType), ValidationFails.ReportTargetDoesntExist); + } + if (validationIssues.Count > 0) + { + return Results.ValidationProblem(validationIssues); + } + + var account = (AccountData) context.Items["Account"]!; + /*var report = new Report() + { + ReporterKey = account.Id, + TargetKey = reportUpload.TargetKey, + Reason = reportUpload.Reason, + ReportType = reportUpload.ReportType, + ReportTargetType = reportUpload.TargetType, + DateCreated = DateTime.UtcNow + }; + database.Reports.Add(report); + database.SaveChanges();*/ + + return Results.Ok(); + }); + // TODO: Reimplement this + //rateLimitEndpoints.Add("/accounts/{accountId}/report", (1, TimeSpan.FromSeconds(60))); + //authRequiredEndpoints.Add("/accounts/{accountId}/report"); + + httpServer.MapPost("/accounts/{accountId}/unfollow", ([FromBody] string userId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + + // Personal account endpoints + httpServer.MapPost("/accounts/me/email", ([FromBody] string email, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/me/pen-name", ([FromBody] string penName, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/me/biography", ([FromBody] string biography, HttpContext context) => + { + + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/me/location", ([FromBody] string location, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/me/role", ([FromBody] string role, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/accounts/me/avatar", ([FromBody] string avatarUrl, HttpContext context) => + { + throw new NotImplementedException(); + }); + } +} \ No newline at end of file diff --git a/SubliminalServer/Program.Auth.cs b/SubliminalServer/Program.Auth.cs new file mode 100644 index 0000000..14db4d3 --- /dev/null +++ b/SubliminalServer/Program.Auth.cs @@ -0,0 +1,168 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SubliminalServer.ApiModel; +using SubliminalServer.DataModel.Account; + +namespace SubliminalServer; + +public static partial class Program +{ + private static void AddAuthEndpoints() + { + //Creates a new account with a provided pen name, and then gives the client the credentials for their created account + httpServer.MapPost("/auth/signup", static ([FromBody] LoginDetails details, [FromServices] DatabaseContext database, HttpContext context) => + { + if (!PermissibleUsernameRegex().IsMatch(details.Username)) + { + return Results.ValidationProblem(new Dictionary() + { { nameof(LoginDetails.Username), ValidationFails.InvalidUsername } }); + } + + var existingAccount = database.Accounts.SingleOrDefault(account => + account.Email == details.Email || account.Username == details.Username); + if (existingAccount is not null) + { + return Results.Conflict(); + } + + // TODO: Email validation, this will all be moved elsewhere + var tokenString = GenerateToken(); + var account = new AccountData(details.Username, details.Email, DateTime.UtcNow, tokenString); + + // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback + var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); + if (requestIp is null) + { + return Results.Forbid(); + } + account.KnownIPs.Add(new AccountAddress() + { + IpAddress = requestIp + }); + database.Accounts.Add(account); + database.SaveChanges(); + + context.Response.Cookies.Append("Token", tokenString, new CookieOptions() + { + HttpOnly = true, + SameSite = SameSiteMode.Strict, + Expires = DateTimeOffset.UtcNow.AddMonths(1) + }); + // If for some reason they can not persist the cookie, we also send them the token so that they may save it somwehere + // secure on certain platforms, such as a third party non-web client. + return Results.Ok(account.Token); + }); + if (!httpServer.Environment.IsDevelopment()) + { + rateLimitEndpoints.Add("/auth/signup", (1, TimeSpan.FromSeconds(20))); + sizeLimitEndpoints.Add("/auth/signup", PayloadSize.FromKilobytes(5)); + } + + // Allows a user to signin and receive account data + httpServer.MapPost("/auth/signin/token", ([FromBody] string? token, [FromServices] DatabaseContext database, HttpContext context) => + { + if (string.IsNullOrEmpty(token)) + { + var cookieToken = context.Request.Cookies["Token"]; + if (string.IsNullOrEmpty(cookieToken)) + { + return Results.Unauthorized(); + } + + token = cookieToken; + } + + // Completely invalid token - Reject + var expiryString = token.Split(";").Last(); + if (!long.TryParse(expiryString, out var expiry)) + { + return Results.Unauthorized(); + } + + // Expired token - Reject + if (expiry < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + { + return Results.Unauthorized(); + } + + // No account associated with token - Reject + var account = database.Accounts.SingleOrDefault(data => data.Token == token); + if (account is null) + { + return Results.NotFound(); + } + + // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback + var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); + if (requestIp is null) + { + return Results.Forbid(); + } + account.KnownIPs.Add(new AccountAddress() + { + IpAddress = requestIp + }); + database.SaveChanges(); + + return Results.Json(account); + }); + if (!httpServer.Environment.IsDevelopment()) + { + rateLimitEndpoints.Add("/auth/signin/token", (1, TimeSpan.FromSeconds(1))); + sizeLimitEndpoints.Add("/auth/signin/token", PayloadSize.FromKilobytes(5)); + } + + httpServer.MapPost("/auth/signin", async ([FromBody] LoginDetails details, [FromServices] DatabaseContext database, HttpContext context) => + { + var account = database.Accounts.SingleOrDefault(account => + account.Username == details.Username && account.Email == details.Email); + if (account is null) + { + return Results.NotFound(); + } + + // If the current account token is expired, we will generate a new one, + // we will also give them the token cookie regardless + var expiryString = account.Token.Split(";").Last(); + if (long.Parse(expiryString) < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + { + var tokenString = GenerateToken(); + account.Token = tokenString; + database.SaveChanges(); + } + + // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback + var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); + if (requestIp is null) + { + return Results.Forbid(); + } + var addressInfo = await database.AccountAddressInfos + .SingleOrDefaultAsync(info => info.IpAddress == requestIp); + if (addressInfo is null) + { + account.KnownIPs.Add(new AccountAddress() + { + IpAddress = requestIp, + LastUsed = DateTime.Now + }); + } + else + { + addressInfo.LastUsed = DateTime.Now; + } + await database.SaveChangesAsync(); + + context.Response.Cookies.Append("Token", account.Token, new CookieOptions() + { + HttpOnly = true, + SameSite = SameSiteMode.Strict, + Expires = DateTimeOffset.UtcNow.AddMonths(1) + }); + + return Results.Json(account); + }); + rateLimitEndpoints.Add("/auth/signin", (1, TimeSpan.FromSeconds(1))); + sizeLimitEndpoints.Add("/auth/signin", PayloadSize.FromKilobytes(5)); + } +} \ No newline at end of file diff --git a/SubliminalServer/Program.Profiles.cs b/SubliminalServer/Program.Profiles.cs new file mode 100644 index 0000000..c2d0da5 --- /dev/null +++ b/SubliminalServer/Program.Profiles.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using SubliminalServer.ApiModel; + +namespace SubliminalServer; + +public static partial class Program +{ + private static void AddProfileEndpoints() + { + // Get public facing data for an account, will accept either a username or an account key + httpServer.MapGet("/profiles/{profileIdentifier}", (string profileIdentifier, [FromServices] DatabaseContext database) => + { + var account = int.TryParse(profileIdentifier, out var profileId) + ? database.Accounts.SingleOrDefault(account => account.Id == profileId) + : database.Accounts.SingleOrDefault(account => account.Username == profileIdentifier); + if (account is null) + { + return Results.NotFound(); + } + + var profile = new UploadableProfile(account); + return Results.Json(profile); + }); + rateLimitEndpoints.Add("/Profiles", (1, TimeSpan.FromMilliseconds(500))); + } +} \ No newline at end of file diff --git a/SubliminalServer/Program.Purgatory.cs b/SubliminalServer/Program.Purgatory.cs new file mode 100644 index 0000000..c67dcec --- /dev/null +++ b/SubliminalServer/Program.Purgatory.cs @@ -0,0 +1,164 @@ +using Microsoft.AspNetCore.Mvc; +using SubliminalServer.ApiModel; +using SubliminalServer.DataModel.Account; +using SubliminalServer.DataModel.Purgatory; + +namespace SubliminalServer; + +public static partial class Program +{ + private static void AddPurgatoryEndpoints() + { + httpServer.MapGet("/purgatory/{poemId}/report", (string poemId) => + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"WIP: Report function - Poem {poemId} has been reported, please investigate!"); + Console.ResetColor(); + return Results.Ok(); + }); + // TODO: Reimplement this + //authRequiredEndpoints.Add("/purgatory/{poemId}/report"); + //rateLimitEndpoints.Add("/purgatory/{poemId}/report", (1, TimeSpan.FromSeconds(5))); + //sizeLimitEndpoints.Add("/purgatory/{poemId}/report", PayloadSize.FromKilobytes(100)); + + httpServer.MapGet("/purgatory/picks", ([FromServices] DatabaseContext database) => + { + var entries = database.PurgatoryEntries + .Where(entry => entry.Pick == true) + .Select(entry => entry.Id); + return Results.Json(entries); + }); + rateLimitEndpoints.Add("/purgatory/picks", (1, TimeSpan.FromSeconds(2))); + + httpServer.MapGet("/purgatory/after", ([FromQuery(Name = "date")] DateTime date, [FromQuery(Name = "count")] int count, [FromServices] DatabaseContext database) => + { + var poemIds = database.PurgatoryEntries + .Where(entry => entry.DateCreated > date) + .Take(Math.Clamp(count, 1, 50)) + .Select(poem => poem.Id); + return Results.Json(poemIds); + }); + rateLimitEndpoints.Add("/purgatory/after", (1, TimeSpan.FromSeconds(2))); + + + httpServer.MapGet("/purgatory/before", ([FromQuery(Name = "date")] DateTime date, [FromQuery(Name = "count")] int count, [FromServices] DatabaseContext database) => + { + var poemIds = database.PurgatoryEntries + .Where(entry => entry.DateCreated < date) + .Take(Math.Clamp(count, 1, 50)) + .Select(poem => poem.Id); + return Results.Json(poemIds); + }); + rateLimitEndpoints.Add("/purgatory/before", (1, TimeSpan.FromSeconds(2))); + + // Take into account genres that they have liked, accounts they have blocked, new poems and interactions when reccomending + httpServer.MapGet("/purgatory/recommended", () => + { + return Results.Problem(); + }); + authRequiredEndpoints.Add("/purgatory/recommended"); + rateLimitEndpoints.Add("/purgatory/recommended", (1, TimeSpan.FromSeconds(5))); + + httpServer.MapPost("/purgatory", ([FromBody] UploadableEntry entryUpload, [FromServices] DatabaseContext database, HttpContext context) => + { + var validationIssues = new Dictionary(); + var tags = new List(); + + if (entryUpload.Summary?.Length > 300) + { + validationIssues.Add(nameof(entryUpload.Summary), ValidationFails.SummaryTooLong); + } + if (entryUpload.PoemName.Length > 32) + { + validationIssues.Add(nameof(entryUpload.PoemName), ValidationFails.PoemNameTooLong); + } + // TODO: For very long poems, loading it all as a string will rail the database and server memory. + // TODO: Consider moving long poems such as this to have their content handled as a blob or streamed as a separate file. + if (entryUpload.PoemContent.Length > 100_000) + { + validationIssues.Add(nameof(entryUpload.PoemContent), ValidationFails.PoemContentTooLong); + } + for (var i = 0; i < Math.Min(5, entryUpload.PoemTags.Count); i++) + { + var tag = entryUpload.PoemTags[i]; + + if (!PermissibleTagRegex().IsMatch(tag)) + { + validationIssues.Add(nameof(UploadableEntry.PoemTags), ValidationFails.InvalidTagProvided); + } + + tags.Add(new PurgatoryTag() + { + TagName = tag + }); + } + if (validationIssues.Count > 0) + { + return Results.ValidationProblem(validationIssues); + } + + var amendsKey = database.PurgatoryEntries + .Where(entry => entry.Id == entryUpload.Amends) + .Select(entry => entry.Id) + .SingleOrDefault(); + var editsKey = database.PurgatoryEntries + .Where(entry => entry.Id == entryUpload.Edits) + .Select(entry => entry.Id) + .SingleOrDefault(); + + var entry = new PurgatoryEntry + { + Summary = entryUpload.Summary, + ContentWarning = entryUpload.ContentWarning, + PageStyle = entryUpload.PageStyle, + PageBackgroundUrl = null, // TODO: Handle later, might need form/multipart + Tags = tags, + PoemName = entryUpload.PoemName, + PoemContent = entryUpload.PoemContent, + AuthorId = ((AccountData) context.Items["Account"]!).Id, + AmendsId = amendsKey, + EditsId = editsKey, + Approves = 0, + Vetoes = 0, + DateCreated = DateTime.UtcNow, + Pick = false + }; + + database.PurgatoryEntries.Add(entry); + database.SaveChanges(); + + return Results.Ok(entry.Id); + }); + authRequiredEndpoints.Add("/purgatory"); + if (!httpServer.Environment.IsDevelopment()) + { + rateLimitEndpoints.Add("/purgatory", (1, TimeSpan.FromSeconds(60))); + sizeLimitEndpoints.Add("/purgatory", PayloadSize.FromMegabytes(5)); + } + + httpServer.MapPost("/purgatory/{id}/like", ([FromBody] int poemId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/purgatory/{id}/unlike", ([FromBody] int poemId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/purgatory/{id}/pin", ([FromBody] int poemId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/purgatory/{id}/unpin", ([FromBody] int poemId, HttpContext context) => + { + throw new NotImplementedException(); + }); + + httpServer.MapPost("/purgatory/{id}/rate", ([FromBody] UploadableRating ratingUpload, HttpContext context) => + { + throw new NotImplementedException(); + }); + } +} \ No newline at end of file diff --git a/SubliminalServer/Program.cs b/SubliminalServer/Program.cs index 348b3da..3b98df4 100644 --- a/SubliminalServer/Program.cs +++ b/SubliminalServer/Program.cs @@ -4,576 +4,185 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; -using SubliminalServer; +using SubliminalServer.ApiModel; using SubliminalServer.DataModel.Account; -using SubliminalServer.DataModel.Api; -using SubliminalServer.DataModel.Purgatory; using SubliminalServer.DataModel.Report; using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; +namespace SubliminalServer; + // EFCore database setup: // dotnet ef migrations add InitialCreate // dotnet ef database update // Prerelease .NET 9 may require "dotnet tool install --global dotnet-ef --prerelease" // to update from a non-prerelease, do "dotnet tool update --global dotnet-ef --prerelease" -var dataDir = new DirectoryInfo("Data"); -var profileImageDir = new DirectoryInfo(Path.Join(dataDir.FullName, "ProfileImages")); -var soundsDir = new DirectoryInfo(Path.Join(dataDir.FullName, "Sounds")); -var configFile = new FileInfo("config.json"); -var dbPath = Path.Join(dataDir.FullName, "subliminal.db"); - -ServerConfig? config = null; - -if (File.Exists(configFile.Name)) -{ - var configText = File.ReadAllText(configFile.Name); - config = JsonSerializer.Deserialize(configText); -} - -if (config is null) -{ - await using var stream = File.OpenWrite(configFile.Name); - await JsonSerializer.SerializeAsync(stream, new ServerConfig("", "", 1234, false), new JsonSerializerOptions - { - WriteIndented = true, - }); - await stream.FlushAsync(); - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("[LOG]: Config created! Please edit {0} and run this program again!", configFile); - Console.ResetColor(); - Environment.Exit(0); -} - -Console.ForegroundColor = ConsoleColor.Yellow; -foreach (var dirPath in new[] { dataDir, profileImageDir }) -{ - if (!Directory.Exists(dirPath.FullName)) - { - Directory.CreateDirectory(dirPath.FullName); - Console.WriteLine($"[WARN] Could not find {dirPath.Name} directory, creating."); - } -} -Console.ResetColor(); - -// Build web application and configure services -var builder = WebApplication.CreateBuilder(args); - -builder.Configuration["Kestrel:Certificates:Default:Path"] = config.Certificate; -builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = config.Key; - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - { - policy.WithOrigins("https://poemanthology.org", "*") - .WithOrigins("https://zekiah-a.github.io/", "*"); - }); -}); - -builder.Services.Configure(options => -{ - options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.SerializerOptions.PropertyNameCaseInsensitive = true; -}); - -builder.Services.AddDbContext(options => -{ - options.UseSqlite($"Data Source={dbPath}"); -}); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -// Swagger -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -// Configure middlewares and runtime services, including global authorization middleware that will -// validate accounts for all site endpoints -var httpServer = builder.Build(); -httpServer.Urls.Add($"{(config.UseHttps ? "https" : "http")}://*:{config.Port}"); - -httpServer.UseCors(policy => - policy.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(_ => true).AllowCredentials() -); - -httpServer.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider(profileImageDir.FullName), - RequestPath = "/ProfileImage" -}); - -if (httpServer.Environment.IsDevelopment()) -{ - httpServer.UseSwagger(); - httpServer.UseSwaggerUI(); -} - -static string GenerateToken() -{ - var tokenBytes = RandomNumberGenerator.GetBytes(32); - var tokenString = Convert.ToBase64String(tokenBytes) - + ";" + DateTimeOffset.UtcNow.AddMonths(1).ToUnixTimeSeconds(); - return tokenString; -} - -// This is some straightup weirdness to force inject the DB, it seems to work for out current use though -var scope = httpServer.Services.CreateScope(); -var serviceProvider = scope.ServiceProvider; -httpServer.UseMiddleware(serviceProvider.GetRequiredService()); - -var authRequiredEndpoints = new List(); -var rateLimitEndpoints = new Dictionary(); -var sizeLimitEndpoints = new Dictionary(); // Should only really be needed on POST endpoints - -httpServer.MapGet("/PurgatoryReport/{poemKey}", (string poemKey) => +public static partial class Program { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"WIP: Report function - Poem {poemKey} has been reported, please investigate!"); - Console.ResetColor(); - return Results.Ok(); -}); -authRequiredEndpoints.Add("/PurgatoryReport"); -rateLimitEndpoints.Add("/PurgatoryReport", (1, TimeSpan.FromSeconds(5))); -sizeLimitEndpoints.Add("/PurgatoryReport", PayloadSize.FromKilobytes(100)); - -httpServer.MapGet("/PurgatoryPicks", ([FromServices] DatabaseContext database) => -{ - var entries = database.PurgatoryEntries - .Where(entry => entry.Pick == true) - .Select(entry => entry.EntryKey); - return Results.Json(entries); -}); -rateLimitEndpoints.Add("/PurgatoryPicks", (1, TimeSpan.FromSeconds(2))); - -httpServer.MapGet("/PurgatoryAfter", ([FromBody] PurgatoryBeforeAfter since, [FromServices] DatabaseContext database) => -{ - var poemKeys = database.PurgatoryEntries - .Where(entry => entry.DateCreated > since.Date) - .Take(Math.Clamp(since.Count, 1, 50)) - .Select(poem => poem.EntryKey); - return Results.Json(poemKeys); -}); -rateLimitEndpoints.Add("/PurgatoryAfter", (1, TimeSpan.FromSeconds(2))); - - -httpServer.MapGet("/PurgatoryBefore", ([FromBody] PurgatoryBeforeAfter before, [FromServices] DatabaseContext database) => -{ - var poemKeys = database.PurgatoryEntries - .Where(entry => entry.DateCreated < before.Date) - .Take(Math.Clamp(before.Count, 1, 50)) - .Select(poem => poem.EntryKey); - return Results.Json(poemKeys); -}); -rateLimitEndpoints.Add("/PurgatoryBefore", (1, TimeSpan.FromSeconds(2))); - -// Take into account genres that they have liked, accounts they have blocked, new poems and interactions when reccomending -httpServer.MapGet("/PurgatoryRecommended", () => -{ - return Results.Problem(); -}); -authRequiredEndpoints.Add("/PurgatoryRecommended"); -rateLimitEndpoints.Add("/PurgatoryRecommended", (1, TimeSpan.FromSeconds(5))); - -httpServer.MapPost("/PurgatoryUpload", ([FromBody] UploadableEntry entryUpload, [FromServices] DatabaseContext database, HttpContext context) => -{ - var validationIssues = new Dictionary(); - var tags = new List(); + private static WebApplication httpServer; + private static List authRequiredEndpoints; + private static Dictionary rateLimitEndpoints; + // Should only really be needed on POST endpoint + private static Dictionary sizeLimitEndpoints; + + [GeneratedRegex("^[a-z][a-z0-9_-]{0,23}$")] + private static partial Regex PermissibleTagRegex(); + [GeneratedRegex("^[a-z][a-z0-9_.]{0,15}$")] + private static partial Regex PermissibleUsernameRegex(); - if (entryUpload.Summary?.Length > 300) - { - validationIssues.Add(nameof(entryUpload.Summary), ValidationFails.SummaryTooLong); - } - if (entryUpload.PoemName.Length > 32) - { - validationIssues.Add(nameof(entryUpload.PoemName), ValidationFails.PoemNameTooLong); - } - // TODO: For very long poems, loading it all as a string will rail the database and server memory. - // TODO: Consider moving long poems such as this to have their content handled as a blob or streamed as a separate file. - if (entryUpload.PoemContent.Length > 100_000) + public static async Task Main(string[] args) { - validationIssues.Add(nameof(entryUpload.PoemContent), ValidationFails.PoemContentTooLong); - } - for (var i = 0; i < Math.Min(5, entryUpload.PoemTags.Count); i++) - { - var tag = entryUpload.PoemTags[i]; + var dataDir = new DirectoryInfo("Data"); + var profileImageDir = new DirectoryInfo(Path.Join(dataDir.FullName, "ProfileImages")); + var soundsDir = new DirectoryInfo(Path.Join(dataDir.FullName, "Sounds")); + var configFile = new FileInfo("config.json"); + var dbPath = Path.Join(dataDir.FullName, "subliminal.db"); - if (!PermissibleTagRegex().IsMatch(tag)) + ServerConfig? config = null; + + if (File.Exists(configFile.Name)) { - validationIssues.Add(nameof(UploadableEntry.PoemTags), ValidationFails.InvalidTagProvided); + var configText = File.ReadAllText(configFile.Name); + config = JsonSerializer.Deserialize(configText); } - - tags.Add(new PurgatoryTag() - { - TagName = tag - }); - } - if (validationIssues.Count > 0) - { - return Results.ValidationProblem(validationIssues); - } - - var amendsKey = database.PurgatoryEntries - .Where(entry => entry.EntryKey == entryUpload.Amends) - .Select(entry => entry.EntryKey) - .SingleOrDefault(); - var editsKey = database.PurgatoryEntries - .Where(entry => entry.EntryKey == entryUpload.Edits) - .Select(entry => entry.EntryKey) - .SingleOrDefault(); - var entry = new PurgatoryEntry - { - Summary = entryUpload.Summary, - ContentWarning = entryUpload.ContentWarning, - PageStyle = entryUpload.PageStyle, - PageBackgroundUrl = null, // TODO: Handle later, might need form/multipart - Tags = tags, - PoemName = entryUpload.PoemName, - PoemContent = entryUpload.PoemContent, - AuthorKey = ((AccountData) context.Items["Account"]!).AccountKey, - AmendsKey = amendsKey, - EditsKey = editsKey, - Approves = 0, - Vetoes = 0, - DateCreated = DateTime.UtcNow, - Pick = false - }; - - database.PurgatoryEntries.Add(entry); - database.SaveChanges(); - - return Results.Ok(entry.EntryKey); -}); -authRequiredEndpoints.Add("/PurgatoryUpload"); -//rateLimitEndpoints.Add("/PurgatoryUpload", (1, TimeSpan.FromSeconds(60))); -sizeLimitEndpoints.Add("/PurgatoryUpload", PayloadSize.FromMegabytes(5)); - -//Creates a new account with a provided pen name, and then gives the client the credentials for their created account -httpServer.MapPost("/Signup", static ([FromBody] LoginDetails details, [FromServices] DatabaseContext database, HttpContext context) => -{ - if (!PermissibleUsernameRegex().IsMatch(details.Username)) - { - return Results.ValidationProblem(new Dictionary() - { { nameof(LoginDetails.Username), ValidationFails.InvalidUsername } }); - } - - var existingAccount = database.Accounts.SingleOrDefault(account => - account.Email == details.Email || account.Username == details.Username); - if (existingAccount is not null) - { - return Results.Conflict(); - } - - // TODO: Email validation, this will all be moved elsewhere - var tokenString = GenerateToken(); - var account = new AccountData(details.Username, details.Email, DateTime.UtcNow, tokenString); - - // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback - var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); - if (requestIp is null) - { - return Results.Forbid(); - } - account.KnownIPs.Add(new AccountAddress() - { - IpAddress = requestIp - }); - database.Accounts.Add(account); - database.SaveChanges(); - - context.Response.Cookies.Append("Token", tokenString, new CookieOptions() - { - HttpOnly = true, - SameSite = SameSiteMode.Strict, - Expires = DateTimeOffset.UtcNow.AddMonths(1) - }); - // If for some reason they can not persist the cookie, we also send them the token so that they may save it somwehere - // secure on certain platforms, such as a third party non-web client. - return Results.Ok(account.Token); -}); -rateLimitEndpoints.Add("/Signup", (1, TimeSpan.FromSeconds(2))); -sizeLimitEndpoints.Add("/Signup", PayloadSize.FromKilobytes(5)); - -// Allows a user to signin and receive account data -httpServer.MapPost("/SigninToken", ([FromBody] string? token, [FromServices] DatabaseContext database, HttpContext context) => -{ - if (string.IsNullOrEmpty(token)) - { - var cookieToken = context.Request.Cookies["Token"]; - if (string.IsNullOrEmpty(cookieToken)) + if (config is null) { - return Results.Unauthorized(); + await using var stream = File.OpenWrite(configFile.Name); + await JsonSerializer.SerializeAsync(stream, new ServerConfig(), new JsonSerializerOptions + { + WriteIndented = true, + }); + await stream.FlushAsync(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[LOG]: Config created! Please edit {0} and run this program again!", configFile); + Console.ResetColor(); + Environment.Exit(0); } - token = cookieToken; - } - - // Completely invalid token - Reject - var expiryString = token.Split(";").Last(); - if (!long.TryParse(expiryString, out var expiry)) - { - return Results.Unauthorized(); - } - - // Expired token - Reject - if (expiry < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) - { - return Results.Unauthorized(); - } - - // No account associated with token - Reject - var account = database.Accounts.SingleOrDefault(data => data.Token == token); - if (account is null) - { - return Results.NotFound(); - } - - // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback - var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); - if (requestIp is null) - { - return Results.Forbid(); - } - account.KnownIPs.Add(new AccountAddress() - { - IpAddress = requestIp - }); - database.SaveChanges(); - - return Results.Json(account); -}); -rateLimitEndpoints.Add("/SigninToken", (1, TimeSpan.FromSeconds(1))); -sizeLimitEndpoints.Add("/SigninToken", PayloadSize.FromKilobytes(5)); - -httpServer.MapPost("/Signin", ([FromBody] LoginDetails details, [FromServices] DatabaseContext database, HttpContext context) => -{ - var account = database.Accounts.SingleOrDefault(account => - account.Username == details.Username && account.Email == details.Email); - if (account is null) - { - return Results.NotFound(); - } - - // If the current account token is expired, we will generate a new one, - // we will also give them the token cookie regardless - var expiryString = account.Token.Split(";").Last(); - if (long.Parse(expiryString) < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) - { - var tokenString = GenerateToken(); - account.Token = tokenString; - database.SaveChanges(); - } - - // Rate limit middleware should have passed us a nicely sanitised IP. Otherwise we will just fallback - var requestIp = context.Items["RealIp"] as string ?? context.Connection.RemoteIpAddress?.ToString(); - if (requestIp is null) - { - return Results.Forbid(); - } - account.KnownIPs.Add(new AccountAddress() - { - IpAddress = requestIp - }); - database.SaveChanges(); - - context.Response.Cookies.Append("Token", account.Token, new CookieOptions() - { - HttpOnly = true, - SameSite = SameSiteMode.Strict, - Expires = DateTimeOffset.UtcNow.AddMonths(1) - }); - - return Results.Json(account); -}); -rateLimitEndpoints.Add("/Signin", (1, TimeSpan.FromSeconds(1))); -sizeLimitEndpoints.Add("/Signin", PayloadSize.FromKilobytes(5)); - -//Get public facing data for an account, will accept either a username or an account key -httpServer.MapGet("/Profiles/{profileIdentifier}", (string profileIdentifier, [FromServices] DatabaseContext database) => -{ - var account = int.TryParse(profileIdentifier, out var profileKey) - ? database.Accounts.SingleOrDefault(account => account.AccountKey == profileKey) - : database.Accounts.SingleOrDefault(account => account.Username == profileIdentifier); - if (account is null) - { - return Results.NotFound(); - } - - var profile = new UploadableProfile(account); - return Results.Json(profile); -}); -rateLimitEndpoints.Add("/Profiles", (1, TimeSpan.FromMilliseconds(500))); - -// Account action endpoints -httpServer.MapPost("/Block", ([FromBody] int userKey, [FromServices] DatabaseContext database, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); -rateLimitEndpoints.Add("/Block", (1, TimeSpan.FromSeconds(2))); -authRequiredEndpoints.Add("/Block"); - -httpServer.MapPost("/Unblock", ([FromBody] int userKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); - -httpServer.MapPost("/Follow", ([FromBody] int userKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); - -httpServer.MapPost("/Report", ([FromBody] UploadableReport reportUpload, [FromServices] DatabaseContext database, HttpContext context) => -{ - var validationIssues = new Dictionary(); - if (reportUpload.Reason.Length > 300) - { - validationIssues.Add(nameof(reportUpload.Reason), ValidationFails.ReportReasonTooLong); - } - var validKey = reportUpload.TargetType switch - { - ReportTargetType.Entry => database.PurgatoryEntries - .Any(entry => entry.EntryKey == reportUpload.TargetKey), - ReportTargetType.Account => database.Accounts - .Any(account => account.AccountKey == reportUpload.TargetKey), - ReportTargetType.Annotation => database.PurgatoryAnnotations - .Any(entry => entry.AnnotationKey == reportUpload.TargetKey), - _ => throw new ArgumentOutOfRangeException(nameof(reportUpload.TargetType)) - }; - if (!validKey) - { - validationIssues.Add(nameof(reportUpload.TargetType), ValidationFails.ReportTargetDoesntExist); - } - if (validationIssues.Count > 0) - { - return Results.ValidationProblem(validationIssues); - } - - var account = (AccountData) context.Items["Account"]!; - /*var report = new Report() - { - ReporterKey = account.AccountKey, - TargetKey = reportUpload.TargetKey, - Reason = reportUpload.Reason, - ReportType = reportUpload.ReportType, - ReportTargetType = reportUpload.TargetType, - DateCreated = DateTime.UtcNow - }; - database.Reports.Add(report); - database.SaveChanges();*/ - - return Results.Ok(); -}); -rateLimitEndpoints.Add("/Report", (1, TimeSpan.FromSeconds(60))); -authRequiredEndpoints.Add("/Report"); - -httpServer.MapPost("/UnfollowUser", ([FromBody] string userKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); - -httpServer.MapPost("/LikePoem", ([FromBody] int poemKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); - -httpServer.MapPost("/UnlikePoem", ([FromBody] int poemKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + Console.ForegroundColor = ConsoleColor.Yellow; + foreach (var dirPath in new[] { dataDir, profileImageDir }) + { + if (!Directory.Exists(dirPath.FullName)) + { + Directory.CreateDirectory(dirPath.FullName); + Console.WriteLine($"[WARN] Could not find {dirPath.Name} directory, creating."); + } + } + Console.ResetColor(); -httpServer.MapPost("/PinPoem", ([FromBody] int poemKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + // Build web application and configure services + var builder = WebApplication.CreateBuilder(args); -httpServer.MapPost("/UnpinPoem", ([FromBody] int poemKey, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + builder.Configuration["Kestrel:Certificates:Default:Path"] = config.Certificate; + builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = config.Key; -httpServer.MapPost("/UpdateEmail", ([FromBody] string email, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + builder.Services.AddCors(options => + { + options.AddDefaultPolicy(policy => + { + policy.WithOrigins("https://poemanthology.org", "*") + .WithOrigins("https://zekiah-a.github.io/", "*"); + }); + }); -httpServer.MapPost("/UpdatePenName", ([FromBody] string penName, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + builder.Services.Configure(options => + { + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.SerializerOptions.PropertyNameCaseInsensitive = true; + }); -httpServer.MapPost("/UpdateBiography", ([FromBody] string biography, HttpContext context) => -{ - - return Results.Problem(); // TODO: Implement -}); + builder.Services.AddDbContext(options => + { + options.UseSqlite($"Data Source={dbPath}"); + }); + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -httpServer.MapPost("/UpdateLocation", ([FromBody] string location, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + // Swagger + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); -httpServer.MapPost("/UpdateRole", ([FromBody] string role, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + // Configure middlewares and runtime services, including global authorization middleware that will + // validate accounts for all site endpoints + httpServer = builder.Build(); + httpServer.Urls.Add($"{(config.UseHttps ? "https" : "http")}://*:{config.Port}"); -httpServer.MapPost("/UpdateAvatar", ([FromBody] string avatarUrl, HttpContext context) => -{ - return Results.Problem(); // TODO: Implement -}); + httpServer.UseCors(policy => + policy.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(_ => true).AllowCredentials() + ); -httpServer.MapPost("/RatePoem", ([FromBody] UploadableRating ratingUpload, HttpContext context) => -{ - - return Results.Problem(); // TODO: Implement -}); + httpServer.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(profileImageDir.FullName), + RequestPath = "/ProfileImage" + }); -// Endpoints that enforce Account/IP rate limiting -foreach (var endpointArgsPair in rateLimitEndpoints) -{ - httpServer.UseWhen - ( - context => context.Request.Path.StartsWithSegments(endpointArgsPair.Key), - appBuilder => + if (httpServer.Environment.IsDevelopment()) { - appBuilder.UseMiddleware(endpointArgsPair.Value.RequestLimit, endpointArgsPair.Value.TimeInterval); + httpServer.UseSwagger(); + httpServer.UseSwaggerUI(); } - ); -} - -foreach (var endpointArgsPair in sizeLimitEndpoints) -{ - httpServer.UseWhen - ( - context => context.Request.Path.StartsWithSegments(endpointArgsPair.Key), - appBuilder => + + // This is some straightup weirdness to force inject the DB, it seems to work for out current use though + var scope = httpServer.Services.CreateScope(); + var serviceProvider = scope.ServiceProvider; + httpServer.UseMiddleware(serviceProvider.GetRequiredService()); + + authRequiredEndpoints = new List(); + rateLimitEndpoints = new Dictionary(); + sizeLimitEndpoints = new Dictionary(); // Should only really be needed on POST endpoints + + AddPurgatoryEndpoints(); + AddAuthEndpoints(); + AddAccountEndpoints(); + AddProfileEndpoints(); + + // Endpoints that enforce Account/IP rate limiting + foreach (var endpointArgsPair in rateLimitEndpoints) { - appBuilder.UseMiddleware(endpointArgsPair.Value.AsLong()); + httpServer.UseWhen + ( + context => context.Request.Path.StartsWithSegments(endpointArgsPair.Key), + appBuilder => + { + appBuilder.UseMiddleware(endpointArgsPair.Value.RequestLimit, endpointArgsPair.Value.TimeInterval); + } + ); } - ); -} -// Endpoints that require an account to access -foreach (var endpoint in authRequiredEndpoints) -{ - httpServer.UseWhen - ( - context => context.Request.Path.StartsWithSegments(endpoint), - appBuilder => + foreach (var endpointArgsPair in sizeLimitEndpoints) { - appBuilder.UseMiddleware(); + httpServer.UseWhen + ( + context => context.Request.Path.StartsWithSegments(endpointArgsPair.Key), + appBuilder => + { + appBuilder.UseMiddleware(endpointArgsPair.Value.AsLong()); + } + ); } - ); -} - -await httpServer.RunAsync(); - -internal partial class Program -{ - [GeneratedRegex("^[a-z][a-z0-9_-]{0,23}$")] - private static partial Regex PermissibleTagRegex(); - [GeneratedRegex("^[a-z][a-z0-9_.]{0,15}$")] - private static partial Regex PermissibleUsernameRegex(); + // Endpoints that require an account to access + foreach (var endpoint in authRequiredEndpoints) + { + httpServer.UseWhen + ( + context => context.Request.Path.StartsWithSegments(endpoint), + appBuilder => + { + appBuilder.UseMiddleware(); + } + ); + } + await httpServer.RunAsync(); + } + + private static string GenerateToken() + { + var tokenBytes = RandomNumberGenerator.GetBytes(32); + var tokenString = Convert.ToBase64String(tokenBytes) + + ";" + DateTimeOffset.UtcNow.AddMonths(1).ToUnixTimeSeconds(); + return tokenString; + } } \ No newline at end of file diff --git a/SubliminalServer/Properties/launchSettings.json b/SubliminalServer/Properties/launchSettings.json index 7b010f9..21c1641 100644 --- a/SubliminalServer/Properties/launchSettings.json +++ b/SubliminalServer/Properties/launchSettings.json @@ -1,18 +1,10 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:61966", - "sslPort": 44398 - } - }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5104", + "applicationUrl": "http://localhost:1234", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -21,14 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7076;http://localhost:5104", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, + "applicationUrl": "https://localhost:1235;http://localhost:1234", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/SubliminalServer/ServerConfig.cs b/SubliminalServer/ServerConfig.cs index a5a82f1..d7f54c2 100644 --- a/SubliminalServer/ServerConfig.cs +++ b/SubliminalServer/ServerConfig.cs @@ -1,3 +1,11 @@ namespace SubliminalServer; -public record ServerConfig(string Certificate, string Key, int Port, bool UseHttps); \ No newline at end of file +public class ServerConfig +{ + public const int LatestVersion = 0; + public int Version = 0; + public string? Certificate = null; + public string? Key = null; + public int Port = 1234; + public bool UseHttps = false; +} \ No newline at end of file diff --git a/SubliminalServer/SubliminalServer.csproj b/SubliminalServer/SubliminalServer.csproj index a69fd98..715a2e9 100644 --- a/SubliminalServer/SubliminalServer.csproj +++ b/SubliminalServer/SubliminalServer.csproj @@ -20,18 +20,6 @@ - - - - - - - - - - - - diff --git a/SubliminalServer/ValidationFails.cs b/SubliminalServer/ValidationFails.cs index 0963a89..5e7c11b 100644 --- a/SubliminalServer/ValidationFails.cs +++ b/SubliminalServer/ValidationFails.cs @@ -2,12 +2,12 @@ namespace SubliminalServer; public static class ValidationFails { - public static readonly string[] InvalidUsername = { "Invalid username supplied" }; - public static readonly string[] SummaryTooLong = { "Provided summary must be less than 300 characters." }; - public static readonly string[] PoemNameTooLong = { "Provided poem name must be less than 32 characters." }; - public static readonly string[] PoemContentTooLong = { "Provided poem content must be less than 500000 characters." }; - public static readonly string[] InvalidTagProvided = { "Invalid tag provided" }; + public static readonly string[] InvalidUsername = ["Invalid username supplied"]; + public static readonly string[] SummaryTooLong = ["Provided summary must be less than 300 characters."]; + public static readonly string[] PoemNameTooLong = ["Provided poem name must be less than 32 characters."]; + public static readonly string[] PoemContentTooLong = ["Provided poem content must be less than 500000 characters."]; + public static readonly string[] InvalidTagProvided = ["Invalid tag provided"]; - public static readonly string[] ReportReasonTooLong = { "Provided report reason was too long" }; - public static readonly string[] ReportTargetDoesntExist = { "Provided report target does not exist" }; + public static readonly string[] ReportReasonTooLong = ["Provided report reason was too long"]; + public static readonly string[] ReportTargetDoesntExist = ["Provided report target does not exist"]; } \ No newline at end of file diff --git a/SubliminalServer/config.json b/SubliminalServer/config.json index a50b6e3..884b47a 100644 --- a/SubliminalServer/config.json +++ b/SubliminalServer/config.json @@ -1,6 +1,7 @@ { - "Certificate": "", - "Key": "", + "Version": 0, + "Certificate": null, + "Key": null, "Port": 1234, "UseHttps": false } \ No newline at end of file diff --git a/Volume-2/Idiots/industrial-society.json b/Volume-2/Idiots/industrial-society.json index 0d44b89..92eb8fe 100644 --- a/Volume-2/Idiots/industrial-society.json +++ b/Volume-2/Idiots/industrial-society.json @@ -1 +1 @@ -{"summary":"","tags":"","cWarning":false,"cWarningAdditions":"","poemName":"Industrial Society","poemAuthor":"Bushi","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"Machine Whirs Like Urea In The "},{"type":"newline","count":1},{"type":"text","content":"Wind That Carries Society "},{"type":"newline","count":1},{"type":"text","content":"On It's Cold Castle In The "},{"type":"newline","count":1},{"type":"text","content":"Grass. "},{"type":"newline","count":2},{"type":"text","content":"\"I like society\" said a man, his "},{"type":"newline","count":1},{"type":"text","content":"name was \"I\". "},{"type":"newline","count":1},{"type":"text","content":"\"Society is Potassium Dichromate (IV)\" "},{"type":"newline","count":1},{"type":"text","content":"said a woman whose name was \"Society\" "},{"type":"newline","count":1},{"type":"text","content":"\"I am Society\" said an idiot. "},{"type":"newline","count":2},{"type":"text","content":"They lived in a society, of idiots. "},{"type":"newline","count":1},{"type":"text","content":"Nature around them whirled into "},{"type":"newline","count":1},{"type":"text","content":"Sand and there they were. "},{"type":"newline","count":1},{"type":"text","content":"At a shattered visage, because "},{"type":"newline","count":1},{"type":"text","content":"all cliffs have a summit. "},{"type":"newline","count":2},{"type":"text","content":"\"Idiots\" "},{"type":"newline","count":3}]},"pageStyle":"poem-centre-wide","pageBackground":""} \ No newline at end of file +{"summary":"","tags":"","cWarning":false,"cWarningAdditions":"","poemName":"Industrial Society","poemAuthor":"Bushi","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"Machine Whirs Like Urea In The "},{"type":"newline","count":1},{"type":"text","content":"Wind That Carries Society "},{"type":"newline","count":1},{"type":"text","content":"On It's Cold Castle In The "},{"type":"newline","count":1},{"type":"text","content":"Grass. "},{"type":"newline","count":2},{"type":"text","content":"\"I like society\" said a man, his "},{"type":"newline","count":1},{"type":"text","content":"name was \"I\". "},{"type":"newline","count":1},{"type":"text","content":"\"Society is Potassium Dichromate (IV)\" "},{"type":"newline","count":1},{"type":"text","content":"said a woman whose name was \"Society\" "},{"type":"newline","count":1},{"type":"text","content":"\"I am Society\" said an idiot. "},{"type":"newline","count":2},{"type":"text","content":"They lived in a society, of idiots. "},{"type":"newline","count":1},{"type":"text","content":"Nature around them whirled into "},{"type":"newline","count":1},{"type":"text","content":"Sand and there they were. "},{"type":"newline","count":1},{"type":"text","content":"At a shattered visage, because "},{"type":"newline","count":1},{"type":"text","content":"all cliffs have a summit. "},{"type":"newline","count":2},{"type":"fragment","styles":[{"type":"bold"},{"type":"italic"},{"type":"colour","hex":"#FF0000"}],"children":[{"type":"text","content":"\"Idiots\""}]},{"type":"newline","count":3}]},"pageStyle":"poem-centre-wide","pageBackground":""} \ No newline at end of file diff --git a/Volume-2/Idiots/mifism.json b/Volume-2/Idiots/mifism.json index 60f85dd..955c82f 100644 --- a/Volume-2/Idiots/mifism.json +++ b/Volume-2/Idiots/mifism.json @@ -1 +1 @@ -{"summary":"","tags":"","cWarning":true,"cWarningAdditions":"Content Warning: This poem is not for goodie-goodie three shoes.","poemName":"MIFISM","poemAuthor":"Zekiah","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"The test is over,"},{"type":"newline","count":1},{"type":"text","content":"the marking has begun,"},{"type":"newline","count":1},{"type":"text","content":"\"MISS MISS MISSSS"},{"type":"newline","count":1},{"type":"text","content":"Can I have one mark here"},{"type":"newline","count":1},{"type":"text","content":"MISS"},{"type":"newline","count":1},{"type":"text","content":"Please just one\","},{"type":"newline","count":1},{"type":"text","content":""},{"type":"newline","count":1},{"type":"text","content":"The class is livid, everyone with"},{"type":"newline","count":1},{"type":"text","content":"their hands in their ears, sick of"},{"type":"newline","count":1},{"type":"text","content":"their retarded cry,"},{"type":"newline","count":1},{"type":"text","content":"\"You don't need any more FUCKING"},{"type":"newline","count":1},{"type":"text","content":"marks, you have grade 9.\""},{"type":"newline","count":1},{"type":"text","content":"You're wasting time, It's just a mistake,"},{"type":"newline","count":1},{"type":"text","content":"You can't do this in your GCSEs,"},{"type":"newline","count":1},{"type":"text","content":"You're making us late, to break,"},{"type":"newline","count":1},{"type":"text","content":"and lunch, and life, and going home,"},{"type":"newline","count":1},{"type":"text","content":"So respectfully,"},{"type":"newline","count":1},{"type":"text","content":"Enjoy your grade,"},{"type":"newline","count":1},{"type":"text","content":"Control your emotions,"},{"type":"newline","count":1},{"type":"text","content":"Please, just ask after,"},{"type":"newline","count":1},{"type":"text","content":"It's just a number."},{"type":"newline","count":2},{"type":"text","content":"It makes no difference,"},{"type":"newline","count":1},{"type":"text","content":"Do we care? NO!"},{"type":"newline","count":3}]},"pageStyle":"centre","pageBackground":""} \ No newline at end of file +{"summary":"","tags":"","cWarning":true,"cWarningAdditions":"Content Warning: This poem is not for goodie-goodie three shoes.","poemName":"MIFISM","poemAuthor":"Zekiah","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"The test is over,"},{"type":"newline","count":1},{"type":"text","content":"the marking has begun,"},{"type":"newline","count":1},{"type":"fragment","styles":[{"type":"italic"}],"children":[{"type":"text","content":"\"MISS MISS MISSSS"},{"type":"newline","count":1},{"type":"text","content":"Can I have one mark here"},{"type":"newline","count":1},{"type":"text","content":"MISS"},{"type":"newline","count":1},{"type":"text","content":"Please just one\","}]},{"type":"newline","count":2},{"type":"newline","count":1},{"type":"text","content":"The class is livid, everyone with"},{"type":"newline","count":1},{"type":"text","content":"their hands in their ears, sick of"},{"type":"newline","count":1},{"type":"text","content":"their retarded cry,"},{"type":"newline","count":1},{"type":"fragment","styles":[{"type":"italic"}],"children":[{"type":"text","content":"\"You don't need any more FUCKING"},{"type":"newline","count":1},{"type":"text","content":"marks, you have grade 9.\""}]},{"type":"newline","count":1},{"type":"text","content":"You're wasting time, It's just a mistake,"},{"type":"newline","count":1},{"type":"text","content":"You can't do this in your GCSEs,"},{"type":"newline","count":1},{"type":"text","content":"You're making us late, to break,"},{"type":"newline","count":1},{"type":"text","content":"and lunch, and life, and going home,"},{"type":"newline","count":1},{"type":"text","content":"So respectfully,"},{"type":"newline","count":1},{"type":"text","content":"Enjoy your grade,"},{"type":"newline","count":1},{"type":"text","content":"Control your emotions,"},{"type":"newline","count":1},{"type":"text","content":"Please, just ask after,"},{"type":"newline","count":1},{"type":"text","content":"It's just a number."},{"type":"newline","count":2},{"type":"text","content":"It makes no difference,"},{"type":"newline","count":1},{"type":"text","content":"Do we care? "},{"type":"fragment","styles":[{"type":"bold"}],"children":[{"type":"text","content":"NO!"}]},{"type":"newline","count":3}]},"pageStyle":"centre","pageBackground":""} \ No newline at end of file diff --git a/Volume-2/Idiots/suspended-in-h.json b/Volume-2/Idiots/suspended-in-h.json index 09eaf50..d71d1a9 100644 --- a/Volume-2/Idiots/suspended-in-h.json +++ b/Volume-2/Idiots/suspended-in-h.json @@ -1 +1 @@ -{"summary":"","tags":"","cWarning":true,"cWarningAdditions":"Content Warning: This poem is not for hypocrites.","poemName":"Suspended in H","poemAuthor":"Zekiah","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"Bring a knife, get in a fight,"},{"type":"newline","count":1},{"type":"text","content":"bully, beat up, call racial slurs,"},{"type":"newline","count":1},{"type":"text","content":"\"Dogs and cats are on Chinese plates\","},{"type":"newline","count":1},{"type":"text","content":"\"Muslims are good at crashing planes\""},{"type":"newline","count":1},{"type":"text","content":"All nothing compared to \"Hail H\"."},{"type":"newline","count":1},{"type":"text","content":"The most egregious statement to touch the page,"},{"type":"newline","count":1},{"type":"text","content":"or plan of any yearbook."},{"type":"newline","count":2},{"type":"text","content":"Hail Hunna,"},{"type":"newline","count":1},{"type":"text","content":"Hail and Snow,"},{"type":"newline","count":1},{"type":"text","content":"According to the teacher,"},{"type":"newline","count":1},{"type":"text","content":"all a NO. The only possible"},{"type":"newline","count":1},{"type":"text","content":"meaning of these words,"},{"type":"newline","count":1},{"type":"text","content":"could be \"Heil H-tler\""},{"type":"newline","count":2},{"type":"text","content":"And as the teacher read this out,"},{"type":"newline","count":1},{"type":"text","content":"She stood right up, and decided to shout."},{"type":"newline","count":1},{"type":"text","content":"\"Sieg Heil\", the teacher screamed,"},{"type":"newline","count":1},{"type":"text","content":"then shot her eyes around the class,"},{"type":"newline","count":2},{"type":"text","content":"\"You heard someone"},{"type":"newline","count":1},{"type":"text","content":"say something mildly"},{"type":"newline","count":1},{"type":"text","content":"offensive!\""},{"type":"newline","count":2},{"type":"text","content":"So you're the racist!"},{"type":"newline","count":2},{"type":"text","content":"W.R."},{"type":"newline","count":3}]},"pageStyle":"centre","pageBackground":""} \ No newline at end of file +{"summary":"","tags":"","cWarning":true,"cWarningAdditions":"Content Warning: This poem is not for hypocrites.","poemName":"Suspended in H","poemAuthor":"Zekiah","poemContent":{"type":"fragment","styles":[],"children":[{"type":"text","content":"Bring a knife, get in a fight,"},{"type":"newline","count":1},{"type":"text","content":"bully, beat up, call racial slurs,"},{"type":"newline","count":1},{"type":"text","content":"\"Dogs and cats are on Chinese plates\","},{"type":"newline","count":1},{"type":"text","content":"\"Muslims are good at crashing planes\""},{"type":"newline","count":1},{"type":"text","content":"All nothing compared to \"Hail H\"."},{"type":"newline","count":1},{"type":"text","content":"The most egregious statement to touch the page,"},{"type":"newline","count":1},{"type":"text","content":"or plan of any yearbook."},{"type":"newline","count":2},{"type":"text","content":"Hail Hunna,"},{"type":"newline","count":1},{"type":"text","content":"Hail and Snow,"},{"type":"newline","count":1},{"type":"text","content":"According to the teacher,"},{"type":"newline","count":1},{"type":"text","content":"all a NO. The only possible"},{"type":"newline","count":1},{"type":"text","content":"meaning of these words,"},{"type":"newline","count":1},{"type":"text","content":"could be \"Heil H-tler\""},{"type":"newline","count":2},{"type":"text","content":"And as the teacher read this out,"},{"type":"newline","count":1},{"type":"text","content":"She stood right up, and decided to shout."},{"type":"newline","count":1},{"type":"text","content":"\"Sieg Heil\", the teacher screamed,"},{"type":"newline","count":1},{"type":"text","content":"then shot her eyes around the class,"},{"type":"newline","count":2},{"type":"fragment","styles":[{"type":"italic"}],"children":[{"type":"text","content":"\"You heard someone"},{"type":"newline","count":1},{"type":"text","content":"say something mildly"},{"type":"newline","count":1},{"type":"text","content":"offensive!\""}]},{"type":"newline","count":2},{"type":"text","content":"So you're the racist!"},{"type":"newline","count":2},{"type":"fragment","styles":[{"type":"bold"}],"children":[{"type":"text","content":"W.R."}]},{"type":"newline","count":3}]},"pageStyle":"centre","pageBackground":""} \ No newline at end of file diff --git a/account.html b/account.html index e566227..70536b2 100644 --- a/account.html +++ b/account.html @@ -118,6 +118,8 @@ .editable { position: relative; + background-color: transparent; + color: var(--text-colour); border: none; } @@ -153,34 +155,34 @@ - - + +
@@ -216,7 +218,7 @@

Enter your email:

I'm

- ,

a writer

+ onclick="profileRole.showModal()">a writer

@username