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
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
/// <summary>

View File

@ -285,6 +285,40 @@ public class ClubGifService : IClubGifService
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)
{
var club = await _clubRepository.GetByIdAsync(clubId, ct)

View File

@ -78,34 +78,67 @@
Variant="Variant.Outlined"
Class="mb-2" />
<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>
<MudButtonGroup OverrideStyles="false" Class="mb-4" Style="width: 100%;">
<MudButton Variant="@(_useUrl ? Variant.Outlined : Variant.Filled)"
Color="Color.Primary"
Style="flex: 1;"
OnClick="() => SetUploadMode(false)">
<MudIcon Icon="@Icons.Material.Filled.UploadFile" Class="mr-1" Size="Size.Small" />
Datei
</MudButton>
<MudButton Variant="@(_useUrl ? Variant.Filled : Variant.Outlined)"
Color="Color.Primary"
Style="flex: 1;"
OnClick="() => SetUploadMode(true)">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-1" Size="Size.Small" />
URL
</MudButton>
</MudButtonGroup>
@if (_useUrl)
{
<MudTextField @bind-Value="_url"
Label="URL zum GIF/Video"
Placeholder="https://example.com/animation.gif"
Required="true"
Variant="Variant.Outlined"
InputType="InputType.Url"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Link"
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)
{
@ -167,8 +200,33 @@
private bool _isSubmitting;
private bool _isSubmitted;
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()
{
@ -208,20 +266,27 @@
private async Task SubmitGif()
{
if (!CanSubmit || _selectedFile == null) return;
if (!CanSubmit) return;
_isSubmitting = true;
_error = null;
try
{
using var stream = _selectedFile.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
if (_useUrl)
{
await GifService.SubmitAnonymousFromUrlAsync(Token, _url, _name);
}
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);
await GifService.SubmitAnonymousAsync(Token, formFile, _name);
var formFile = new FormFileFromStream(ms, _selectedFile.Name, _selectedFile.ContentType);
await GifService.SubmitAnonymousAsync(Token, formFile, _name);
}
_isSubmitted = true;
Snackbar.Add("GIF erfolgreich eingereicht!", Severity.Success);
@ -244,6 +309,8 @@
_selectedFile = null;
_previewUrl = null;
_error = null;
_url = "";
_useUrl = false;
}
private static string FormatFileSize(long bytes)