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 @@
-