submit gifs by URL:

UI (Submit.razor)
  - Toggle zwischen "Datei" und "URL" Modus
  - URL-Eingabefeld mit Validierung (nur HTTP/HTTPS)
  - Beide Modi teilen Submit-Button und Erfolgsanzeige

  Service (IClubGifService, ClubGifService)
  - Neue Methode SubmitAnonymousFromUrlAsync(token, url, name)
  - Nutzt bestehende SaveGifFromUrlAsync von MediaStorageService

  Serverseitige Validierung (bereits in MediaStorageService vorhanden):
  - Content-Type Prüfung: nur image/gif, video/mp4, video/webm
  - Dateigröße max. 20MB
  - Datei wird nach Download nochmals auf Größe geprüft
  - Ungültige Content-Types werden mit Exception abgelehnt
This commit is contained in:
beo3000 2026-01-01 10:58:02 +01:00
parent 968026729d
commit 3b12a82982
3 changed files with 138 additions and 36 deletions

View File

@ -36,6 +36,7 @@ public interface IClubGifService
// Anonymous Submission // Anonymous Submission
Task<ClubGifDto> SubmitAnonymousAsync(string token, IFormFile file, string name, CancellationToken ct = default); Task<ClubGifDto> SubmitAnonymousAsync(string token, IFormFile file, string name, CancellationToken ct = default);
Task<ClubGifDto> SubmitAnonymousFromUrlAsync(string token, string url, string name, CancellationToken ct = default);
// Template Seeding // Template Seeding
/// <summary> /// <summary>

View File

@ -285,6 +285,40 @@ public class ClubGifService : IClubGifService
return MapToDto(gif, club.LoginName); return MapToDto(gif, club.LoginName);
} }
public async Task<ClubGifDto> SubmitAnonymousFromUrlAsync(string token, string url, string name, CancellationToken ct = default)
{
var tokenEntity = await _repository.GetSubmissionTokenAsync(token, ct)
?? throw new ArgumentException("Invalid or expired token");
if (!tokenEntity.IsValid)
throw new ArgumentException("Token is expired or usage limit reached");
var club = await _clubRepository.GetByIdAsync(tokenEntity.ClubId, ct)
?? throw new ArgumentException("Club not found");
var (fileName, contentType) = await _mediaStorage.SaveGifFromUrlAsync(club.LoginName, url, ct);
var fileInfo = new FileInfo(Path.Combine("wwwroot", "club-media", club.LoginName, "gifs", fileName));
var gif = new ClubGif
{
ClubId = tokenEntity.ClubId,
Name = name,
FileName = fileName,
OriginalFileName = Path.GetFileName(new Uri(url).LocalPath),
ContentType = contentType,
FileSizeBytes = fileInfo.Exists ? fileInfo.Length : 0,
SourceUrl = url,
AssignedEvents = ThrowEventType.None,
IsEnabled = false,
IsPendingApproval = true
};
gif = await _repository.AddAsync(gif, ct);
await _repository.IncrementSubmissionCountAsync(tokenEntity.Id, ct);
return MapToDto(gif, club.LoginName);
}
public async Task<int> SeedTemplateGifsAsync(Guid clubId, CancellationToken ct = default) public async Task<int> SeedTemplateGifsAsync(Guid clubId, CancellationToken ct = default)
{ {
var club = await _clubRepository.GetByIdAsync(clubId, ct) var club = await _clubRepository.GetByIdAsync(clubId, ct)

View File

@ -78,34 +78,67 @@
Variant="Variant.Outlined" Variant="Variant.Outlined"
Class="mb-2" /> Class="mb-2" />
<MudFileUpload T="IBrowserFile" <MudButtonGroup OverrideStyles="false" Class="mb-4" Style="width: 100%;">
Accept=".gif,.mp4,.webm" <MudButton Variant="@(_useUrl ? Variant.Outlined : Variant.Filled)"
FilesChanged="OnFileSelected" Color="Color.Primary"
MaximumFileCount="1" Style="flex: 1;"
Class="mb-4"> OnClick="() => SetUploadMode(false)">
<ActivatorContent> <MudIcon Icon="@Icons.Material.Filled.UploadFile" Class="mr-1" Size="Size.Small" />
<MudPaper Outlined="true" Datei
Class="pa-4 d-flex flex-column align-center justify-center" </MudButton>
Style="border-style: dashed; min-height: 120px; cursor: pointer;"> <MudButton Variant="@(_useUrl ? Variant.Filled : Variant.Outlined)"
@if (_selectedFile != null) Color="Color.Primary"
{ Style="flex: 1;"
<MudIcon Icon="@Icons.Material.Filled.InsertDriveFile" Size="Size.Large" Color="Color.Success" /> OnClick="() => SetUploadMode(true)">
<MudText Typo="Typo.body1">@_selectedFile.Name</MudText> <MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-1" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary"> URL
@FormatFileSize(_selectedFile.Size) </MudButton>
</MudText> </MudButtonGroup>
}
else @if (_useUrl)
{ {
<MudIcon Icon="@Icons.Material.Filled.CloudUpload" Size="Size.Large" Color="Color.Primary" /> <MudTextField @bind-Value="_url"
<MudText Typo="Typo.body1">Datei auswaehlen</MudText> Label="URL zum GIF/Video"
<MudText Typo="Typo.caption" Color="Color.Secondary"> Placeholder="https://example.com/animation.gif"
GIF, MP4 oder WebM (max. 20MB) Required="true"
</MudText> Variant="Variant.Outlined"
} InputType="InputType.Url"
</MudPaper> Adornment="Adornment.Start"
</ActivatorContent> AdornmentIcon="@Icons.Material.Filled.Link"
</MudFileUpload> Class="mb-2"
HelperText="Direkt-Link zu GIF, MP4 oder WebM" />
}
else
{
<MudFileUpload T="IBrowserFile"
Accept=".gif,.mp4,.webm"
FilesChanged="OnFileSelected"
MaximumFileCount="1"
Class="mb-4">
<ActivatorContent>
<MudPaper Outlined="true"
Class="pa-4 d-flex flex-column align-center justify-center"
Style="border-style: dashed; min-height: 120px; cursor: pointer;">
@if (_selectedFile != null)
{
<MudIcon Icon="@Icons.Material.Filled.InsertDriveFile" Size="Size.Large" Color="Color.Success" />
<MudText Typo="Typo.body1">@_selectedFile.Name</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@FormatFileSize(_selectedFile.Size)
</MudText>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.CloudUpload" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1">Datei auswaehlen</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
GIF, MP4 oder WebM (max. 20MB)
</MudText>
}
</MudPaper>
</ActivatorContent>
</MudFileUpload>
}
@if (_previewUrl != null) @if (_previewUrl != null)
{ {
@ -167,8 +200,33 @@
private bool _isSubmitting; private bool _isSubmitting;
private bool _isSubmitted; private bool _isSubmitted;
private string? _error; private string? _error;
private bool _useUrl;
private string _url = "";
private bool CanSubmit => !string.IsNullOrWhiteSpace(_name) && _selectedFile != null; private bool CanSubmit => !string.IsNullOrWhiteSpace(_name) &&
(_useUrl ? IsValidUrl(_url) : _selectedFile != null);
private static bool IsValidUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
return Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
private void SetUploadMode(bool useUrl)
{
_useUrl = useUrl;
_error = null;
if (useUrl)
{
_selectedFile = null;
_previewUrl = null;
}
else
{
_url = "";
}
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -208,20 +266,27 @@
private async Task SubmitGif() private async Task SubmitGif()
{ {
if (!CanSubmit || _selectedFile == null) return; if (!CanSubmit) return;
_isSubmitting = true; _isSubmitting = true;
_error = null; _error = null;
try try
{ {
using var stream = _selectedFile.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024); if (_useUrl)
using var ms = new MemoryStream(); {
await stream.CopyToAsync(ms); await GifService.SubmitAnonymousFromUrlAsync(Token, _url, _name);
ms.Position = 0; }
else if (_selectedFile != null)
{
using var stream = _selectedFile.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
var formFile = new FormFileFromStream(ms, _selectedFile.Name, _selectedFile.ContentType); var formFile = new FormFileFromStream(ms, _selectedFile.Name, _selectedFile.ContentType);
await GifService.SubmitAnonymousAsync(Token, formFile, _name); await GifService.SubmitAnonymousAsync(Token, formFile, _name);
}
_isSubmitted = true; _isSubmitted = true;
Snackbar.Add("GIF erfolgreich eingereicht!", Severity.Success); Snackbar.Add("GIF erfolgreich eingereicht!", Severity.Success);
@ -244,6 +309,8 @@
_selectedFile = null; _selectedFile = null;
_previewUrl = null; _previewUrl = null;
_error = null; _error = null;
_url = "";
_useUrl = false;
} }
private static string FormatFileSize(long bytes) private static string FormatFileSize(long bytes)