Local user://settings.cfg meant zoom level didn't follow a student across devices/browsers. Question bank and CCS text scale now live on the user's study profile in Mongo (QuestionTextScale/CcsTextScale), synced through a new PUT /api/profile/me/text-scale endpoint and the existing preferences GET/cache already used for study preferences. Both question_player.gd and ccs_service.gd read the synced value at startup and react to AccountProfileService.profile_changed so a value pulled down after login (or changed from another session) applies without a restart. Backend validates the 0.85-1.35 range and updates only the field(s) provided via a targeted $set, so it can't clobber the other scale or the unrelated study-profile preferences.
67 lines
2.4 KiB
C#
67 lines
2.4 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using QuestionsApi.Models;
|
|
using QuestionsApi.Services;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace QuestionsApi.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/profile")]
|
|
public sealed class ProfileController(UserProfileStore profiles, IConfiguration configuration) : ControllerBase
|
|
{
|
|
[HttpGet("me/preferences")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> GetPreferences()
|
|
{
|
|
var profile = await profiles.GetAsync(UserId());
|
|
return Ok(profile ?? new UserStudyProfile { UserId = UserId() });
|
|
}
|
|
|
|
[HttpPut("me/preferences")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> UpdatePreferences(UpdateUserStudyProfileRequest request)
|
|
{
|
|
if (UserStudyProfileValidator.Validate(request) is { } error) return BadRequest(error);
|
|
return Ok(await profiles.UpsertAsync(UserId(), request));
|
|
}
|
|
|
|
[HttpPut("me/text-scale")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> UpdateTextScale(UpdateTextScaleRequest request)
|
|
{
|
|
if (UpdateTextScaleRequestValidator.Validate(request) is { } error) return BadRequest(error);
|
|
return Ok(await profiles.UpdateTextScaleAsync(UserId(), request));
|
|
}
|
|
|
|
[HttpGet("me/export")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> Export() => Ok(new
|
|
{
|
|
schemaVersion = 1,
|
|
exportedUtc = DateTime.UtcNow,
|
|
userId = UserId(),
|
|
records = await profiles.ExportAsync(UserId())
|
|
});
|
|
|
|
[HttpDelete("internal/users/{userId}")]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> Purge(string userId)
|
|
{
|
|
var expected = configuration["InternalPurge:Secret"];
|
|
var supplied = Request.Headers["X-MedMind-Internal-Key"].FirstOrDefault();
|
|
if (string.IsNullOrWhiteSpace(expected) || !CryptographicOperations.FixedTimeEquals(
|
|
System.Text.Encoding.UTF8.GetBytes(expected),
|
|
System.Text.Encoding.UTF8.GetBytes(supplied ?? "")))
|
|
return Unauthorized();
|
|
await profiles.PurgeAsync(userId);
|
|
return NoContent();
|
|
}
|
|
|
|
private string UserId() => User.FindFirstValue(ClaimTypes.NameIdentifier)
|
|
?? User.FindFirstValue(ClaimTypes.Name)
|
|
?? User.Identity?.Name
|
|
?? throw new UnauthorizedAccessException();
|
|
}
|