net10, the refactor finished, and federation that works
Build / Build (push) Successful in 28s
Deploy / privapub.thepra.dev (push) Successful in 19s

The tree had not compiled since its first commit: Group, DmGroup and
IGroupUsersService were referenced and never written, an IDE rename had
turned the user-settings DTO into the ViewAvatarServer enum, and the settings
were saved as an entity they no longer were.

Built now:
- Group (an ActivityPub Group actor with its own keys, members, invitation
  code and optional password) and DmGroup (a conversation), with
  /clientapi/group/{list,insert,update,join,leave,approve}.
- Posts and DMs: /clientapi/post/{list,insert,delete}, /clientapi/dm/{list,insert};
  DM recipients are local usernames or user@host handles resolved by WebFinger.
- Invitation sign-up and login against the group's invitation code, checking
  the password before any account is created.
- Federation: WebFinger, NodeInfo 2.0, actors at /peasants/{name} (Person,
  Group, and an Application instance actor) with SPKI keys, draft-cavage
  RSA-SHA256 HTTP signatures both ways, an inbox handling Follow (+Accept),
  Undo, Create, Delete and Update, an outbox, notes at /posts/{id}, and a
  persisted, retried, signed delivery queue. A post to a group is announced
  by the group to its followers (FEP-1b12).
- The unused, broken typed ActivityPub models are replaced by a renderer;
  NSign's HMAC setup, which could not federate, is gone.

Upgrade: net10.0, MongoDB.Entities 25.1 (instance DB API, Standard GUIDs),
Swashbuckle 10 / OpenApi 2, Serilog.AspNetCore 10, MailKit 4.18,
PasswordGenerator 3. The JWT keys are 64 bytes (IdentityModel 8 refuses
shorter for HS512). Production runs its own mongod on 127.0.0.1:27022, as
Sintopia's apps do, and deploys to privapub.thepra.dev from the build runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 08:05:56 +02:00
1 parent 56c5396106
commit 075c22228a
78 files changed
+3419 -1091

No files matched your search

+58 -61
View File
@@ -1,4 +1,4 @@
using MailKit.Net.Smtp;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Localization;
@@ -72,12 +72,10 @@ namespace PrivaPub.Services
newUser.Policies.Add(Policies.IsModerator);
}
await newUser.SaveAsync();
var cultureLanguage = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
var language = await DbEntities.Languages.Match(l => l.International2Code == cultureLanguage).ExecuteFirstAsync();
var newRootUserSettings = new RootUserSettings
newUser.Settings = new RootUserSettings
{
RootUserId = newUser.ID,
LanguageCode = language?.International2Code ?? "en",
LightThemeIndexColour = signUpForm.LightThemeIndexColour,
DarkThemeIndexColour = signUpForm.DarkThemeIndexColour,
@@ -87,7 +85,7 @@ namespace PrivaPub.Services
PreferSystemTheming = signUpForm.PreferSystemTheming,
ThemeIsDarkMode = signUpForm.ThemeIsDarkMode
};
await newRootUserSettings.SaveAsync();
await DB.Default.SaveAsync(newUser);
//if (!string.IsNullOrEmpty(invitationCode))
//{
@@ -106,17 +104,7 @@ namespace PrivaPub.Services
// return result;
//}
result.Data = (newUser, new ViewAvatarServer
{
LanguageCode = newRootUserSettings.LanguageCode,
LightThemeIndexColour = newRootUserSettings.LightThemeIndexColour,
ThemeIsDarkMode = newRootUserSettings.ThemeIsDarkMode,
IconsThemeIndexColour = newRootUserSettings.IconsThemeIndexColour,
ThemeIsDarkGray = newRootUserSettings.ThemeIsDarkGray,
ThemeIsLightGray = newRootUserSettings.ThemeIsLightGray,
DarkThemeIndexColour = newRootUserSettings.DarkThemeIndexColour,
PreferSystemTheming = newRootUserSettings.PreferSystemTheming
});
result.Data = (newUser, ToViewSettings(newUser.Settings));
return result;
}
@@ -134,7 +122,7 @@ namespace PrivaPub.Services
try
{
loginForm.UserName = loginForm.UserName.ToLower();
if (!await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName && u.DeletionDate == null)
if (!await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName && u.DeletedAt == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", loginForm.UserName]);
@@ -151,7 +139,7 @@ namespace PrivaPub.Services
result.ErrorMessage = Localizer["Needs upgrade!"];
var userSettingsResult = await GetUserSettingsAsync(user.ID, loginForm);
var userSettings = (ViewAvatarServer)userSettingsResult.Data;
var userSettings = (ViewUserSettings)userSettingsResult.Data;
//if (!string.IsNullOrEmpty(invitationCode))
//{
@@ -194,7 +182,7 @@ namespace PrivaPub.Services
return result.Invalidate(Localizer["Email '{0}' already taken.", userForm.Email]);
}
await DB.Update<RootUser>()
await DB.Default.Update<RootUser>()
.Match(u => u.ID == userId)
.Modify(u => u.Email, userForm.Email)
.ExecuteAsync();
@@ -208,7 +196,7 @@ namespace PrivaPub.Services
}
}
public async Task<WebResult> UpdateUserSettingsAsync(ViewAvatarServer userSettings, string userId)
public async Task<WebResult> UpdateUserSettingsAsync(ViewUserSettings userSettings, string userId)
{
var result = new WebResult();
try
@@ -224,16 +212,17 @@ namespace PrivaPub.Services
var language = await DbEntities.Languages.Match(l => l.International2Code == userSettings.LanguageCode)
.ExecuteFirstAsync();
_ = await DB.Update<RootUserSettings>()
.Match(u => u.RootUserId == userId)
.Modify(us => us.LanguageCode, language.International2Code)
.Modify(us => us.LightThemeIndexColour, userSettings.LightThemeIndexColour)
.Modify(us => us.DarkThemeIndexColour, userSettings.DarkThemeIndexColour)
.Modify(us => us.PreferSystemTheming, userSettings.PreferSystemTheming)
.Modify(us => us.ThemeIsDarkMode, userSettings.ThemeIsDarkMode)
.Modify(us => us.ThemeIsDarkGray, userSettings.ThemeIsDarkGray)
.Modify(us => us.ThemeIsLightGray, userSettings.ThemeIsLightGray)
.Modify(us => us.IconsThemeIndexColour, userSettings.IconsThemeIndexColour)
_ = await DB.Default.Update<RootUser>()
.MatchID(userId)
.Modify(u => u.Settings.LanguageCode, language.International2Code)
.Modify(u => u.Settings.LightThemeIndexColour, userSettings.LightThemeIndexColour)
.Modify(u => u.Settings.DarkThemeIndexColour, userSettings.DarkThemeIndexColour)
.Modify(u => u.Settings.PreferSystemTheming, userSettings.PreferSystemTheming)
.Modify(u => u.Settings.ThemeIsDarkMode, userSettings.ThemeIsDarkMode)
.Modify(u => u.Settings.ThemeIsDarkGray, userSettings.ThemeIsDarkGray)
.Modify(u => u.Settings.ThemeIsLightGray, userSettings.ThemeIsLightGray)
.Modify(u => u.Settings.IconsThemeIndexColour, userSettings.IconsThemeIndexColour)
.Modify(u => u.UpdatedAt, DateTime.UtcNow)
.ExecuteAsync();
return result;
@@ -250,7 +239,7 @@ namespace PrivaPub.Services
var result = new WebResult();
try
{
var user = await DbEntities.RootUsers.Match(u => u.ID == userId && u.DeletionDate == null).ExecuteFirstAsync();
var user = await DbEntities.RootUsers.Match(u => u.ID == userId && u.DeletedAt == null).ExecuteFirstAsync();
if (user == null)
return result.Invalidate(Localizer["Username '{0}' not found.", userId]);
@@ -262,7 +251,8 @@ namespace PrivaPub.Services
var newPasswordHashed = PasswordHasher.Hash(userPasswordForm.NewPassword);
user.HashedPassword = newPasswordHashed;
await user.SaveAsync();
user.UpdatedAt = DateTime.UtcNow;
await DB.Default.SaveAsync(user);
return result;
}
@@ -278,7 +268,7 @@ namespace PrivaPub.Services
var result = new WebResult();
try
{
var users = await DbEntities.RootUsers.Match(u => usersIds.UserIdList.Contains(u.ID) && u.DeletionDate == default).ExecuteAsync();
var users = await DbEntities.RootUsers.Match(u => usersIds.UserIdList.Contains(u.ID) && u.DeletedAt == default).ExecuteAsync();
if (users == null || users.Count == 0)
return result.Invalidate(Localizer["User already deleted."]);
@@ -291,9 +281,9 @@ namespace PrivaPub.Services
user.IsEmailValidated = false;
//user.TempSecret = null;
user.UserName = Localizer["Deleted user"];
user.DeletionDate = DateTime.UtcNow;
user.DeletedAt = DateTime.UtcNow;
await user.SaveAsync();
await DB.Default.SaveAsync(user);
}
return result;
@@ -310,7 +300,7 @@ namespace PrivaPub.Services
var result = new WebResult();
try
{
await DB.Update<RootUser>()
await DB.Default.Update<RootUser>()
.Match(u => usersIds.UserIdList.Contains(u.ID))
.Modify(u => u.IsBanned, true)
.ExecuteAsync();
@@ -329,7 +319,7 @@ namespace PrivaPub.Services
var result = new WebResult();
try
{
await DB.Update<RootUser>()
await DB.Default.Update<RootUser>()
.Match(u => usersIds.UserIdList.Contains(u.ID))
.Modify(u => u.IsBanned, false)
.ExecuteAsync();
@@ -348,24 +338,20 @@ namespace PrivaPub.Services
var result = new WebResult();
try
{
var userSettings = await DbEntities.RootUsersSettings.Match(u => u.RootUserId == userId).ExecuteFirstAsync();
if (loginForm != default && (loginForm.ThemeIsDarkMode != userSettings.ThemeIsDarkMode))
var user = await DbEntities.RootUsers.MatchID(userId).ExecuteFirstAsync();
if (user == default)
return result.Invalidate(Localizer["User not found."], StatusCodes.Status404NotFound);
if (loginForm != default && loginForm.ThemeIsDarkMode != user.Settings.ThemeIsDarkMode)
{
userSettings.ThemeIsDarkMode = loginForm.ThemeIsDarkMode;
await userSettings.SaveAsync();
user.Settings.ThemeIsDarkMode = loginForm.ThemeIsDarkMode;
await DB.Default.Update<RootUser>()
.MatchID(userId)
.Modify(u => u.Settings.ThemeIsDarkMode, loginForm.ThemeIsDarkMode)
.ExecuteAsync();
}
result.Data = new ViewAvatarServer
{
LanguageCode = userSettings.LanguageCode,
LightThemeIndexColour = userSettings.LightThemeIndexColour,
DarkThemeIndexColour = userSettings.DarkThemeIndexColour,
PreferSystemTheming = userSettings.PreferSystemTheming,
ThemeIsDarkMode = userSettings.ThemeIsDarkMode,
IconsThemeIndexColour = userSettings.IconsThemeIndexColour,
ThemeIsDarkGray = userSettings.ThemeIsDarkGray,
ThemeIsLightGray = userSettings.ThemeIsLightGray
};
result.Data = ToViewSettings(user.Settings);
return result;
}
catch (Exception ex)
@@ -383,7 +369,7 @@ namespace PrivaPub.Services
var usernameExists = false;
if (passwordRecoveryForm.IsEmailDisabled)
{
if (!await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName && u.DeletionDate == null)
if (!await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName && u.DeletedAt == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName],
StatusCodes.Status404NotFound);
@@ -391,7 +377,7 @@ namespace PrivaPub.Services
}
else
{
if (!await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email && u.DeletionDate == null)
if (!await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email && u.DeletedAt == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName],
StatusCodes.Status404NotFound);
@@ -416,12 +402,12 @@ namespace PrivaPub.Services
{
RootUserId = user.ID
};
await emailRecovery.SaveAsync();
await DB.Default.SaveAsync(emailRecovery);
}
var recoveryCodeGenerator = new Password(true, true, true, false, 127);
using var recoveryCodeGenerator = new Password(true, true, true, false, 127);
emailRecovery.RecoveryCode = recoveryCodeGenerator.Next();
await emailRecovery.SaveAsync();
await DB.Default.SaveAsync(emailRecovery);
using (var smtpClient = new SmtpClient())
{
@@ -557,12 +543,12 @@ Eugene from collAnon, following is the password recovery link:
var newHashedPassword = PasswordHasher.Hash(newPasswordForm.NewPassword);
_ = await DB.Update<RootUser>()
.Match(u => u.ID == emailRecovery.RootUserId && u.DeletionDate == null)
_ = await DB.Default.Update<RootUser>()
.Match(u => u.ID == emailRecovery.RootUserId && u.DeletedAt == null)
.Modify(u => u.HashedPassword, newHashedPassword)
.ExecuteAsync();
_ = await DB.DeleteAsync<EmailRecovery>(er => er.RecoveryCode == newPasswordForm.RecoveryCode);
_ = await DB.Default.DeleteAsync<EmailRecovery>(er => er.RecoveryCode == newPasswordForm.RecoveryCode);
return result;
}
@@ -573,5 +559,16 @@ Eugene from collAnon, following is the password recovery link:
}
}
static ViewUserSettings ToViewSettings(RootUserSettings settings) => new()
{
LanguageCode = settings.LanguageCode,
LightThemeIndexColour = settings.LightThemeIndexColour,
DarkThemeIndexColour = settings.DarkThemeIndexColour,
PreferSystemTheming = settings.PreferSystemTheming,
ThemeIsDarkMode = settings.ThemeIsDarkMode,
IconsThemeIndexColour = settings.IconsThemeIndexColour,
ThemeIsDarkGray = settings.ThemeIsDarkGray,
ThemeIsLightGray = settings.ThemeIsLightGray
};
}
}
}