226 lines
No EOL
7.9 KiB
C#
226 lines
No EOL
7.9 KiB
C#
using System.Net;
|
|
using System.Net.Mime;
|
|
using System.Text;
|
|
using AngleSharp.Text;
|
|
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
|
using Iceshrimp.Backend.Core.Configuration;
|
|
using Iceshrimp.Backend.Core.Database;
|
|
using Iceshrimp.Backend.Core.Database.Tables;
|
|
using Iceshrimp.Backend.Core.Extensions;
|
|
using Iceshrimp.Backend.Core.Helpers;
|
|
using Iceshrimp.Backend.Core.Middleware;
|
|
using Iceshrimp.Backend.Core.Services;
|
|
using Iceshrimp.Shared.Schemas.Web;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
using QRCoder;
|
|
|
|
namespace Iceshrimp.Backend.Controllers.Web;
|
|
|
|
[ApiController]
|
|
[Authenticate]
|
|
[Authorize]
|
|
[EnableRateLimiting("sliding")]
|
|
[Route("/api/iceshrimp/settings")]
|
|
[Produces(MediaTypeNames.Application.Json)]
|
|
public class SettingsController(
|
|
DatabaseContext db,
|
|
ImportExportService importExportSvc,
|
|
MetaService meta,
|
|
IOptions<Config.InstanceSection> instance
|
|
) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
public async Task<UserSettingsResponse> GetSettings()
|
|
{
|
|
var settings = await GetOrInitUserSettings();
|
|
return new UserSettingsResponse
|
|
{
|
|
FilterInaccessible = settings.FilterInaccessible,
|
|
PrivateMode = settings.PrivateMode,
|
|
AlwaysMarkSensitive = settings.AlwaysMarkSensitive,
|
|
AutoAcceptFollowed = settings.AutoAcceptFollowed,
|
|
DefaultNoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility,
|
|
DefaultRenoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility,
|
|
TwoFactorEnrolled = settings.TwoFactorEnabled
|
|
};
|
|
}
|
|
|
|
[HttpPut]
|
|
[Consumes(MediaTypeNames.Application.Json)]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
public async Task UpdateSettings(UserSettingsRequest newSettings)
|
|
{
|
|
var settings = await GetOrInitUserSettings();
|
|
|
|
if (newSettings.DefaultRenoteVisibility == NoteVisibility.Specified)
|
|
throw GracefulException.BadRequest("Default renote visibility cannot be 'specified'");
|
|
|
|
settings.FilterInaccessible = newSettings.FilterInaccessible;
|
|
settings.PrivateMode = newSettings.PrivateMode;
|
|
settings.AlwaysMarkSensitive = newSettings.AlwaysMarkSensitive;
|
|
settings.AutoAcceptFollowed = newSettings.AutoAcceptFollowed;
|
|
settings.DefaultNoteVisibility = (Note.NoteVisibility)newSettings.DefaultNoteVisibility;
|
|
settings.DefaultRenoteVisibility = (Note.NoteVisibility)newSettings.DefaultRenoteVisibility;
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
[HttpPost("2fa/enroll")]
|
|
[EnableRateLimiting("auth")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.BadRequest)]
|
|
public async Task<TwoFactorEnrollmentResponse> EnrollTwoFactor()
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
if (user.UserSettings is not { } settings)
|
|
throw new Exception("Failed to get user settings object");
|
|
if (settings.TwoFactorEnabled)
|
|
throw GracefulException.BadRequest("2FA is already enabled.");
|
|
|
|
return await EnrollNewTwoFactorSecret(settings, user);
|
|
}
|
|
|
|
[HttpPost("2fa/reenroll")]
|
|
[EnableRateLimiting("auth")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
|
public async Task<TwoFactorEnrollmentResponse> ReenrollTwoFactor(TwoFactorRequest request)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
if (user.UserSettings is not { } settings)
|
|
throw new Exception("Failed to get user settings object");
|
|
if (!settings.TwoFactorEnabled)
|
|
throw GracefulException.BadRequest("2FA is not enabled.");
|
|
if (settings.TwoFactorSecret is not { } secret)
|
|
throw new Exception("2FA is enabled but no secret is set");
|
|
if (!TotpHelper.Validate(secret, request.Code))
|
|
throw GracefulException.Forbidden("Invalid TOTP");
|
|
|
|
return await EnrollNewTwoFactorSecret(settings, user);
|
|
}
|
|
|
|
[HttpPost("2fa/confirm")]
|
|
[EnableRateLimiting("auth")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
|
public async Task ConfirmTwoFactor(TwoFactorRequest request)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
if (user.UserSettings is not { } settings)
|
|
throw new Exception("Failed to get user settings object");
|
|
if (settings.TwoFactorTempSecret is not { } secret)
|
|
throw GracefulException.BadRequest("No pending 2FA enrollment found");
|
|
if (!TotpHelper.Validate(secret, request.Code))
|
|
throw GracefulException.Forbidden("Invalid TOTP");
|
|
|
|
settings.TwoFactorEnabled = true;
|
|
settings.TwoFactorSecret = secret;
|
|
settings.TwoFactorTempSecret = null;
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
[HttpPost("2fa/disable")]
|
|
[EnableRateLimiting("auth")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
|
public async Task DisableTwoFactor(TwoFactorRequest request)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
if (user.UserSettings is not { } settings)
|
|
throw new Exception("Failed to get user settings object");
|
|
if (!settings.TwoFactorEnabled)
|
|
throw GracefulException.BadRequest("2FA is not enabled.");
|
|
if (settings.TwoFactorSecret is not { } secret)
|
|
throw new Exception("2FA is enabled but no secret is set");
|
|
if (!TotpHelper.Validate(secret, request.Code))
|
|
throw GracefulException.Forbidden("Invalid TOTP");
|
|
|
|
settings.TwoFactorEnabled = false;
|
|
settings.TwoFactorSecret = null;
|
|
settings.TwoFactorTempSecret = null;
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
[HttpPost("export/following")]
|
|
[ProducesResults(HttpStatusCode.OK)]
|
|
[ProducesErrors(HttpStatusCode.BadRequest)]
|
|
public async Task<FileContentResult> ExportFollowing()
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
|
|
var followCount = await db.Followings
|
|
.CountAsync(p => p.FollowerId == user.Id);
|
|
if (followCount < 1)
|
|
throw GracefulException.BadRequest("You do not follow any users");
|
|
|
|
var following = await importExportSvc.ExportFollowingAsync(user);
|
|
|
|
return File(Encoding.UTF8.GetBytes(following), "text/csv", $"following-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.csv");
|
|
}
|
|
|
|
[HttpPost("import/following")]
|
|
[EnableRateLimiting("imports")]
|
|
[ProducesResults(HttpStatusCode.Accepted)]
|
|
public async Task<AcceptedResult> ImportFollowing(IFormFile file)
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
|
|
var reader = new StreamReader(file.OpenReadStream());
|
|
var contents = await reader.ReadToEndAsync();
|
|
|
|
var fqns = contents
|
|
.Split("\n")
|
|
.Where(line => !string.IsNullOrWhiteSpace(line))
|
|
.Select(line => line.SplitCommas().First())
|
|
.Where(fqn => fqn.Contains('@'))
|
|
.ToList();
|
|
|
|
await importExportSvc.ImportFollowingAsync(user, fqns);
|
|
|
|
return Accepted();
|
|
}
|
|
|
|
private async Task<UserSettings> GetOrInitUserSettings()
|
|
{
|
|
var user = HttpContext.GetUserOrFail();
|
|
var settings = user.UserSettings;
|
|
if (settings != null) return settings;
|
|
|
|
settings = new UserSettings { User = user };
|
|
db.Add(settings);
|
|
await db.SaveChangesAsync();
|
|
await db.ReloadEntityAsync(settings);
|
|
return settings;
|
|
}
|
|
|
|
private async Task<TwoFactorEnrollmentResponse> EnrollNewTwoFactorSecret(UserSettings settings, User user)
|
|
{
|
|
settings.TwoFactorTempSecret = TotpHelper.GenerateSecret();
|
|
await db.SaveChangesAsync();
|
|
|
|
var secret = settings.TwoFactorTempSecret;
|
|
var instanceName = await meta.GetAsync(MetaEntity.InstanceName) ?? "Iceshrimp.NET";
|
|
|
|
var label = $"@{user.Username}@{instance.Value.AccountDomain}".Replace(':', '_');
|
|
var issuer = instanceName.Replace(':', '_');
|
|
var url = $"otpauth://totp/{label.UrlEncode()}?secret={secret}&issuer={issuer.UrlEncode()}";
|
|
|
|
using var qrData = QRCodeGenerator.GenerateQrCode(url, QRCodeGenerator.ECCLevel.Default, true, true);
|
|
using var qrPng = new PngByteQRCode(qrData);
|
|
var qrPngBytes = qrPng.GetGraphic(10, false);
|
|
var qrPngBase64 = Convert.ToBase64String(qrPngBytes);
|
|
|
|
return new TwoFactorEnrollmentResponse
|
|
{
|
|
Secret = settings.TwoFactorTempSecret,
|
|
Url = url,
|
|
QrPng = $"data:image/png;base64,{qrPngBase64}"
|
|
};
|
|
}
|
|
} |