Files
GmRelayBot/src/GmRelay.Web/Components/Pages/Profile.razor
T
Toutsu 0c1d3abd7e
PR Checks / test-and-build (pull_request) Successful in 12m32s
feat(web): add public master profiles
Add sanitized public GM profiles with publication controls, public /gm/{slug} pages, and links from public game surfaces.

Bump version -> 3.5.0
2026-05-29 00:08:14 +03:00

318 lines
12 KiB
Plaintext

@page "/profile"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.Extensions.Configuration
@attribute [Authorize]
@inject ISessionStore SessionStore
@inject AuthorizedSessionService AuthorizedSessionService
@inject IConfiguration Configuration
@inject NavigationManager Navigation
<PageTitle>Профиль — GM-Relay</PageTitle>
<div class="profile-container">
<h1 class="page-title">Профиль</h1>
@if (masterProfile is not null)
{
<div class="profile-card master-profile-card">
<div class="profile-card-header">
<div>
<h2 class="section-title">Публичный профиль мастера</h2>
<p class="muted-text">Показывается в каталоге, опубликованных играх и публичных страницах клуба.</p>
</div>
<span class="identity-badge">@(masterProfile.IsPublic ? "Публичный" : "Скрыт")</span>
</div>
<EditForm Model="@masterProfileModel" OnValidSubmit="SaveMasterProfile">
<div class="gm-form-group public-toggle-field">
<label class="gm-checkbox-label">
<InputCheckbox @bind-Value="masterProfileModel.IsPublic" />
<span>Опубликовать профиль</span>
</label>
</div>
<div class="profile-form-grid">
<div class="gm-form-group">
<label class="gm-form-label">Имя в публичном профиле</label>
<InputText @bind-Value="masterProfileModel.DisplayName" class="gm-form-control" />
</div>
<div class="gm-form-group">
<label class="gm-form-label">Короткий адрес</label>
<InputText @bind-Value="masterProfileModel.PublicSlug" class="gm-form-control" />
<div class="gm-form-hint">Латиница, цифры и дефисы, например `night-city-gm`.</div>
</div>
</div>
<div class="gm-form-group">
<label class="gm-form-label">Описание</label>
<InputTextArea @bind-Value="masterProfileModel.Bio" class="gm-form-control master-profile-bio" />
</div>
<div class="public-settings-actions">
<button type="submit" class="btn-gm btn-gm-primary" disabled="@savingMasterProfile">
@(savingMasterProfile ? "Сохраняем..." : "Сохранить профиль")
</button>
@if (PublicMasterProfileUrl is not null)
{
<a href="@PublicMasterProfileUrl" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline">
Открыть публичный профиль
</a>
}
</div>
</EditForm>
@if (PublicMasterProfileUrl is not null)
{
<div class="public-link-row">
<span>Ссылка профиля</span>
<a href="@PublicMasterProfileUrl" target="_blank" rel="noopener noreferrer">@PublicMasterProfileUrl</a>
</div>
}
</div>
}
@if (identities is null)
{
<p class="loading-text">Загрузка...</p>
}
else if (identities.Count == 0)
{
<div class="profile-card">
<p>Связанные аккаунты не найдены.</p>
</div>
}
else
{
<div class="profile-card">
<h2 class="section-title">Связанные аккаунты</h2>
<ul class="identity-list">
@foreach (var id in identities)
{
<li class="identity-item">
<div class="identity-info">
<span class="identity-platform">@id.Platform</span>
<span class="identity-name">@id.DisplayName</span>
</div>
@if (id.Platform != currentPlatform || id.ExternalUserId != currentExternalUserId)
{
<button class="btn btn-secondary btn-small"
@onclick="() => Unlink(id.Platform, id.ExternalUserId)"
disabled="@isUnlinking">
Отвязать
</button>
}
else
{
<span class="identity-badge">Текущий</span>
}
</li>
}
</ul>
</div>
}
<div class="profile-card">
<h2 class="section-title">Добавить аккаунт</h2>
@if (!HasLinkedPlatform("Discord"))
{
<a href="/auth/discord" class="btn btn-primary">
Привязать Discord
</a>
}
else
{
<p class="muted-text">Discord уже привязан.</p>
}
@if (currentPlatform == "Discord" && !HasLinkedPlatform("Telegram"))
{
var botUsername = Configuration["Telegram__BotUsername"] ?? Configuration["Telegram:BotUsername"];
if (!string.IsNullOrWhiteSpace(botUsername))
{
var authUrl = new Uri(new Uri(Navigation.BaseUri), "auth/telegram").ToString();
var widgetHtml = $"<script async src=\"https://telegram.org/js/telegram-widget.js?22\" data-telegram-login=\"{botUsername}\" data-size=\"large\" data-auth-url=\"{authUrl}\" data-request-access=\"write\"></script>";
<div class="telegram-widget-wrapper">
@((MarkupString)widgetHtml)
</div>
}
}
</div>
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="alert alert-error">@errorMessage</div>
}
@if (!string.IsNullOrWhiteSpace(successMessage))
{
<div class="alert alert-success">@successMessage</div>
}
</div>
@code {
private List<LinkedIdentity>? identities;
private MasterProfileSettings? masterProfile;
private string? currentPlatform;
private string? currentExternalUserId;
private bool isUnlinking;
private bool savingMasterProfile;
private string? errorMessage;
private string? successMessage;
private MasterProfileEditModel masterProfileModel = new();
[CascadingParameter]
private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
[SupplyParameterFromQuery]
public string? Linked { get; set; }
[SupplyParameterFromQuery(Name = "link_error")]
public string? LinkError { get; set; }
protected override async Task OnInitializedAsync()
{
if (AuthenticationStateTask is not null)
{
var authState = await AuthenticationStateTask;
var user = authState.User;
if (user.TryGetPlatformIdentity(out var plat, out var extId))
{
currentPlatform = plat;
currentExternalUserId = extId;
}
}
if (!string.IsNullOrWhiteSpace(Linked))
{
successMessage = $"{Linked} аккаунт успешно привязан!";
}
if (!string.IsNullOrWhiteSpace(LinkError))
{
errorMessage = $"Ошибка привязки: {Uri.UnescapeDataString(LinkError)}";
}
await LoadIdentities();
await LoadMasterProfile();
}
private async Task LoadIdentities()
{
try
{
if (currentPlatform is not null && currentExternalUserId is not null)
{
identities = await SessionStore.GetLinkedIdentitiesAsync(currentPlatform, currentExternalUserId);
}
else
{
identities = [];
}
}
catch (Exception ex)
{
errorMessage = $"Не удалось загрузить аккаунты: {ex.Message}";
}
}
private async Task LoadMasterProfile()
{
try
{
masterProfile = await AuthorizedSessionService.GetMasterProfileSettingsForCurrentUserAsync();
if (masterProfile is not null)
{
masterProfileModel = new MasterProfileEditModel
{
DisplayName = masterProfile.DisplayName,
PublicSlug = masterProfile.PublicSlug ?? string.Empty,
IsPublic = masterProfile.IsPublic,
Bio = masterProfile.Bio ?? string.Empty
};
}
}
catch (Exception ex)
{
errorMessage = $"Не удалось загрузить профиль мастера: {ex.Message}";
}
}
private string? PublicMasterProfileUrl =>
masterProfile?.IsPublic == true && !string.IsNullOrWhiteSpace(masterProfile.PublicSlug)
? Navigation.ToAbsoluteUri($"/gm/{masterProfile.PublicSlug}").ToString()
: null;
private async Task SaveMasterProfile()
{
savingMasterProfile = true;
errorMessage = null;
successMessage = null;
try
{
await AuthorizedSessionService.UpdateMasterProfileSettingsForCurrentUserAsync(
masterProfileModel.PublicSlug,
masterProfileModel.IsPublic,
masterProfileModel.DisplayName,
masterProfileModel.Bio);
successMessage = "Публичный профиль мастера обновлён.";
await LoadMasterProfile();
}
catch (Exception ex)
{
errorMessage = $"Не удалось сохранить профиль мастера: {ex.Message}";
}
finally
{
savingMasterProfile = false;
}
}
private bool HasLinkedPlatform(string platform)
{
return identities?.Any(i => i.Platform == platform) ?? false;
}
private async Task Unlink(string platform, string externalUserId)
{
isUnlinking = true;
errorMessage = null;
successMessage = null;
try
{
if (currentPlatform is null || currentExternalUserId is null)
{
errorMessage = "Не удалось определить текущего пользователя.";
return;
}
await SessionStore.UnlinkIdentityAsync(currentPlatform, currentExternalUserId, platform, externalUserId);
successMessage = $"{platform} аккаунт отвязан.";
await LoadIdentities();
}
catch (InvalidOperationException ex)
{
errorMessage = $"Ошибка отвязки: {ex.Message}";
}
catch (Exception ex)
{
errorMessage = $"Ошибка отвязки: {ex.Message}";
}
finally
{
isUnlinking = false;
}
}
private sealed class MasterProfileEditModel
{
public string DisplayName { get; set; } = string.Empty;
public string PublicSlug { get; set; } = string.Empty;
public bool IsPublic { get; set; }
public string Bio { get; set; } = string.Empty;
}
}