From f80e9d00eea02f524f9c6f1473db035299180458 Mon Sep 17 00:00:00 2001 From: Florian Edlmayer Date: Wed, 11 Feb 2026 10:57:03 +0100 Subject: [PATCH] New: User Search and User-Contact UI and Service --- .../Index.razor | 1 + .../UserSearch.razor | 107 ++++++++++++++++ Client/Services/UserContactService.cs | 32 +++++ Client/Startup/ClientStartup.cs | 4 + Server/Services/ServerUserContactService.cs | 114 ++++++++++++++++++ Server/Startup/ServerStartup.cs | 2 +- 6 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 Client/Modules/SZUAbsolventenverein.Module.PremiumArea/UserSearch.razor create mode 100644 Client/Services/UserContactService.cs create mode 100644 Server/Services/ServerUserContactService.cs diff --git a/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/Index.razor b/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/Index.razor index 69bc802..55f4a83 100644 --- a/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/Index.razor +++ b/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/Index.razor @@ -11,6 +11,7 @@
+ @if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { diff --git a/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/UserSearch.razor b/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/UserSearch.razor new file mode 100644 index 0000000..32cbfc3 --- /dev/null +++ b/Client/Modules/SZUAbsolventenverein.Module.PremiumArea/UserSearch.razor @@ -0,0 +1,107 @@ +@using SZUAbsolventenverein.Module.PremiumArea.Services +@using Oqtane.Models +@namespace SZUAbsolventenverein.Module.PremiumArea +@inherits ModuleBase +@inject IUserContactService ContactService +@inject NavigationManager NavigationManager + +@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, "Premium Member")) +{ +

Mitglieder Suche

+ +
+ + +
+ + @if (_searchResults != null) + { + @if (_searchResults.Count == 0) + { +

Keine Mitglieder gefunden.

+ } + else + { +
    + @foreach (var user in _searchResults) + { +
  • + + @user.DisplayName (@user.Username) + + +
  • + } +
+ } + } + + @if (_selectedUser != null) + { +
+
Nachricht an: @_selectedUser.DisplayName
+
+
+ + +
+ + + + @if (!string.IsNullOrEmpty(_statusMsg)) + { +
@_statusMsg
+ } +
+
+ } +} +else +{ +
+ Sie müssen Premium Kunde sein um diese Funktion zu nutzen. +
+} + +@code { + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; + + private string _query; + private List _searchResults; + private User _selectedUser; + private string _messageBody; + private string _statusMsg; + + private async Task Search() + { + if (string.IsNullOrWhiteSpace(_query) || _query.Length < 3) return; + _searchResults = await ContactService.SearchUsersAsync(_query, ModuleState.ModuleId); + _selectedUser = null; + } + + private void InitContact(User user) + { + _selectedUser = user; + _messageBody = ""; + _statusMsg = ""; + } + + private async Task Send() + { + if (string.IsNullOrWhiteSpace(_messageBody)) return; + + try + { + await ContactService.SendMessageAsync(_selectedUser.UserId, _messageBody, ModuleState.ModuleId); + _statusMsg = "Message Sent Successully!"; + // Reset after delay or allow closing + await Task.Delay(2000); + _selectedUser = null; + StateHasChanged(); + } + catch (Exception ex) + { + _statusMsg = "Error sending message: " + ex.Message; + } + } +} diff --git a/Client/Services/UserContactService.cs b/Client/Services/UserContactService.cs new file mode 100644 index 0000000..d6c1873 --- /dev/null +++ b/Client/Services/UserContactService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; + +namespace SZUAbsolventenverein.Module.PremiumArea.Services +{ + public interface IUserContactService + { + Task> SearchUsersAsync(string query, int moduleId); + Task SendMessageAsync(int recipientUserId, string message, int moduleId); + } + + public class UserContactService : ServiceBase, IUserContactService + { + public UserContactService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("UserContact"); + + public async Task> SearchUsersAsync(string query, int moduleId) + { + return await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}/search/{query}?moduleid={moduleId}", EntityNames.Module, moduleId)); + } + + public async Task SendMessageAsync(int recipientUserId, string message, int moduleId) + { + await PostAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/send?recipientId={recipientUserId}&moduleid={moduleId}&message={System.Net.WebUtility.UrlEncode(message)}", EntityNames.Module, moduleId)); + } + } +} diff --git a/Client/Startup/ClientStartup.cs b/Client/Startup/ClientStartup.cs index e74faf9..bbf8b0d 100644 --- a/Client/Startup/ClientStartup.cs +++ b/Client/Startup/ClientStartup.cs @@ -17,6 +17,10 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup { services.AddScoped(); } + if (!services.Any(s => s.ServiceType == typeof(IUserContactService))) + { + services.AddScoped(); + } } } } diff --git a/Server/Services/ServerUserContactService.cs b/Server/Services/ServerUserContactService.cs new file mode 100644 index 0000000..5f16c9d --- /dev/null +++ b/Server/Services/ServerUserContactService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Security; +using Oqtane.Shared; +using SZUAbsolventenverein.Module.PremiumArea.Models; + +namespace SZUAbsolventenverein.Module.PremiumArea.Services +{ + public class ServerUserContactService : IUserContactService + { + private readonly IUserRepository _userRepository; + private readonly INotificationRepository _notificationRepository; + private readonly IUserPermissions _userPermissions; + private readonly ILogManager _logger; + private readonly IHttpContextAccessor _accessor; + private readonly ITenantManager _tenantManager; + private readonly Alias _alias; + + public ServerUserContactService(IUserRepository userRepository, INotificationRepository notificationRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) + { + _userRepository = userRepository; + _notificationRepository = notificationRepository; + _userPermissions = userPermissions; + _logger = logger; + _accessor = accessor; + _tenantManager = tenantManager; + _alias = tenantManager.GetAlias(); + } + + public Task> SearchUsersAsync(string query, int moduleId) + { + // Note: moduleId param added to match Interface if it requires it, or just ignore if interface doesn't have it. + // Client interface: Task> SearchUsersAsync(string query, int moduleId); + // My previous server impl: SearchUsersAsync(string query) -> Mismatch! + // I must match the signature of the Interface defined in Client. + + if (string.IsNullOrWhiteSpace(query) || query.Length < 3) + { + return Task.FromResult(new List()); + } + + if (!_accessor.HttpContext.User.Identity.IsAuthenticated) + { + return Task.FromResult(new List()); + } + + // Try GetUsers() without params first + var users = _userRepository.GetUsers(); + var results = users.Where(u => + (u.DisplayName != null && u.DisplayName.Contains(query, StringComparison.OrdinalIgnoreCase)) || + (u.Username != null && u.Username.Contains(query, StringComparison.OrdinalIgnoreCase)) + ).Take(20).ToList(); + + var sanitized = results.Select(u => new User + { + UserId = u.UserId, + Username = u.Username, + DisplayName = u.DisplayName, + PhotoFileId = u.PhotoFileId + }).ToList(); + + return Task.FromResult(sanitized); + } + + public Task SendMessageAsync(int recipientUserId, string message, int moduleId) + { + var sender = _accessor.HttpContext.User; + if (!sender.Identity.IsAuthenticated) return Task.CompletedTask; + + int senderId = _accessor.HttpContext.GetUserId(); + var recipient = _userRepository.GetUser(recipientUserId); + if (recipient == null) return Task.CompletedTask; + + var notification = new Notification + { + SiteId = _alias.SiteId, + FromUserId = senderId, + ToUserId = recipientUserId, + ToEmail = "", + Subject = "New Message from " + sender.Identity.Name, + Body = message, + ParentId = null, + CreatedOn = DateTime.UtcNow, + IsDelivered = false, + DeliveredOn = null + }; + _notificationRepository.AddNotification(notification); + + var emailNotification = new Notification + { + SiteId = _alias.SiteId, + FromUserId = senderId, + ToUserId = recipientUserId, + ToEmail = recipient.Email, + Subject = $"New Connection Request from {sender.Identity.Name}", + Body = $"Hello {recipient.DisplayName},

{sender.Identity.Name} sent you a message:
{message}


Login to reply.", + ParentId = null, + CreatedOn = DateTime.UtcNow, + IsDelivered = false + }; + _notificationRepository.AddNotification(emailNotification); + + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Message sent from {SenderId} to {RecipientId}", senderId, recipientUserId); + return Task.CompletedTask; + } + } +} diff --git a/Server/Startup/ServerStartup.cs b/Server/Startup/ServerStartup.cs index 9068c53..355725b 100644 --- a/Server/Startup/ServerStartup.cs +++ b/Server/Startup/ServerStartup.cs @@ -23,7 +23,7 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup { services.AddTransient(); services.AddTransient(); - + services.AddTransient(); services.AddDbContextFactory(opt => { }, ServiceLifetime.Transient); } }