Compare commits
No commits in common. "main" and "feat/reports" have entirely different histories.
main
...
feat/repor
81 changed files with 397 additions and 2414 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -6,11 +6,7 @@ config.ini
|
|||
*.DotSettings.user
|
||||
proxy-config.json
|
||||
.DS_Store
|
||||
.idea/.idea.Foxnouns.NET/.idea/dataSources.xml
|
||||
.idea/.idea.Foxnouns.NET/.idea/sqldialects.xml
|
||||
|
||||
docker/config.ini
|
||||
docker/proxy-config.json
|
||||
docker/frontend.env
|
||||
|
||||
Foxnouns.DataMigrator/apps.json
|
||||
|
|
|
@ -4,31 +4,14 @@
|
|||
{
|
||||
"name": "run-prettier",
|
||||
"command": "pnpm",
|
||||
"args": [
|
||||
"prettier",
|
||||
"-w",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"Foxnouns.Frontend/**/*.ts",
|
||||
"Foxnouns.Frontend/**/*.json",
|
||||
"Foxnouns.Frontend/**/*.scss",
|
||||
"Foxnouns.Frontend/**/*.js",
|
||||
"Foxnouns.Frontend/**/*.svelte"
|
||||
],
|
||||
"cwd": "Foxnouns.Frontend/",
|
||||
"args": ["format"],
|
||||
"pathMode": "absolute"
|
||||
},
|
||||
{
|
||||
"name": "run-csharpier",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"**/*.cs"
|
||||
]
|
||||
"args": [ "csharpier", "${staged}" ],
|
||||
"include": [ "**/*.cs" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/internal/self-delete")]
|
||||
[Authorize("*")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DeleteUserController(DatabaseContext db, IClock clock, ILogger logger)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<DeleteUserController>();
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> DeleteSelfAsync()
|
||||
{
|
||||
_logger.Information(
|
||||
"User {UserId} has requested their account to be deleted",
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = true;
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant();
|
||||
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("force")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> ForceDeleteAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested an early full delete of their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
// This is the easiest way to force delete a user, don't judge me
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant() - Duration.FromDays(365);
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("undelete")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> UndeleteSelfAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
if (CurrentUser!.DeletedBy != null)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Your account has been suspended and can't be reactivated by yourself."
|
||||
);
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested to undelete their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = false;
|
||||
CurrentUser.DeletedAt = null;
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
|
@ -26,7 +26,6 @@ namespace Foxnouns.Backend.Controllers;
|
|||
|
||||
[Route("/api/internal/data-exports")]
|
||||
[Authorize("identify")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ExportsController(
|
||||
ILogger logger,
|
||||
|
@ -58,7 +57,7 @@ public class ExportsController(
|
|||
}
|
||||
|
||||
private string ExportUrl(Snowflake userId, string filename) =>
|
||||
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}/data-export.zip";
|
||||
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip";
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> QueueDataExportAsync()
|
||||
|
|
|
@ -22,7 +22,6 @@ using Foxnouns.Backend.Services;
|
|||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
|
@ -35,7 +34,7 @@ public class FlagsController(
|
|||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
[Authorize("user.read_flags")]
|
||||
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
|
||||
|
@ -65,7 +64,6 @@ public class FlagsController(
|
|||
var flag = new PrideFlag
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
UserId = CurrentUser!.Id,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
|
|
|
@ -38,8 +38,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
|
|||
{
|
||||
if (template.StartsWith("api/v2"))
|
||||
template = template["api/v2".Length..];
|
||||
else if (template.StartsWith("api/v1"))
|
||||
template = template["api/v1".Length..];
|
||||
template = PathVarRegex()
|
||||
.Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}`
|
||||
.Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}`
|
||||
|
|
|
@ -26,7 +26,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using NodaTime;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
|
@ -45,7 +44,7 @@ public class MembersController(
|
|||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
|
@ -54,7 +53,7 @@ public class MembersController(
|
|||
|
||||
[HttpGet("{memberRef}")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
|
@ -102,7 +101,6 @@ public class MembersController(
|
|||
var member = new Member
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
User = CurrentUser!,
|
||||
Name = req.Name,
|
||||
DisplayName = req.DisplayName,
|
||||
|
|
|
@ -18,7 +18,6 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -50,8 +49,6 @@ public class ReportsController(
|
|||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
User target = await db.ResolveUserAsync(id);
|
||||
|
||||
if (target.Id == CurrentUser!.Id)
|
||||
|
@ -99,7 +96,6 @@ public class ReportsController(
|
|||
TargetUserId = target.Id,
|
||||
TargetMemberId = null,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.User,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
@ -116,8 +112,6 @@ public class ReportsController(
|
|||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
Member target = await db.ResolveMemberAsync(id);
|
||||
|
||||
if (target.User.Id == CurrentUser!.Id)
|
||||
|
@ -164,7 +158,6 @@ public class ReportsController(
|
|||
TargetUserId = target.User.Id,
|
||||
TargetMemberId = target.Id,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.Member,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
|
@ -31,7 +17,7 @@ public class NotificationsController(
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetNotificationsAsync([FromQuery] bool all = false)
|
||||
{
|
||||
IQueryable<Notification> query = db.Notifications.Where(n => n.TargetId == CurrentUser!.Id);
|
||||
|
@ -45,7 +31,7 @@ public class NotificationsController(
|
|||
|
||||
[HttpPut("{id}/ack")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> AcknowledgeNotificationAsync(Snowflake id)
|
||||
{
|
||||
Notification? notification = await db.Notifications.FirstOrDefaultAsync(n =>
|
||||
|
|
|
@ -42,7 +42,7 @@ public class UsersController(
|
|||
|
||||
[HttpGet("{userRef}")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Limit(UsableBySuspendedUsers = true)]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
|
@ -222,7 +222,7 @@ public class UsersController(
|
|||
.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key))
|
||||
.ToDictionary();
|
||||
|
||||
foreach (CustomPreferenceUpdateRequest r in req)
|
||||
foreach (CustomPreferenceUpdateRequest? r in req)
|
||||
{
|
||||
if (r.Id != null && preferences.ContainsKey(r.Id.Value))
|
||||
{
|
||||
|
@ -233,7 +233,6 @@ public class UsersController(
|
|||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = preferences[r.Id.Value].LegacyId,
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -245,7 +244,6 @@ public class UsersController(
|
|||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = Guid.NewGuid(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.V1;
|
||||
|
||||
[Route("/api/v1")]
|
||||
public class V1ReadController(
|
||||
UsersV1Service usersV1Service,
|
||||
MembersV1Service membersV1Service,
|
||||
DatabaseContext db
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("users/@me")]
|
||||
[Authorize("identify")]
|
||||
public async Task<IActionResult> GetMeAsync(CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync("@me", CurrentToken, ct);
|
||||
return Ok(await usersV1Service.RenderCurrentUserAsync(user, ct));
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}")]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await usersV1Service.RenderUserAsync(
|
||||
user,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("members/{id}")]
|
||||
public async Task<IActionResult> GetMemberAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(id, ct);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members")]
|
||||
public async Task<IActionResult> GetUserMembersAsync(
|
||||
string userRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
List<Member> members = await db
|
||||
.Members.Where(m => m.UserId == user.Id)
|
||||
.OrderBy(m => m.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<MemberResponse> responses = [];
|
||||
foreach (Member member in members)
|
||||
{
|
||||
responses.Add(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
user,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(responses);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members/{memberRef}")]
|
||||
public async Task<IActionResult> GetUserMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(
|
||||
userRef,
|
||||
memberRef,
|
||||
CurrentToken,
|
||||
ct
|
||||
);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -108,12 +108,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
.HasFilter("fediverse_application_id IS NULL")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<AuditLogEntry>()
|
||||
.HasOne(e => e.Report)
|
||||
.WithOne(e => e.AuditLogEntry)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<User>().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()");
|
||||
modelBuilder.Entity<User>().Property(u => u.Fields).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.Names).HasColumnType("jsonb");
|
||||
|
@ -139,26 +133,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
|||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!)
|
||||
.HasName("find_free_member_sid");
|
||||
|
||||
// Indexes for legacy IDs for APIv1
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => m.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<PrideFlag>().HasIndex(f => f.LegacyId).IsUnique();
|
||||
|
||||
// a UUID is not an xid, but this should always be set by the application anyway.
|
||||
// we're just setting it here to shut EFCore up because squashing migrations is for nerds
|
||||
modelBuilder
|
||||
.Entity<User>()
|
||||
.Property(u => u.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<Member>()
|
||||
.Property(m => m.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<PrideFlag>()
|
||||
.Property(f => f.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241217195351_AddFediAppForceRefresh")]
|
||||
public partial class AddFediAppForceRefresh : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<Dictionary<string, string>>(
|
||||
name: "localization_params",
|
||||
table: "notifications",
|
||||
type: "hstore",
|
||||
nullable: false,
|
||||
oldClrType: typeof(Dictionary<string, string>),
|
||||
oldType: "hstore",
|
||||
oldNullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "force_refresh",
|
||||
table: "fediverse_applications",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "force_refresh", table: "fediverse_applications");
|
||||
|
||||
migrationBuilder.AlterColumn<Dictionary<string, string>>(
|
||||
name: "localization_params",
|
||||
table: "notifications",
|
||||
type: "hstore",
|
||||
nullable: true,
|
||||
oldClrType: typeof(Dictionary<string, string>),
|
||||
oldType: "hstore"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218195457_AddContextToReports")]
|
||||
public partial class AddContextToReports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "context",
|
||||
table: "reports",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "context", table: "reports");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218201855_MakeAuditLogReportsNullable")]
|
||||
public partial class MakeAuditLogReportsNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id"
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241225155818_AddLegacyIds")]
|
||||
public partial class AddLegacyIds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "pride_flags",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_legacy_id",
|
||||
table: "users",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_pride_flags_legacy_id",
|
||||
table: "pride_flags",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_members_legacy_id",
|
||||
table: "members",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(name: "ix_users_legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_pride_flags_legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_members_legacy_id", table: "members");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "members");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -113,7 +113,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
|
@ -217,10 +216,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<bool>("ForceRefresh")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("force_refresh");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
@ -254,13 +249,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
|
@ -299,10 +287,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
@ -358,7 +342,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnName("localization_key");
|
||||
|
||||
b.Property<Dictionary<string, string>>("LocalizationParams")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore")
|
||||
.HasColumnName("localization_params");
|
||||
|
||||
|
@ -397,13 +380,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
|
@ -416,10 +392,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_pride_flags");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_pride_flags_legacy_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_pride_flags_user_id");
|
||||
|
||||
|
@ -432,10 +404,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
@ -443,7 +411,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Property<long>("ReporterId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("reporter_id");
|
||||
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
@ -604,13 +572,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_sid_reroll");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
|
@ -666,10 +627,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
@ -713,9 +670,8 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
|
||||
.WithOne("AuditLogEntry")
|
||||
.HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.WithMany()
|
||||
.HasForeignKey("ReportId")
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
|
@ -878,11 +834,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Navigation("AuditLogEntry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ public class FediverseApplication : BaseModel
|
|||
public required string ClientId { get; set; }
|
||||
public required string ClientSecret { get; set; }
|
||||
public required FediverseInstanceType InstanceType { get; set; }
|
||||
public bool ForceRefresh { get; set; }
|
||||
}
|
||||
|
||||
public enum FediverseInstanceType
|
||||
|
|
|
@ -18,7 +18,6 @@ public class Member : BaseModel
|
|||
{
|
||||
public required string Name { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
|
|
|
@ -17,7 +17,6 @@ namespace Foxnouns.Backend.Database.Models;
|
|||
public class PrideFlag : BaseModel
|
||||
{
|
||||
public required Snowflake UserId { get; init; }
|
||||
public required string LegacyId { get; init; }
|
||||
|
||||
// A null hash means the flag hasn't been processed yet.
|
||||
public string? Hash { get; set; }
|
||||
|
|
|
@ -29,12 +29,9 @@ public class Report : BaseModel
|
|||
|
||||
public ReportStatus Status { get; set; }
|
||||
public ReportReason Reason { get; init; }
|
||||
public string? Context { get; init; }
|
||||
|
||||
public ReportTargetType TargetType { get; init; }
|
||||
public string? TargetSnapshot { get; init; }
|
||||
|
||||
public AuditLogEntry? AuditLogEntry { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
|
|
|
@ -25,7 +25,6 @@ public class User : BaseModel
|
|||
{
|
||||
public required string Username { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? MemberTitle { get; set; }
|
||||
|
@ -70,8 +69,6 @@ public class User : BaseModel
|
|||
// This type is generally serialized directly, so the converter is applied here.
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public PreferenceSize Size { get; set; }
|
||||
|
||||
public Guid LegacyId { get; init; } = Guid.NewGuid();
|
||||
}
|
||||
|
||||
public static readonly Duration DeleteAfter = Duration.FromDays(30);
|
||||
|
|
|
@ -57,7 +57,7 @@ public record NotificationResponse(
|
|||
|
||||
public record AuditLogEntity(Snowflake Id, string Username);
|
||||
|
||||
public record CreateReportRequest(ReportReason Reason, string? Context = null);
|
||||
public record CreateReportRequest(ReportReason Reason);
|
||||
|
||||
public record IgnoreReportRequest(string? Reason = null);
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ public record UserResponse(
|
|||
IEnumerable<FieldEntry> Names,
|
||||
IEnumerable<Pronoun> Pronouns,
|
||||
IEnumerable<Field> Fields,
|
||||
Dictionary<Snowflake, CustomPreferenceResponse> CustomPreferences,
|
||||
Dictionary<Snowflake, User.CustomPreference> CustomPreferences,
|
||||
IEnumerable<PrideFlagResponse> Flags,
|
||||
int? UtcOffset,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role,
|
||||
|
@ -52,14 +52,6 @@ public record UserResponse(
|
|||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted
|
||||
);
|
||||
|
||||
public record CustomPreferenceResponse(
|
||||
string Icon,
|
||||
string Tooltip,
|
||||
bool Muted,
|
||||
bool Favourite,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] PreferenceSize Size
|
||||
);
|
||||
|
||||
public record AuthMethodResponse(
|
||||
Snowflake Id,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Foxnouns.Backend.Dto.V1;
|
||||
|
||||
public record PartialMember(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns
|
||||
);
|
||||
|
||||
public record MemberResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialUser User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted
|
||||
);
|
||||
|
||||
public record PartialUser(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Avatar,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences
|
||||
);
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Dto.V1;
|
||||
|
||||
public record UserResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? MemberTitle,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialMember[] Members,
|
||||
int? UtcOffset,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences
|
||||
);
|
||||
|
||||
public record CurrentUserResponse(
|
||||
string Id,
|
||||
Snowflake IdNew,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
string? MemberTitle,
|
||||
string? Avatar,
|
||||
string[] Links,
|
||||
FieldEntry[] Names,
|
||||
PronounEntry[] Pronouns,
|
||||
ProfileField[] Fields,
|
||||
PrideFlag[] Flags,
|
||||
PartialMember[] Members,
|
||||
int? UtcOffset,
|
||||
Dictionary<Guid, CustomPreference> CustomPreferences,
|
||||
Instant CreatedAt,
|
||||
string? Timezone,
|
||||
bool IsAdmin,
|
||||
bool ListPrivate,
|
||||
Instant LastSidReroll,
|
||||
string? Discord,
|
||||
string? DiscordUsername,
|
||||
string? Google,
|
||||
string? GoogleUsername,
|
||||
string? Tumblr,
|
||||
string? TumblrUsername,
|
||||
string? Fediverse,
|
||||
string? FediverseUsername,
|
||||
string? FediverseInstance
|
||||
);
|
||||
|
||||
public record CustomPreference(
|
||||
string Icon,
|
||||
string Tooltip,
|
||||
[property: JsonConverter(typeof(StringEnumConverter), typeof(SnakeCaseNamingStrategy))]
|
||||
PreferenceSize Size,
|
||||
bool Muted,
|
||||
bool Favourite
|
||||
);
|
||||
|
||||
public record ProfileField(string Name, FieldEntry[] Entries)
|
||||
{
|
||||
public static ProfileField FromField(
|
||||
Field field,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) => new(field.Name, FieldEntry.FromEntries(field.Entries, customPreferences));
|
||||
|
||||
public static ProfileField[] FromFields(
|
||||
IEnumerable<Field> fields,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) => fields.Select(f => FromField(f, customPreferences)).ToArray();
|
||||
}
|
||||
|
||||
public record FieldEntry(string Value, string Status)
|
||||
{
|
||||
public static FieldEntry[] FromEntries(
|
||||
IEnumerable<Foxnouns.Backend.Database.Models.FieldEntry> entries,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
entries
|
||||
.Select(e => new FieldEntry(
|
||||
e.Value,
|
||||
V1Utils.TranslateStatus(e.Status, customPreferences)
|
||||
))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public record PronounEntry(string Pronouns, string? DisplayText, string Status)
|
||||
{
|
||||
public static PronounEntry[] FromPronouns(
|
||||
IEnumerable<Pronoun> pronouns,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
pronouns
|
||||
.Select(p => new PronounEntry(
|
||||
p.Value,
|
||||
p.DisplayText,
|
||||
V1Utils.TranslateStatus(p.Status, customPreferences)
|
||||
))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description);
|
|
@ -19,7 +19,6 @@ using Foxnouns.Backend.Jobs;
|
|||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio;
|
||||
using NodaTime;
|
||||
|
@ -128,10 +127,7 @@ public static class WebApplicationExtensions
|
|||
.AddTransient<MemberAvatarUpdateInvocable>()
|
||||
.AddTransient<UserAvatarUpdateInvocable>()
|
||||
.AddTransient<CreateFlagInvocable>()
|
||||
.AddTransient<CreateDataExportInvocable>()
|
||||
// Legacy services
|
||||
.AddScoped<UsersV1Service>()
|
||||
.AddScoped<MembersV1Service>();
|
||||
.AddTransient<CreateDataExportInvocable>();
|
||||
|
||||
if (!config.Logging.EnableMetrics)
|
||||
services.AddHostedService<BackgroundMetricsCollectionService>();
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0"/>
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageReference Include="Yort.Xid.Net" Version="2.0.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
||||
|
|
|
@ -220,5 +220,5 @@ public class CreateDataExportInvocable(
|
|||
}
|
||||
|
||||
private static string ExportPath(Snowflake userId, string b64) =>
|
||||
$"data-exports/{userId}/{b64}/data-export.zip";
|
||||
$"data-exports/{userId}/{b64}.zip";
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ public class LimitMiddleware : IMiddleware
|
|||
return;
|
||||
}
|
||||
|
||||
if (token?.User.Deleted == true && !attribute.UsableByDeletedUsers)
|
||||
if (token?.User.Deleted == true && !attribute.UsableBySuspendedUsers)
|
||||
throw new ApiError.Forbidden("Deleted users cannot access this endpoint.");
|
||||
|
||||
if (attribute.RequireAdmin && token?.User.Role != UserRole.Admin)
|
||||
|
@ -62,7 +62,7 @@ public class LimitMiddleware : IMiddleware
|
|||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class LimitAttribute : Attribute
|
||||
{
|
||||
public bool UsableByDeletedUsers { get; init; }
|
||||
public bool UsableBySuspendedUsers { get; init; }
|
||||
public bool RequireAdmin { get; init; }
|
||||
public bool RequireModerator { get; init; }
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ using Foxnouns.Backend.Utils;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
|
@ -71,7 +70,6 @@ public class AuthService(
|
|||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
@ -118,7 +116,6 @@ public class AuthService(
|
|||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
|
|
@ -58,7 +58,7 @@ public partial class FediverseAuthService
|
|||
)
|
||||
{
|
||||
FediverseApplication app = await GetApplicationAsync(instance);
|
||||
return await GenerateAuthUrlAsync(app, forceRefresh || app.ForceRefresh, state);
|
||||
return await GenerateAuthUrlAsync(app, forceRefresh, state);
|
||||
}
|
||||
|
||||
// thank you, gargron and syuilo, for agreeing on a name for *once* in your lives,
|
||||
|
|
|
@ -128,5 +128,5 @@ public class DataCleanupService(
|
|||
}
|
||||
|
||||
private static string ExportPath(Snowflake userId, string b64) =>
|
||||
$"data-exports/{userId}/{b64}/data-export.zip";
|
||||
$"data-exports/{userId}/{b64}.zip";
|
||||
}
|
||||
|
|
|
@ -103,8 +103,7 @@ public class UserRendererService(
|
|||
user.Names,
|
||||
user.Pronouns,
|
||||
user.Fields,
|
||||
user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value)))
|
||||
.ToDictionary(),
|
||||
user.CustomPreferences,
|
||||
flags.Select(f => RenderPrideFlag(f.PrideFlag)),
|
||||
utcOffset,
|
||||
user.Role,
|
||||
|
@ -131,14 +130,6 @@ public class UserRendererService(
|
|||
: a.RemoteUsername
|
||||
);
|
||||
|
||||
public static CustomPreferenceResponse RenderCustomPreference(User.CustomPreference pref) =>
|
||||
new(pref.Icon, pref.Tooltip, pref.Muted, pref.Favourite, pref.Size);
|
||||
|
||||
public static Dictionary<Snowflake, CustomPreferenceResponse> RenderCustomPreferences(
|
||||
User user
|
||||
) =>
|
||||
user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))).ToDictionary();
|
||||
|
||||
public PartialUser RenderPartialUser(User user) =>
|
||||
new(
|
||||
user.Id,
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
|
||||
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
|
||||
public class MembersV1Service(DatabaseContext db, UsersV1Service usersV1Service)
|
||||
{
|
||||
public async Task<Member> ResolveMemberAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
Member? member;
|
||||
if (Snowflake.TryParse(id, out Snowflake? sf))
|
||||
{
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == sf && !m.User.Deleted, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
}
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.LegacyId == id && !m.User.Deleted, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound);
|
||||
}
|
||||
|
||||
public async Task<Member> ResolveMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, token, ct);
|
||||
|
||||
Member? member;
|
||||
if (Snowflake.TryParse(memberRef, out Snowflake? sf))
|
||||
{
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == sf && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
}
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.LegacyId == memberRef && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == user.Id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
|
||||
throw new ApiError.NotFound(
|
||||
"No member with that ID or name found.",
|
||||
ErrorCode.MemberNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<MemberResponse> RenderMemberAsync(
|
||||
Member m,
|
||||
Token? token = default,
|
||||
User? user = null,
|
||||
bool renderFlags = true,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
user ??= m.User;
|
||||
bool renderUnlisted = m.UserId == token?.UserId;
|
||||
|
||||
List<MemberFlag> flags = renderFlags
|
||||
? await db.MemberFlags.Where(f => f.MemberId == m.Id).OrderBy(f => f.Id).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
return new MemberResponse(
|
||||
m.LegacyId,
|
||||
m.Id,
|
||||
m.Sid,
|
||||
m.Name,
|
||||
m.DisplayName,
|
||||
m.Bio,
|
||||
m.Avatar,
|
||||
m.Links,
|
||||
Names: FieldEntry.FromEntries(m.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(m.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(m.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
User: UsersV1Service.RenderPartialUser(user),
|
||||
Unlisted: renderUnlisted ? m.Unlisted : null
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
|
||||
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
|
||||
public class UsersV1Service(DatabaseContext db)
|
||||
{
|
||||
public async Task<User> ResolveUserAsync(
|
||||
string userRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
if (userRef == "@me")
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
throw new ApiError.Unauthorized(
|
||||
"This endpoint requires an authenticated user.",
|
||||
ErrorCode.AuthenticationRequired
|
||||
);
|
||||
}
|
||||
|
||||
return await db.Users.FirstAsync(u => u.Id == token.UserId, ct);
|
||||
}
|
||||
|
||||
User? user;
|
||||
if (Snowflake.TryParse(userRef, out Snowflake? sf))
|
||||
{
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.Id == sf && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
}
|
||||
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.LegacyId == userRef && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
|
||||
user = await db.Users.FirstOrDefaultAsync(u => u.Username == userRef && !u.Deleted, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
|
||||
throw new ApiError.NotFound(
|
||||
"No user with that ID or username found.",
|
||||
ErrorCode.UserNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<UserResponse> RenderUserAsync(
|
||||
User user,
|
||||
Token? token = null,
|
||||
bool renderMembers = true,
|
||||
bool renderFlags = true,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
bool isSelfUser = user.Id == token?.UserId;
|
||||
renderMembers = renderMembers && (isSelfUser || !user.ListHidden);
|
||||
|
||||
// Only fetch members if we're rendering members (duh)
|
||||
List<Member> members = renderMembers
|
||||
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
List<UserFlag> flags = renderFlags
|
||||
? await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
int? utcOffset = null;
|
||||
if (
|
||||
user.Timezone != null
|
||||
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz)
|
||||
)
|
||||
{
|
||||
utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds;
|
||||
}
|
||||
|
||||
return new UserResponse(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Sid,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Bio,
|
||||
user.MemberTitle,
|
||||
user.Avatar,
|
||||
user.Links,
|
||||
Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(),
|
||||
utcOffset,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<CurrentUserResponse> RenderCurrentUserAsync(
|
||||
User user,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
List<Member> members = await db
|
||||
.Members.Where(m => m.UserId == user.Id)
|
||||
.OrderBy(m => m.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<UserFlag> flags = await db
|
||||
.UserFlags.Where(f => f.UserId == user.Id)
|
||||
.OrderBy(f => f.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
int? utcOffset = null;
|
||||
if (
|
||||
user.Timezone != null
|
||||
&& TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz)
|
||||
)
|
||||
{
|
||||
utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds;
|
||||
}
|
||||
|
||||
List<AuthMethod> authMethods = await db
|
||||
.AuthMethods.Include(a => a.FediverseApplication)
|
||||
.Where(a => a.UserId == user.Id)
|
||||
.OrderBy(a => a.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
AuthMethod? discord = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Discord);
|
||||
AuthMethod? google = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Google);
|
||||
AuthMethod? tumblr = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Tumblr);
|
||||
AuthMethod? fediverse = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Fediverse);
|
||||
|
||||
return new CurrentUserResponse(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Sid,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Bio,
|
||||
user.MemberTitle,
|
||||
user.Avatar,
|
||||
user.Links,
|
||||
Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
|
||||
Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences),
|
||||
Flags: flags
|
||||
.Where(f => f.PrideFlag.Hash != null)
|
||||
.Select(f => new PrideFlag(
|
||||
f.PrideFlag.LegacyId,
|
||||
f.PrideFlag.Id,
|
||||
f.PrideFlag.Hash!,
|
||||
f.PrideFlag.Name,
|
||||
f.PrideFlag.Description
|
||||
))
|
||||
.ToArray(),
|
||||
Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(),
|
||||
utcOffset,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences),
|
||||
user.Id.Time,
|
||||
user.Timezone,
|
||||
user.Role is UserRole.Admin,
|
||||
user.ListHidden,
|
||||
user.LastSidReroll,
|
||||
discord?.RemoteId,
|
||||
discord?.RemoteUsername,
|
||||
google?.RemoteId,
|
||||
google?.RemoteUsername,
|
||||
tumblr?.RemoteId,
|
||||
tumblr?.RemoteUsername,
|
||||
fediverse?.RemoteId,
|
||||
fediverse?.RemoteUsername,
|
||||
fediverse?.FediverseApplication?.Domain
|
||||
);
|
||||
}
|
||||
|
||||
private static Dictionary<Guid, CustomPreference> RenderCustomPreferences(
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
customPreferences
|
||||
.Select(x =>
|
||||
(
|
||||
x.Value.LegacyId,
|
||||
new CustomPreference(
|
||||
x.Value.Icon,
|
||||
x.Value.Tooltip,
|
||||
x.Value.Size,
|
||||
x.Value.Muted,
|
||||
x.Value.Favourite
|
||||
)
|
||||
)
|
||||
)
|
||||
.ToDictionary();
|
||||
|
||||
private static PartialMember RenderPartialMember(
|
||||
Member m,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
) =>
|
||||
new(
|
||||
m.LegacyId,
|
||||
m.Id,
|
||||
m.Sid,
|
||||
m.Name,
|
||||
m.DisplayName,
|
||||
m.Bio,
|
||||
m.Avatar,
|
||||
m.Links,
|
||||
Names: FieldEntry.FromEntries(m.Names, customPreferences),
|
||||
Pronouns: PronounEntry.FromPronouns(m.Pronouns, customPreferences)
|
||||
);
|
||||
|
||||
public static PartialUser RenderPartialUser(User user) =>
|
||||
new(
|
||||
user.LegacyId,
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
|
||||
);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
||||
namespace Foxnouns.Backend.Services.V1;
|
||||
|
||||
public static class V1Utils
|
||||
{
|
||||
public static string TranslateStatus(
|
||||
string status,
|
||||
Dictionary<Snowflake, User.CustomPreference> customPreferences
|
||||
)
|
||||
{
|
||||
if (!Snowflake.TryParse(status, out Snowflake? sf))
|
||||
return status;
|
||||
|
||||
return customPreferences.TryGetValue(sf.Value, out User.CustomPreference? cf)
|
||||
? cf.LegacyId.ToString()
|
||||
: "unknown";
|
||||
}
|
||||
}
|
|
@ -196,13 +196,6 @@ public static partial class ValidationUtils
|
|||
};
|
||||
}
|
||||
|
||||
public const int MaximumReportContextLength = 512;
|
||||
|
||||
public static ValidationError? ValidateReportContext(string? context) =>
|
||||
context?.Length > MaximumReportContextLength
|
||||
? ValidationError.GenericValidationError("Avatar is too large", null)
|
||||
: null;
|
||||
|
||||
public const int MinimumPasswordLength = 12;
|
||||
public const int MaximumPasswordLength = 1024;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
; The host the server will listen on
|
||||
Host = localhost
|
||||
; The port the server will listen on
|
||||
Port = 6000
|
||||
Port = 5000
|
||||
; The base *external* URL
|
||||
BaseUrl = https://pronouns.localhost
|
||||
; The base URL for media, without a trailing slash. This must be publicly accessible.
|
||||
|
|
|
@ -293,12 +293,6 @@
|
|||
"System.Runtime": "4.3.1"
|
||||
}
|
||||
},
|
||||
"Yort.Xid.Net": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.1, )",
|
||||
"resolved": "2.0.1",
|
||||
"contentHash": "+3sNX7/RKSKheVuMz9jtWLazD+R4PXpx8va2d9SdDgvKOhETbEb0VYis8K/fD1qm/qOQT57LadToSpzReGMZlw=="
|
||||
},
|
||||
"BouncyCastle.Cryptography": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.0",
|
||||
|
|
|
@ -6,7 +6,6 @@ using Foxnouns.Backend.Extensions;
|
|||
using Foxnouns.DataMigrator.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql;
|
||||
using Serilog;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
|
@ -23,12 +22,6 @@ internal class Program
|
|||
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen)
|
||||
.CreateLogger();
|
||||
|
||||
var minUserId = new Snowflake(0);
|
||||
if (args.Length > 0)
|
||||
minUserId = ulong.Parse(args[0]);
|
||||
|
||||
Log.Information("Starting migration from user ID {MinUserId}", minUserId);
|
||||
|
||||
Config config =
|
||||
new ConfigurationBuilder()
|
||||
.AddConfiguration()
|
||||
|
@ -42,30 +35,11 @@ internal class Program
|
|||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
Dictionary<int, Snowflake> appIds;
|
||||
if (minUserId == new Snowflake(0))
|
||||
{
|
||||
Log.Information("Migrating applications");
|
||||
appIds = await MigrateAppsAsync(conn, context);
|
||||
|
||||
string appJson = JsonConvert.SerializeObject(appIds);
|
||||
await File.WriteAllTextAsync("apps.json", appJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information(
|
||||
"Not the first migration, reading application IDs from {Filename}",
|
||||
"apps.json"
|
||||
);
|
||||
|
||||
string appJson = await File.ReadAllTextAsync("apps.json");
|
||||
appIds =
|
||||
JsonConvert.DeserializeObject<Dictionary<int, Snowflake>>(appJson)
|
||||
?? throw new Exception("invalid apps.json file");
|
||||
}
|
||||
Log.Information("Migrating applications");
|
||||
Dictionary<int, Snowflake> appIds = await MigrateAppsAsync(conn, context);
|
||||
|
||||
Log.Information("Migrating users");
|
||||
List<GoUser> users = await Queries.GetUsersAsync(conn, minUserId);
|
||||
List<GoUser> users = await Queries.GetUsersAsync(conn);
|
||||
List<GoUserField> userFields = await Queries.GetUserFieldsAsync(conn);
|
||||
List<GoMemberField> memberFields = await Queries.GetMemberFieldsAsync(conn);
|
||||
List<GoPrideFlag> prideFlags = await Queries.GetUserFlagsAsync(conn);
|
||||
|
@ -96,12 +70,6 @@ internal class Program
|
|||
|
||||
await context.SaveChangesAsync();
|
||||
Log.Information("Migration complete!");
|
||||
Log.Information(
|
||||
"Migrated {Count} users, last user was {UserId}. Complete? {Complete}",
|
||||
users.Count,
|
||||
users.Last().SnowflakeId,
|
||||
users.Count != 1000
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<int, Snowflake>> MigrateAppsAsync(
|
||||
|
@ -124,7 +92,6 @@ internal class Program
|
|||
ClientId = app.ClientId,
|
||||
ClientSecret = app.ClientSecret,
|
||||
InstanceType = app.TypeToEnum(),
|
||||
ForceRefresh = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,13 +13,8 @@ public static class Queries
|
|||
public static async Task<List<GoFediverseApp>> GetFediverseAppsAsync(NpgsqlConnection conn) =>
|
||||
(await conn.QueryAsync<GoFediverseApp>("select * from fediverse_apps")).ToList();
|
||||
|
||||
public static async Task<List<GoUser>> GetUsersAsync(NpgsqlConnection conn, Snowflake minId) =>
|
||||
(
|
||||
await conn.QueryAsync<GoUser>(
|
||||
"select * from users where snowflake_id > @Id order by snowflake_id limit 1000",
|
||||
new { Id = minId.Value }
|
||||
)
|
||||
).ToList();
|
||||
public static async Task<List<GoUser>> GetUsersAsync(NpgsqlConnection conn) =>
|
||||
(await conn.QueryAsync<GoUser>("select * from users order by id")).ToList();
|
||||
|
||||
public static async Task<List<GoUserField>> GetUserFieldsAsync(NpgsqlConnection conn) =>
|
||||
(await conn.QueryAsync<GoUserField>("select * from user_fields order by id")).ToList();
|
||||
|
|
|
@ -39,7 +39,6 @@ public class UserMigrator(
|
|||
_user = new User
|
||||
{
|
||||
Id = goUser.SnowflakeId,
|
||||
LegacyId = goUser.Id,
|
||||
Username = goUser.Username,
|
||||
DisplayName = goUser.DisplayName,
|
||||
Bio = goUser.Bio,
|
||||
|
@ -140,7 +139,6 @@ public class UserMigrator(
|
|||
new PrideFlag
|
||||
{
|
||||
Id = flag.SnowflakeId,
|
||||
LegacyId = flag.Id,
|
||||
UserId = _user!.Id,
|
||||
Hash = flag.Hash,
|
||||
Name = flag.Name,
|
||||
|
@ -192,7 +190,6 @@ public class UserMigrator(
|
|||
UserId = _user!.Id,
|
||||
Name = goMember.Name,
|
||||
Sid = goMember.Sid,
|
||||
LegacyId = goMember.Id,
|
||||
DisplayName = goMember.DisplayName,
|
||||
Bio = goMember.Bio,
|
||||
Avatar = goMember.Avatar,
|
||||
|
@ -238,7 +235,6 @@ public class UserMigrator(
|
|||
"small" => PreferenceSize.Small,
|
||||
_ => PreferenceSize.Normal,
|
||||
},
|
||||
LegacyId = new Guid(id),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Example .env file--DO NOT EDIT, copy to .env or .env.local then edit
|
||||
# Example .env file--DO NOT EDIT
|
||||
PUBLIC_LANGUAGE=en
|
||||
PUBLIC_BASE_URL=https://pronouns.cc
|
||||
PUBLIC_SHORT_URL=https://prns.cc
|
||||
PUBLIC_API_BASE=https://pronouns.cc/api
|
||||
PRIVATE_API_HOST=http://localhost:5003/api
|
||||
PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api
|
||||
PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.10",
|
||||
"@sveltejs/kit": "^2.12.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@sveltejs/kit": "^2.11.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
|
@ -28,13 +28,13 @@
|
|||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"sass": "^1.83.0",
|
||||
"svelte": "^5.14.3",
|
||||
"svelte": "^5.13.0",
|
||||
"svelte-bootstrap-icons": "^3.1.1",
|
||||
"svelte-check": "^4.1.1",
|
||||
"sveltekit-i18n": "^2.4.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"vite": "^6.0.3"
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
|
||||
"dependencies": {
|
||||
|
|
File diff suppressed because it is too large
Load diff
9
Foxnouns.Frontend/src/app.d.ts
vendored
9
Foxnouns.Frontend/src/app.d.ts
vendored
|
@ -1,16 +1,7 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
|
||||
import type { ErrorCode } from "$api/error";
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Error {
|
||||
message: string;
|
||||
status: number;
|
||||
code: ErrorCode;
|
||||
id: string;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
|
|
|
@ -64,11 +64,3 @@
|
|||
max-width: 200px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.big-footer {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: bootstrap.shade-color(bootstrap.$dark, 20%);
|
||||
}
|
||||
|
||||
background-color: bootstrap.shade-color(bootstrap.$light, 5%);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import ApiError, { ErrorCode } from "$api/error";
|
||||
import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private";
|
||||
import { PUBLIC_API_BASE } from "$env/static/public";
|
||||
import log from "$lib/log";
|
||||
import type { HandleFetch, HandleServerError } from "@sveltejs/kit";
|
||||
import type { HandleFetch } from "@sveltejs/kit";
|
||||
|
||||
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
||||
if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) {
|
||||
|
@ -13,24 +11,3 @@ export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
|||
|
||||
return await fetch(request);
|
||||
};
|
||||
|
||||
export const handleError: HandleServerError = async ({ error, status, message }) => {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
return {
|
||||
id,
|
||||
status: error.raw?.status || status,
|
||||
message: error.raw?.message || "Unknown error",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 400 && status <= 499) {
|
||||
return { id, status, message, code: ErrorCode.GenericApiError };
|
||||
}
|
||||
|
||||
log.error("[%s] error in handler:", id, error);
|
||||
|
||||
return { id, status, message, code: ErrorCode.InternalServerError };
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|||
/**
|
||||
* Optional arguments for a request. `load` and `action` functions should always pass `fetch` and `cookies`.
|
||||
*/
|
||||
export type RequestArgs<T> = {
|
||||
export type RequestArgs = {
|
||||
/**
|
||||
* The token for this request. Where possible, `cookies` should be passed instead.
|
||||
* Will override `cookies` if both are passed.
|
||||
|
@ -23,7 +23,7 @@ export type RequestArgs<T> = {
|
|||
/**
|
||||
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
|
||||
*/
|
||||
body?: T;
|
||||
body?: unknown;
|
||||
/**
|
||||
* The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests.
|
||||
*/
|
||||
|
@ -41,10 +41,10 @@ export type RequestArgs<T> = {
|
|||
* @param args Optional arguments to the request function.
|
||||
* @returns A Response object.
|
||||
*/
|
||||
export async function baseRequest<T = unknown>(
|
||||
export async function baseRequest(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<T> = {},
|
||||
args: RequestArgs = {},
|
||||
): Promise<Response> {
|
||||
const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME);
|
||||
|
||||
|
@ -72,11 +72,11 @@ export async function baseRequest<T = unknown>(
|
|||
* @param args Optional arguments to the request function.
|
||||
* @returns The response deserialized as `T`.
|
||||
*/
|
||||
export async function apiRequest<TResponse, TRequest = unknown>(
|
||||
export async function apiRequest<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<TRequest> = {},
|
||||
): Promise<TResponse> {
|
||||
args: RequestArgs = {},
|
||||
): Promise<T> {
|
||||
const resp = await baseRequest(method, path, args);
|
||||
|
||||
if (resp.status < 200 || resp.status > 299) {
|
||||
|
@ -84,7 +84,7 @@ export async function apiRequest<TResponse, TRequest = unknown>(
|
|||
if ("code" in err) throw new ApiError(err);
|
||||
else throw new ApiError();
|
||||
}
|
||||
return (await resp.json()) as TResponse;
|
||||
return (await resp.json()) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,10 +94,10 @@ export async function apiRequest<TResponse, TRequest = unknown>(
|
|||
* @param args Optional arguments to the request function.
|
||||
* @param enforce204 Whether to throw an error on a non-204 status code.
|
||||
*/
|
||||
export async function fastRequest<T = unknown>(
|
||||
export async function fastRequest(
|
||||
method: Method,
|
||||
path: string,
|
||||
args: RequestArgs<T> = {},
|
||||
args: RequestArgs = {},
|
||||
enforce204: boolean = false,
|
||||
): Promise<void> {
|
||||
const resp = await baseRequest(method, path, args);
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
export type CreateReportRequest = {
|
||||
reason: ReportReason;
|
||||
context: string | null;
|
||||
};
|
||||
|
||||
export enum ReportReason {
|
||||
Totalitarianism = "TOTALITARIANISM",
|
||||
HateSpeech = "HATE_SPEECH",
|
||||
Racism = "RACISM",
|
||||
Homophobia = "HOMOPHOBIA",
|
||||
Transphobia = "TRANSPHOBIA",
|
||||
Queerphobia = "QUEERPHOBIA",
|
||||
Exclusionism = "EXCLUSIONISM",
|
||||
Sexism = "SEXISM",
|
||||
Ableism = "ABLEISM",
|
||||
ChildPornography = "CHILD_PORNOGRAPHY",
|
||||
PedophiliaAdvocacy = "PEDOPHILIA_ADVOCACY",
|
||||
Harassment = "HARASSMENT",
|
||||
Impersonation = "IMPERSONATION",
|
||||
Doxxing = "DOXXING",
|
||||
EncouragingSelfHarm = "ENCOURAGING_SELF_HARM",
|
||||
Spam = "SPAM",
|
||||
Trolling = "TROLLING",
|
||||
Advertisement = "ADVERTISEMENT",
|
||||
CopyrightViolation = "COPYRIGHT_VIOLATION",
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Meta } from "$api/models";
|
||||
import Git from "svelte-bootstrap-icons/lib/Git.svelte";
|
||||
import Reception4 from "svelte-bootstrap-icons/lib/Reception4.svelte";
|
||||
import Newspaper from "svelte-bootstrap-icons/lib/Newspaper.svelte";
|
||||
import CardText from "svelte-bootstrap-icons/lib/CardText.svelte";
|
||||
import Shield from "svelte-bootstrap-icons/lib/Shield.svelte";
|
||||
import Envelope from "svelte-bootstrap-icons/lib/Envelope.svelte";
|
||||
import CashCoin from "svelte-bootstrap-icons/lib/CashCoin.svelte";
|
||||
import Logo from "./Logo.svelte";
|
||||
|
||||
type Props = { meta: Meta };
|
||||
let { meta }: Props = $props();
|
||||
</script>
|
||||
|
||||
<footer class="big-footer mt-3 pt-3 pb-1 px-5">
|
||||
<div class="d-flex flex-column flex-md-row mb-2">
|
||||
<div class="align-start flex-grow-1">
|
||||
<Logo />
|
||||
<ul class="mt-2 list-unstyled">
|
||||
<li><strong>Version</strong> {meta.version}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="align-end">
|
||||
<ul class="list-unstyled">
|
||||
<li>{meta.users.total} <strong>users</strong></li>
|
||||
<li>{meta.members} <strong>members</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-inline">
|
||||
<a
|
||||
class="list-inline-item link-underline link-underline-opacity-0"
|
||||
target="_blank"
|
||||
href={meta.repository}
|
||||
>
|
||||
<li class="list-inline-item">
|
||||
<Git />
|
||||
Source code
|
||||
</li>
|
||||
</a>
|
||||
<a
|
||||
class="list-inline-item link-underline link-underline-opacity-0"
|
||||
target="_blank"
|
||||
href="https://status.pronouns.cc"
|
||||
>
|
||||
<li class="list-inline-item">
|
||||
<Reception4 />
|
||||
Status
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/about">
|
||||
<li class="list-inline-item">
|
||||
<Envelope />
|
||||
About and contact
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/tos">
|
||||
<li class="list-inline-item">
|
||||
<CardText />
|
||||
Terms of service
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/privacy">
|
||||
<li class="list-inline-item">
|
||||
<Shield />
|
||||
Privacy policy
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/changelog">
|
||||
<li class="list-inline-item">
|
||||
<Newspaper />
|
||||
Changelog
|
||||
</li>
|
||||
</a>
|
||||
<a class="list-inline-item link-underline link-underline-opacity-0" href="/page/donate">
|
||||
<li class="list-inline-item">
|
||||
<CashCoin />
|
||||
Donate
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
</footer>
|
|
@ -8,7 +8,7 @@
|
|||
NavLink,
|
||||
NavItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { page } from "$app/state";
|
||||
import { page } from "$app/stores";
|
||||
import type { Meta, MeUser } from "$api/models/index";
|
||||
import Logo from "$components/Logo.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
|
@ -25,14 +25,12 @@
|
|||
{#if user.suspended}
|
||||
<strong>{$t("nav.suspended-account-hint")}</strong>
|
||||
<br />
|
||||
<a href="/settings">{$t("nav.delete-permanently-link")}</a> •
|
||||
<a href="/contact">{$t("nav.appeal-suspension-link")}</a> •
|
||||
<a href="/settings/export">{$t("nav.export-link")}</a>
|
||||
<a href="/contact">{$t("nav.appeal-suspension-link")}</a>
|
||||
{:else}
|
||||
<strong>{$t("nav.deleted-account-hint")}</strong>
|
||||
<br />
|
||||
<a href="/settings">{$t("nav.reactivate-or-delete-link")}</a> •
|
||||
<a href="/settings/export">{$t("nav.export-link")}</a>
|
||||
<a href="/settings/reactivate">{$t("nav.reactivate-account-link")}</a> •
|
||||
<a href="/contact">{$t("nav.delete-permanently-link")}</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -53,19 +51,19 @@
|
|||
<NavItem>
|
||||
<NavLink
|
||||
href="/@{user.username}"
|
||||
active={page.url.pathname.startsWith(`/@${user.username}`)}
|
||||
active={$page.url.pathname.startsWith(`/@${user.username}`)}
|
||||
>
|
||||
@{user.username}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/settings" active={page.url.pathname.startsWith("/settings")}>
|
||||
<NavLink href="/settings" active={$page.url.pathname.startsWith("/settings")}>
|
||||
{$t("nav.settings")}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{:else}
|
||||
<NavItem>
|
||||
<NavLink href="/auth/log-in" active={page.url.pathname === "/auth/log-in"}>
|
||||
<NavLink href="/auth/log-in" active={$page.url.pathname === "/auth/log-in"}>
|
||||
{$t("nav.log-in")}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = { required?: boolean };
|
||||
let { required }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if required}
|
||||
<small class="text-danger"><abbr title={$t("form.required")}>*</abbr></small>
|
||||
{:else}
|
||||
<small class="text-body-secondary">{$t("form.optional")}</small>
|
||||
{/if}
|
|
@ -1,29 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { type User, type Member, type CustomPreference } from "$api/models";
|
||||
import StatusIcon from "$components/StatusIcon.svelte";
|
||||
|
||||
type Props = { profile: User | Member; allPreferences: Record<string, CustomPreference> };
|
||||
let { profile, allPreferences }: Props = $props();
|
||||
|
||||
let preferences = $derived.by(() => {
|
||||
let preferenceKeys = Object.keys(allPreferences).filter(
|
||||
(pref) =>
|
||||
profile.names.some((entry) => entry.status === pref) ||
|
||||
profile.pronouns.some((entry) => entry.status === pref) ||
|
||||
profile.fields.some((field) => field.entries.some((entry) => entry.status === pref)),
|
||||
);
|
||||
|
||||
return preferenceKeys.map((pref) => allPreferences[pref]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="text-center text-body-secondary">
|
||||
<ul class="list-inline">
|
||||
{#each preferences as preference}
|
||||
<li class="list-inline-item mx-2">
|
||||
<StatusIcon {preference} />
|
||||
{preference.tooltip}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
|
@ -1,36 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { MeUser } from "$api/models";
|
||||
import { PUBLIC_BASE_URL, PUBLIC_SHORT_URL } from "$env/static/public";
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
type Props = {
|
||||
user: string;
|
||||
member?: string;
|
||||
sid: string;
|
||||
reportUrl: string;
|
||||
meUser: MeUser | null;
|
||||
};
|
||||
|
||||
let { user, member, sid, reportUrl, meUser }: Props = $props();
|
||||
|
||||
let profileUrl = $derived(
|
||||
member ? `${PUBLIC_BASE_URL}/@${user}/${member}` : `${PUBLIC_BASE_URL}/@${user}`,
|
||||
);
|
||||
let shortUrl = $derived(`${PUBLIC_SHORT_URL}/${sid}`);
|
||||
|
||||
const copyUrl = async (url: string) => {
|
||||
await navigator.clipboard.writeText(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick={() => copyUrl(profileUrl)}>
|
||||
{$t("profile.copy-link-button")}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick={() => copyUrl(shortUrl)}>
|
||||
{$t("profile.copy-short-link-button")}
|
||||
</button>
|
||||
{#if meUser && meUser.username !== user}
|
||||
<a class="btn btn-outline-danger" href={reportUrl}>{$t("profile.report-button")}</a>
|
||||
{/if}
|
||||
</div>
|
|
@ -5,11 +5,9 @@
|
|||
"settings": "Settings",
|
||||
"suspended-account-hint": "Your account has been suspended. Your profile has been hidden and you will not be able to change any settings.",
|
||||
"appeal-suspension-link": "I want to appeal",
|
||||
"deleted-account-hint": "You have requested deletion of your account.",
|
||||
"deleted-account-hint": "You have requested deletion of your account. If you want to reactivate it, click the link below.",
|
||||
"reactivate-account-link": "Reactivate account",
|
||||
"delete-permanently-link": "I want my account deleted permanently",
|
||||
"reactivate-or-delete-link": "I want to reactivate my account or delete all my data",
|
||||
"export-link": "I want to export a copy of my data"
|
||||
"delete-permanently-link": "I want my account deleted permanently"
|
||||
},
|
||||
"avatar-tooltip": "Avatar for {{name}}",
|
||||
"profile": {
|
||||
|
@ -20,10 +18,7 @@
|
|||
"pronouns-header": "Pronouns",
|
||||
"default-members-header": "Members",
|
||||
"create-member-button": "Create member",
|
||||
"back-to-user": "Back to {{name}}",
|
||||
"copy-link-button": "Copy link",
|
||||
"copy-short-link-button": "Copy short link",
|
||||
"report-button": "Report profile"
|
||||
"back-to-user": "Back to {{name}}"
|
||||
},
|
||||
"title": {
|
||||
"log-in": "Log in",
|
||||
|
@ -157,44 +152,7 @@
|
|||
"flag-description-placeholder": "Description",
|
||||
"flag-name-placeholder": "Name",
|
||||
"flag-upload-success": "Successfully uploaded your flag! It may take a few seconds before it's saved.",
|
||||
"custom-preferences-title": "Custom preferences",
|
||||
"change-username-header": "Change your username",
|
||||
"force-delete-button": "Delete my account permanently",
|
||||
"force-delete-warning": "This is irreversible. Consider exporting a copy of your data before doing this.",
|
||||
"force-delete-explanation": "Your account is currently pending deletion. If you want your data deleted permanently, use the button below.",
|
||||
"reactivate-explanation": "Your account is currently pending deletion. If you want to cancel this and keep using your account, use the link below.",
|
||||
"reactivate-header": "Reactivate your account",
|
||||
"force-delete-header": "Permanently delete your account",
|
||||
"reactivate-button": "Reactivate my account",
|
||||
"reactivated-header": "Account reactivated",
|
||||
"reactivated-explanation": "Your account has been reactivated!",
|
||||
"force-delete-input-label": "To delete your account, type your username (@{{username}}), including the @, in the box below:",
|
||||
"force-delete-export-hint": "If you haven't done so yet, we recommend you download an export of your data before continuing:",
|
||||
"force-delete-export-link": "export your data",
|
||||
"force-delete-irreversible": "This process is irreversible.",
|
||||
"force-delete-username-available": "Your username will immediately be available for other users to take.",
|
||||
"force-delete-immediate-delete": "This will immediately delete all of your profiles, including avatars.",
|
||||
"force-delete-page-explanation": "Your account is currently pending deletion. If you want all your data deleted immediately, you can do so here.",
|
||||
"force-delete-page-header": "Permanently delete your account",
|
||||
"force-delete-checkbox-label": "Yes, I understand that my data will be permanently deleted and cannot be recovered.",
|
||||
"force-delete-page-button": "Delete my account",
|
||||
"account-is-deleted-header": "Your account has been deleted",
|
||||
"account-is-deleted-permanently-description": "Your account has been deleted. Note that it may take a few minutes for all of your data to be removed.",
|
||||
"account-is-deleted-close-page": "You may now close this page.",
|
||||
"soft-delete-button": "Deactivate your account",
|
||||
"soft-delete-hint": "If you want to delete your account, use the button below.",
|
||||
"soft-delete-header": "Deactivate your account",
|
||||
"force-delete-page-cancel": "I changed my mind, cancel",
|
||||
"soft-delete-page-header": "Deactivate your account",
|
||||
"soft-delete-page-explanation": "If you want to delete your account, you can do so here.",
|
||||
"soft-delete-90-days": "Your account will be permanently deleted after 90 days.",
|
||||
"soft-delete-can-reactivate": "If you change your mind, you can log in and go to the settings page at any time to reactivate your account.",
|
||||
"soft-delete-keep-username": "You will keep your current username until your account is permanently deleted.",
|
||||
"soft-delete-can-delete-permanently": "If you want to delete all your data early, you can do so by logging in and going to the settings page.",
|
||||
"soft-delete-page-button": "Deactivate my account",
|
||||
"soft-delete-input-label": "To deactivate your account, type your username (@{{username}}), including the @, in the box below:",
|
||||
"account-is-deactivated-header": "Your account has been deactivated",
|
||||
"account-is-deactivated-description": "Your account has been deactivated, and will be deleted in 90 days. If you change your mind, just log in again, and you will have the option to reactivate your account. If you want to delete your data immediately, you should also log in again, and you will be able to request immediate deletion."
|
||||
"custom-preferences-title": "Custom preferences"
|
||||
},
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
|
@ -279,35 +237,5 @@
|
|||
"custom-preference-muted": "Show as muted text",
|
||||
"custom-preference-favourite": "Treat like favourite"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"report": {
|
||||
"title": "Reporting {{name}}",
|
||||
"totalitarianism": "Support of totalitarian regimes",
|
||||
"hate-speech": "Hate speech",
|
||||
"racism": "Racism or xenophobia",
|
||||
"homophobia": "Homophobia",
|
||||
"transphobia": "Transphobia",
|
||||
"queerphobia": "Queerphobia (other)",
|
||||
"exclusionism": "Queer or plural exclusionism",
|
||||
"sexism": "Sexism or misogyny",
|
||||
"ableism": "Ableism",
|
||||
"child-pornography": "Child pornography",
|
||||
"pedophilia-advocacy": "Pedophilia advocacy",
|
||||
"harassment": "Harassment",
|
||||
"impersonation": "Impersonation",
|
||||
"doxxing": "Doxxing",
|
||||
"encouraging-self-harm": "Encouraging self-harm or suicide",
|
||||
"spam": "Spam",
|
||||
"trolling": "Trolling",
|
||||
"advertisement": "Advertising",
|
||||
"copyright-violation": "Copyright or trademark violation",
|
||||
"success": "Successfully submitted report!",
|
||||
"reason-label": "Why are you reporting this profile?",
|
||||
"context-label": "Is there any context you'd like to give us?",
|
||||
"submit-button": "Submit report"
|
||||
},
|
||||
"form": {
|
||||
"optional": "(optional)",
|
||||
"required": "Required"
|
||||
}
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import Error from "$components/Error.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { LayoutData } from "./$types";
|
||||
|
||||
|
@ -15,8 +14,22 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<Error {error} headerElem="h3" />
|
||||
<div class="btn-group mt-2">
|
||||
<h3>{$t("title.an-error-occurred")}</h3>
|
||||
<p>
|
||||
<strong>{$page.status}</strong>: {error.message}
|
||||
</p>
|
||||
<p>
|
||||
{#if $page.status === 400}
|
||||
{$t("error.400-description")}
|
||||
{:else if $page.status === 404}
|
||||
{$t("error.404-description")}
|
||||
{:else if $page.status === 500}
|
||||
{$t("error.500-description")}
|
||||
{:else}
|
||||
{$t("error.unknown-status-description")}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
{#if data.meUser}
|
||||
<a class="btn btn-primary" href="/@{data.meUser.username}">
|
||||
{$t("error.back-to-profile-button")}
|
||||
|
|
|
@ -3,16 +3,11 @@
|
|||
import "../app.scss";
|
||||
import type { LayoutData } from "./$types";
|
||||
import Navbar from "$components/Navbar.svelte";
|
||||
import Footer from "$components/Footer.svelte";
|
||||
|
||||
type Props = { children: Snippet; data: LayoutData };
|
||||
let { children, data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<div class="flex-grow-1">
|
||||
<Navbar user={data.meUser} meta={data.meta} />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<Footer meta={data.meta} />
|
||||
</div>
|
||||
<Navbar user={data.meUser} meta={data.meta} />
|
||||
|
||||
{@render children?.()}
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { ErrorCode } from "$api/error.js";
|
||||
import type { UserWithMembers } from "$api/models";
|
||||
import log from "$lib/log.js";
|
||||
import paginate from "$lib/paginate";
|
||||
import { error } from "@sveltejs/kit";
|
||||
|
||||
const MEMBERS_PER_PAGE = 20;
|
||||
|
||||
export const load = async ({ params, fetch, cookies, url }) => {
|
||||
const user = await apiRequest<UserWithMembers>("GET", `/users/${params.username}`, {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
let user: UserWithMembers;
|
||||
|
||||
try {
|
||||
user = await apiRequest<UserWithMembers>("GET", `/users/${params.username}`, {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.code === ErrorCode.UserNotFound) error(404, "User not found");
|
||||
log.error("Error fetching user %s:", params.username, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const { data, currentPage, pageCount } = paginate(
|
||||
user.members,
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
import Paginator from "$components/Paginator.svelte";
|
||||
import MemberCard from "$components/profile/user/MemberCard.svelte";
|
||||
import ProfileButtons from "$components/profile/ProfileButtons.svelte";
|
||||
import PreferenceCheatsheet from "$components/profile/PreferenceCheatsheet.svelte";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
|
@ -29,13 +27,6 @@
|
|||
|
||||
<ProfileHeader name="@{data.user.username}" profile={data.user} offset={data.user.utc_offset} />
|
||||
<ProfileFields profile={data.user} {allPreferences} />
|
||||
<PreferenceCheatsheet profile={data.user} {allPreferences} />
|
||||
<ProfileButtons
|
||||
meUser={data.meUser}
|
||||
user={data.user.username}
|
||||
sid={data.user.sid}
|
||||
reportUrl="/report/{data.user.id}"
|
||||
/>
|
||||
|
||||
{#if data.members.length > 0}
|
||||
<hr />
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
import { apiRequest } from "$api";
|
||||
import ApiError, { ErrorCode } from "$api/error.js";
|
||||
import type { Member } from "$api/models/member";
|
||||
import log from "$lib/log.js";
|
||||
import { error } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ params, fetch, cookies }) => {
|
||||
const member = await apiRequest<Member>(
|
||||
"GET",
|
||||
`/users/${params.username}/members/${params.memberName}`,
|
||||
{
|
||||
fetch,
|
||||
cookies,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const member = await apiRequest<Member>(
|
||||
"GET",
|
||||
`/users/${params.username}/members/${params.memberName}`,
|
||||
{
|
||||
fetch,
|
||||
cookies,
|
||||
},
|
||||
);
|
||||
|
||||
return { member };
|
||||
return { member };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
if (e.code === ErrorCode.UserNotFound) error(404, "User not found");
|
||||
if (e.code === ErrorCode.MemberNotFound) error(404, "Member not found");
|
||||
}
|
||||
|
||||
log.error("Error fetching user %s/member %s:", params.username, params.memberName, e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
import ProfileFields from "$components/profile/ProfileFields.svelte";
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
import { t } from "$lib/i18n";
|
||||
import ProfileButtons from "$components/profile/ProfileButtons.svelte";
|
||||
import PreferenceCheatsheet from "$components/profile/PreferenceCheatsheet.svelte";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
|
@ -39,12 +37,4 @@
|
|||
|
||||
<ProfileHeader name="{data.member.name} (@{data.member.user.username})" profile={data.member} />
|
||||
<ProfileFields profile={data.member} {allPreferences} />
|
||||
<PreferenceCheatsheet profile={data.member} {allPreferences} />
|
||||
<ProfileButtons
|
||||
meUser={data.meUser}
|
||||
user={data.member.user.username}
|
||||
member={data.member.name}
|
||||
sid={data.member.sid}
|
||||
reportUrl="/report/{data.member.user.id}?member={data.member.id}"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import { apiRequest, fastRequest } from "$api";
|
||||
import ApiError from "$api/error.js";
|
||||
import type { Member } from "$api/models/member.js";
|
||||
import { type CreateReportRequest, ReportReason } from "$api/models/moderation.js";
|
||||
import type { PartialUser, User } from "$api/models/user.js";
|
||||
import log from "$lib/log.js";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ parent, params, fetch, cookies, url }) => {
|
||||
const { meUser } = await parent();
|
||||
if (!meUser) redirect(303, "/");
|
||||
|
||||
let user: PartialUser;
|
||||
let member: Member | null = null;
|
||||
if (url.searchParams.has("member")) {
|
||||
const resp = await apiRequest<Member>(
|
||||
"GET",
|
||||
`/users/${params.id}/members/${url.searchParams.get("member")}`,
|
||||
{ fetch, cookies },
|
||||
);
|
||||
|
||||
user = resp.user;
|
||||
member = resp;
|
||||
} else {
|
||||
user = await apiRequest<User>("GET", `/users/${params.id}`, { fetch, cookies });
|
||||
}
|
||||
|
||||
if (meUser.id === user.id) redirect(303, "/");
|
||||
|
||||
return { user, member };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, fetch, cookies }) => {
|
||||
const body = await request.formData();
|
||||
|
||||
const targetIsMember = body.get("target-type") === "member";
|
||||
const target = body.get("target-id") as string;
|
||||
const reason = body.get("reason") as ReportReason;
|
||||
const context = body.get("context") as string | null;
|
||||
|
||||
const url = targetIsMember
|
||||
? `/moderation/report-member/${target}`
|
||||
: `/moderation/report-user/${target}`;
|
||||
|
||||
try {
|
||||
await fastRequest<CreateReportRequest>("POST", url, {
|
||||
body: { reason, context },
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
|
||||
return { ok: true, error: null };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) return { ok: false, error: e.obj };
|
||||
log.error("error reporting user or member %s:", target, e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,70 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { ReportReason } from "$api/models/moderation";
|
||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||
import RequiredFieldMarker from "$components/RequiredFieldMarker.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { ActionData, PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData; form: ActionData };
|
||||
let { data, form }: Props = $props();
|
||||
|
||||
let name = $derived(
|
||||
data.member ? `${data.member.name} (@${data.user.username})` : "@" + data.user.username,
|
||||
);
|
||||
|
||||
let link = $derived(
|
||||
data.member ? `/@${data.user.username}/${data.member.name}` : `/@${data.user.username}`,
|
||||
);
|
||||
|
||||
let reasons = $derived.by(() => {
|
||||
const reasons = [];
|
||||
for (const value of Object.values(ReportReason)) {
|
||||
const key = "report." + value.toLowerCase().replaceAll("_", "-");
|
||||
reasons.push({ key, value });
|
||||
}
|
||||
return reasons;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("report.title", { name })} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<form method="POST" class="w-lg-75 mx-auto">
|
||||
<h3>{$t("report.title", { name })}</h3>
|
||||
<FormStatusMarker {form} successMessage={$t("report.success")} />
|
||||
<input type="hidden" name="target-type" value={data.member ? "member" : "user"} />
|
||||
<input type="hidden" name="target-id" value={data.member ? data.member.id : data.user.id} />
|
||||
|
||||
<h4 class="mt-3">{$t("report.reason-label")} <RequiredFieldMarker required /></h4>
|
||||
<div class="row row-cols-1 row-cols-lg-2">
|
||||
{#each reasons as reason}
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="reason"
|
||||
value={reason.value}
|
||||
id="reason-{reason.value}"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="reason-{reason.value}">{$t(reason.key)}</label>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<h4 class="mt-3">
|
||||
{$t("report.context-label")}
|
||||
<RequiredFieldMarker />
|
||||
</h4>
|
||||
<textarea class="form-control" name="context" style="height: 100px;" maxlength={512}></textarea>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-danger">{$t("report.submit-button")}</button>
|
||||
<a href={link} class="btn btn-secondary">{$t("cancel")}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "$lib/i18n";
|
||||
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
@ -10,11 +10,11 @@
|
|||
const isActive = (path: string | string[], prefix: boolean = false) =>
|
||||
typeof path === "string"
|
||||
? prefix
|
||||
? page.url.pathname.startsWith(path)
|
||||
: page.url.pathname === path
|
||||
? $page.url.pathname.startsWith(path)
|
||||
: $page.url.pathname === path
|
||||
: prefix
|
||||
? path.some((p) => page.url.pathname.startsWith(p))
|
||||
: path.some((p) => page.url.pathname === p);
|
||||
? path.some((p) => $page.url.pathname.startsWith(p))
|
||||
: path.some((p) => $page.url.pathname === p);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -18,37 +18,9 @@
|
|||
|
||||
<h3>{$t("settings.general-information-tab")}</h3>
|
||||
|
||||
{#if data.user.deleted}
|
||||
<div class="row mb-3">
|
||||
{#if !data.user.suspended}
|
||||
<div class="col-md">
|
||||
<h4>{$t("settings.reactivate-header")}</h4>
|
||||
<p>
|
||||
{$t("settings.reactivate-explanation")}
|
||||
</p>
|
||||
<a href="/settings/reactivate" class="btn btn-success">
|
||||
{$t("settings.reactivate-button")}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-md">
|
||||
<h4>{$t("settings.force-delete-header")}</h4>
|
||||
<p>
|
||||
{$t("settings.force-delete-explanation")}
|
||||
<strong>
|
||||
{$t("settings.force-delete-warning")}
|
||||
</strong>
|
||||
</p>
|
||||
<a href="/settings/force-delete" class="btn btn-danger">
|
||||
{$t("settings.force-delete-button")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-9">
|
||||
<h5>{$t("settings.change-username-header")}</h5>
|
||||
<h5>Change your username</h5>
|
||||
<form method="POST" action="?/changeUsername" use:enhance>
|
||||
<FormGroup class="mb-3">
|
||||
<InputGroup class="m-1 mt-3 w-md-75">
|
||||
|
@ -108,14 +80,6 @@
|
|||
<a class="btn btn-danger" href="/settings/force-log-out">{$t("settings.force-log-out-button")}</a>
|
||||
</div>
|
||||
|
||||
{#if !data.user.deleted}
|
||||
<div class="mb-3">
|
||||
<h4>{$t("settings.soft-delete-header")}</h4>
|
||||
<p>{$t("settings.soft-delete-hint")}</p>
|
||||
<a href="/settings/delete" class="btn btn-danger">{$t("settings.soft-delete-button")}</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h4>{$t("settings.table-title")}</h4>
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import { fastRequest } from "$api";
|
||||
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||
import { clearToken } from "$lib";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ parent }) => {
|
||||
const { meUser } = await parent();
|
||||
if (!meUser) redirect(303, "/");
|
||||
|
||||
if (meUser.deleted)
|
||||
throw new ApiError({
|
||||
message: "You cannot use this page.",
|
||||
status: 403,
|
||||
code: ErrorCode.Forbidden,
|
||||
});
|
||||
|
||||
return { user: meUser! };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, fetch, cookies }) => {
|
||||
const body = await request.formData();
|
||||
const username = body.get("username") as string;
|
||||
const currentUsername = body.get("current-username") as string;
|
||||
|
||||
if (!username || username !== currentUsername) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: "Username doesn't match your username.",
|
||||
status: 400,
|
||||
code: ErrorCode.BadRequest,
|
||||
} as RawApiError,
|
||||
};
|
||||
}
|
||||
|
||||
await fastRequest("POST", "/self-delete/delete", { fetch, cookies, isInternal: true });
|
||||
clearToken(cookies);
|
||||
redirect(303, "/settings/delete/success");
|
||||
},
|
||||
};
|
|
@ -1,55 +0,0 @@
|
|||
<script lang="ts">
|
||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { ActionData, PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData; form: ActionData };
|
||||
let { data, form }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("settings.soft-delete-page-header")} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="w-lg-75 mx-auto">
|
||||
<h3>{$t("settings.soft-delete-page-header")}</h3>
|
||||
|
||||
<p>
|
||||
{$t("settings.soft-delete-page-explanation")}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>{$t("settings.soft-delete-90-days")}</li>
|
||||
<li>
|
||||
{$t("settings.soft-delete-can-reactivate")}
|
||||
</li>
|
||||
<li>{$t("settings.soft-delete-keep-username")}</li>
|
||||
<li>
|
||||
{$t("settings.soft-delete-can-delete-permanently")}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form method="POST">
|
||||
<FormStatusMarker {form} />
|
||||
<p>
|
||||
{$t("settings.soft-delete-input-label", { username: data.user.username })}
|
||||
<input
|
||||
class="form-control mt-2"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
placeholder="@{data.user.username}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input type="hidden" value="@{data.user.username}" readonly name="current-username" />
|
||||
</p>
|
||||
<div class="btn-group mb-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{$t("settings.soft-delete-page-button")}
|
||||
</button>
|
||||
<a href="/settings" class="btn btn-secondary">{$t("settings.force-delete-page-cancel")}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("settings.soft-delete-page-header")} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="w-lg-75 mx-auto">
|
||||
<h3>{$t("settings.account-is-deactivated-header")}</h3>
|
||||
<p>
|
||||
{$t("settings.account-is-deactivated-description")}
|
||||
</p>
|
||||
<p>{$t("settings.account-is-deleted-close-page")}</p>
|
||||
<p>
|
||||
<a href="/" class="btn btn-secondary">{$t("error.back-to-main-page-button")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -48,6 +48,7 @@
|
|||
|
||||
const idx = flags.findIndex((f) => f.id === id);
|
||||
if (idx === -1) return;
|
||||
console.log("yippee");
|
||||
flags[idx] = { ...flags[idx], name, description };
|
||||
} catch (e) {
|
||||
log.error("Could not update flag %s:", id, e);
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { fastRequest } from "$api";
|
||||
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
|
||||
import { clearToken } from "$lib";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ parent }) => {
|
||||
const { meUser } = await parent();
|
||||
if (!meUser) redirect(303, "/");
|
||||
|
||||
if (!meUser.deleted)
|
||||
throw new ApiError({
|
||||
message: "You cannot use this page.",
|
||||
status: 403,
|
||||
code: ErrorCode.Forbidden,
|
||||
});
|
||||
|
||||
return { user: meUser! };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, fetch, cookies }) => {
|
||||
const body = await request.formData();
|
||||
const username = body.get("username") as string;
|
||||
const currentUsername = body.get("current-username") as string;
|
||||
const confirmed = !!body.get("confirm");
|
||||
|
||||
if (!username || username !== currentUsername) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: "Username doesn't match your username.",
|
||||
status: 400,
|
||||
code: ErrorCode.BadRequest,
|
||||
} as RawApiError,
|
||||
};
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: "You must check the box to continue.",
|
||||
status: 400,
|
||||
code: ErrorCode.BadRequest,
|
||||
} as RawApiError,
|
||||
};
|
||||
}
|
||||
|
||||
await fastRequest("POST", "/self-delete/force", { fetch, cookies, isInternal: true });
|
||||
clearToken(cookies);
|
||||
redirect(303, "/settings/force-delete/success");
|
||||
},
|
||||
};
|
|
@ -1,68 +0,0 @@
|
|||
<script lang="ts">
|
||||
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { ActionData, PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData; form: ActionData };
|
||||
let { data, form }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("settings.force-delete-page-header")} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="w-lg-75 mx-auto">
|
||||
<h3>{$t("settings.force-delete-page-header")}</h3>
|
||||
|
||||
<p>
|
||||
{$t("settings.force-delete-page-explanation")}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>{$t("settings.force-delete-immediate-delete")}</li>
|
||||
<li>{$t("settings.force-delete-username-available")}</li>
|
||||
<li><strong>{$t("settings.force-delete-irreversible")}</strong></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{$t("settings.force-delete-export-hint")}
|
||||
<a href="/settings/export">{$t("settings.force-delete-export-link")}</a>
|
||||
</p>
|
||||
|
||||
<form method="POST">
|
||||
<FormStatusMarker {form} />
|
||||
<p>
|
||||
{$t("settings.force-delete-input-label", { username: data.user.username })}
|
||||
<input
|
||||
class="form-control mt-2"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
placeholder="@{data.user.username}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input type="hidden" value="@{data.user.username}" readonly name="current-username" />
|
||||
</p>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="yes"
|
||||
name="confirm"
|
||||
id="confirm"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="confirm">
|
||||
{$t("settings.force-delete-checkbox-label")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group mt-3 mb-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{$t("settings.force-delete-page-button")}
|
||||
</button>
|
||||
<a href="/settings" class="btn btn-secondary">{$t("settings.force-delete-page-cancel")}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("settings.force-delete-page-header")} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="w-lg-75 mx-auto">
|
||||
<h3>{$t("settings.account-is-deleted-header")}</h3>
|
||||
<p>
|
||||
{$t("settings.account-is-deleted-permanently-description")}
|
||||
</p>
|
||||
<p>{$t("settings.account-is-deleted-close-page")}</p>
|
||||
<p>
|
||||
<a href="/" class="btn btn-secondary">{$t("error.back-to-main-page-button")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { LayoutData } from "./$types";
|
||||
|
||||
type Props = { data: LayoutData; children: Snippet };
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
const isActive = (path: string) => page.url.pathname === path;
|
||||
const isActive = (path: string) => $page.url.pathname === path;
|
||||
|
||||
let name = $derived(
|
||||
data.member.display_name === data.member.name
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "$lib/i18n";
|
||||
import type { LayoutData } from "./$types";
|
||||
|
||||
type Props = { data: LayoutData; children: Snippet };
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
const isActive = (path: string) => page.url.pathname === path;
|
||||
const isActive = (path: string) => $page.url.pathname === path;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { fastRequest } from "$api";
|
||||
import ApiError, { ErrorCode } from "$api/error";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ parent, fetch, cookies }) => {
|
||||
const { meUser } = await parent();
|
||||
if (!meUser) redirect(303, "/");
|
||||
|
||||
if (meUser.suspended || !meUser.deleted)
|
||||
throw new ApiError({
|
||||
message: "You cannot use this page.",
|
||||
status: 403,
|
||||
code: ErrorCode.Forbidden,
|
||||
});
|
||||
|
||||
await fastRequest("POST", "/self-delete/undelete", {
|
||||
fetch,
|
||||
cookies,
|
||||
isInternal: true,
|
||||
});
|
||||
|
||||
return { user: meUser! };
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="w-lg-75 mx-auto">
|
||||
<h3>{$t("settings.reactivated-header")}</h3>
|
||||
|
||||
<p>{$t("settings.reactivated-explanation")}</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="/settings" class="btn btn-primary">{$t("edit-profile.back-to-settings-tab")}</a>
|
||||
<a href="/@{data.user.username}" class="btn btn-secondary">
|
||||
{$t("error.back-to-profile-button")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -38,7 +38,7 @@ func (hn *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// all public api endpoints are prefixed with this
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/v2") && !strings.HasPrefix(r.URL.Path, "/api/v1") {
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/v2") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"port": 5003,
|
||||
"proxy_target": "http://localhost:6000",
|
||||
"proxy_target": "http://localhost:5000",
|
||||
"debug": true,
|
||||
"powered_by": "5 gay rats"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue