Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Backend/SorobanSecurityPortalApi/Common/Data/Db.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class Db : DbContext
public DbSet<FileModel> File { get; set; }
public DbSet<CompanyModel> Company { get; set; }
public DbSet<BookmarkModel> Bookmark { get; set; }
public DbSet<MentionModel> Mention { get; set; }
public DbSet<NotificationModel> Notification { get; set; }
public DbSet<ModerationLogModel> ModerationLog { get; set; }
public DbSet<UserProfileModel> UserProfiles { get; set; }
public DbSet<ForumCategoryModel> ForumCategory { get; set; }
Expand Down
86 changes: 85 additions & 1 deletion Backend/SorobanSecurityPortalApi/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ namespace SorobanSecurityPortalApi.Controllers
public class UserController : ControllerBase
{
private readonly IUserService _userService;
private readonly INotificationService _notificationService;
private readonly Config _config;
private readonly UserContextAccessor _userContextAccessor;

public UserController(IUserService userService, Config config, UserContextAccessor userContextAccessor)
public UserController(
IUserService userService,
INotificationService notificationService,
Config config,
UserContextAccessor userContextAccessor)
{
_userService = userService;
_notificationService = notificationService;
_config = config;
_userContextAccessor = userContextAccessor;
}
Expand Down Expand Up @@ -155,5 +161,83 @@ public async Task<IActionResult> ChangeUserPassword([FromBody] ChangePasswordVie
var result = await _userService.ChangePassword(currentUser!, changePasswordViewModel);
return Ok(result);
}

[Authorize]
[HttpGet("search")]
public async Task<IActionResult> SearchUsers([FromQuery] string query, [FromQuery] int limit = 5)
{
if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
{
return BadRequest("Query must be at least 2 characters long.");
}

if (limit > 10)
{
limit = 10; // Max limit
}

var users = await _userService.SearchUsers(query, limit);
return Ok(users);
}

[Authorize]
[HttpGet("notifications")]
public async Task<IActionResult> GetNotifications([FromQuery] bool onlyUnread = false)
{
var currentUser = this.GetLogin();
if (currentUser == null) return Unauthorized();

var currentUserId = await _userContextAccessor.GetLoginIdAsync();
var notifications = await _notificationService.GetNotificationsForUser(currentUserId, onlyUnread);
return Ok(notifications);
}

[Authorize]
[HttpGet("notifications/unread-count")]
public async Task<IActionResult> GetUnreadNotificationCount()
{
var currentUser = this.GetLogin();
if (currentUser == null) return Unauthorized();

var currentUserId = await _userContextAccessor.GetLoginIdAsync();
var count = await _notificationService.GetUnreadCount(currentUserId);
return Ok(new { count });
}

[Authorize]
[HttpPost("notifications/{notificationId}/read")]
public async Task<IActionResult> MarkNotificationAsRead(int notificationId)
{
var currentUser = this.GetLogin();
if (currentUser == null) return Unauthorized();

var currentUserId = await _userContextAccessor.GetLoginIdAsync();
await _notificationService.MarkAsRead(notificationId, currentUserId);
return Ok();
}

[Authorize]
[HttpPost("notifications/mark-all-read")]
public async Task<IActionResult> MarkAllNotificationsAsRead()
{
var currentUser = this.GetLogin();
if (currentUser == null) return Unauthorized();

var currentUserId = await _userContextAccessor.GetLoginIdAsync();
await _notificationService.MarkAllAsRead(currentUserId);
return Ok();
}

[Authorize]
[HttpDelete("notifications/{notificationId}")]
public async Task<IActionResult> DeleteNotification(int notificationId)
{
var currentUser = this.GetLogin();
if (currentUser == null) return Unauthorized();

var currentUserId = await _userContextAccessor.GetLoginIdAsync();
await _notificationService.DeleteNotification(notificationId, currentUserId);
return Ok();
}
}
}
15 changes: 15 additions & 0 deletions Backend/SorobanSecurityPortalApi/Data/Processors/LoginProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ public async Task<LoginModel> Add(LoginModel loginModel)
await db.SaveChangesAsync();
return loginModel;
}

public async Task<List<LoginModel>> SearchUsers(string query, int limit)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var pattern = $"%{query}%";

return await db.Login.AsNoTracking()
.Where(l => l.IsEnabled &&
(EF.Functions.ILike(l.Login, pattern) ||
EF.Functions.ILike(l.FullName, pattern)))
.OrderBy(l => l.Login)
.Take(limit)
.ToListAsync();
}
}

public interface ILoginProcessor
Expand All @@ -108,5 +122,6 @@ public interface ILoginProcessor
Task Update(LoginModel login);
Task Delete(int id);
Task<LoginModel> Add(LoginModel login);
Task<List<LoginModel>> SearchUsers(string query, int limit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using SorobanSecurityPortalApi.Common.Data;
using SorobanSecurityPortalApi.Models.DbModels;
using Microsoft.EntityFrameworkCore;

namespace SorobanSecurityPortalApi.Data.Processors
{
public class MentionProcessor : IMentionProcessor
{
private readonly IDbContextFactory<Db> _dbFactory;

public MentionProcessor(IDbContextFactory<Db> dbFactory)
{
_dbFactory = dbFactory;
}

public async Task<List<MentionModel>> GetMentionsForEntity(string entityType, int entityId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Mention.AsNoTracking()
.Include(m => m.MentionedUser)
.Include(m => m.MentionedBy)
.Where(m => m.EntityType == entityType && m.EntityId == entityId)
.OrderBy(m => m.CreatedAt)
.ToListAsync();
}

public async Task<List<MentionModel>> GetMentionsByUser(int userId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Mention.AsNoTracking()
.Include(m => m.MentionedUser)
.Include(m => m.MentionedBy)
.Where(m => m.MentionedUserId == userId)
.OrderByDescending(m => m.CreatedAt)
.ToListAsync();
}

public async Task CreateMentions(List<MentionModel> mentions)
{
await using var db = await _dbFactory.CreateDbContextAsync();
await db.Mention.AddRangeAsync(mentions);
await db.SaveChangesAsync();
}

public async Task DeleteMentionsForEntity(string entityType, int entityId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var mentions = await db.Mention
.Where(m => m.EntityType == entityType && m.EntityId == entityId)
.ToListAsync();

if (mentions.Any())
{
db.Mention.RemoveRange(mentions);
await db.SaveChangesAsync();
}
}
}

public interface IMentionProcessor
{
Task<List<MentionModel>> GetMentionsForEntity(string entityType, int entityId);
Task<List<MentionModel>> GetMentionsByUser(int userId);
Task CreateMentions(List<MentionModel> mentions);
Task DeleteMentionsForEntity(string entityType, int entityId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using SorobanSecurityPortalApi.Common.Data;
using SorobanSecurityPortalApi.Models.DbModels;
using Microsoft.EntityFrameworkCore;

namespace SorobanSecurityPortalApi.Data.Processors
{
public class NotificationProcessor : INotificationProcessor
{
private readonly IDbContextFactory<Db> _dbFactory;

public NotificationProcessor(IDbContextFactory<Db> dbFactory)
{
_dbFactory = dbFactory;
}

public async Task<List<NotificationModel>> GetNotificationsForUser(int userId, bool onlyUnread = false)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var query = db.Notification.AsNoTracking()
.Include(n => n.Sender)
.Include(n => n.Recipient)
.Where(n => n.RecipientUserId == userId);

if (onlyUnread)
{
query = query.Where(n => !n.IsRead);
}

return await query.OrderByDescending(n => n.CreatedAt).ToListAsync();
}

public async Task<NotificationModel?> GetNotificationById(int notificationId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Notification.AsNoTracking()
.Include(n => n.Sender)
.Include(n => n.Recipient)
.FirstOrDefaultAsync(n => n.Id == notificationId);
}

public async Task CreateNotifications(List<NotificationModel> notifications)
{
await using var db = await _dbFactory.CreateDbContextAsync();
await db.Notification.AddRangeAsync(notifications);
await db.SaveChangesAsync();
}

public async Task MarkAsRead(int notificationId, int userId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var notification = await db.Notification
.FirstOrDefaultAsync(n => n.Id == notificationId && n.RecipientUserId == userId);

if (notification != null)
{
notification.IsRead = true;
await db.SaveChangesAsync();
}
}

public async Task MarkAllAsRead(int userId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var notifications = await db.Notification
.Where(n => n.RecipientUserId == userId && !n.IsRead)
.ToListAsync();

foreach (var notification in notifications)
{
notification.IsRead = true;
}

await db.SaveChangesAsync();
}

public async Task DeleteNotification(int notificationId, int userId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var notification = await db.Notification
.FirstOrDefaultAsync(n => n.Id == notificationId && n.RecipientUserId == userId);

if (notification != null)
{
db.Notification.Remove(notification);
await db.SaveChangesAsync();
}
}

public async Task<int> GetUnreadCount(int userId)
{
await using var db = await _dbFactory.CreateDbContextAsync();
return await db.Notification
.Where(n => n.RecipientUserId == userId && !n.IsRead)
.CountAsync();
}
}

public interface INotificationProcessor
{
Task<List<NotificationModel>> GetNotificationsForUser(int userId, bool onlyUnread = false);
Task<NotificationModel?> GetNotificationById(int notificationId);
Task CreateNotifications(List<NotificationModel> notifications);
Task MarkAsRead(int notificationId, int userId);
Task MarkAllAsRead(int userId);
Task DeleteNotification(int notificationId, int userId);
Task<int> GetUnreadCount(int userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace SorobanSecurityPortalApi.Migrations
{
/// <inheritdoc />
public partial class AddMentions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "mention",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
mentioned_user_id = table.Column<int>(type: "integer", nullable: false),
mentioned_by_user_id = table.Column<int>(type: "integer", nullable: false),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
entity_type = table.Column<string>(type: "text", nullable: false),
entity_id = table.Column<int>(type: "integer", nullable: false),
start_position = table.Column<int>(type: "integer", nullable: false),
end_position = table.Column<int>(type: "integer", nullable: false),
mentioned_username = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_mention", x => x.id);
table.ForeignKey(
name: "fk_mention_login_mentioned_by_user_id",
column: x => x.mentioned_by_user_id,
principalTable: "login",
principalColumn: "login_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_mention_login_mentioned_user_id",
column: x => x.mentioned_user_id,
principalTable: "login",
principalColumn: "login_id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateIndex(
name: "ix_mention_mentioned_by_user_id",
table: "mention",
column: "mentioned_by_user_id");

migrationBuilder.CreateIndex(
name: "ix_mention_mentioned_user_id",
table: "mention",
column: "mentioned_user_id");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "mention");
}
}
}
Loading
Loading