Files
step3/api/QuestionsApi/Controllers/ProfileController.cs
hz d69a1fd0e3 Sync text zoom preference through the account instead of local settings
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.
2026-07-23 03:59:02 -05:00

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();
}