0c1d3abd7e
PR Checks / test-and-build (pull_request) Successful in 12m32s
Add sanitized public GM profiles with publication controls, public /gm/{slug} pages, and links from public game surfaces.
Bump version -> 3.5.0
318 lines
12 KiB
Plaintext
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;
|
|
}
|
|
}
|