Reorganize root folder structure: Remove apps layer layer
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
public interface IIdentityDbContext
|
||||
{
|
||||
DbSet<User> Users { get; }
|
||||
DbSet<Friendship> Friendships { get; }
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work специфичный для модуля Identity.
|
||||
/// </summary>
|
||||
public interface IIdentityUnitOfWork : IUnitOfWork
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
public interface IJwtTokenProvider
|
||||
{
|
||||
string Generate(User user);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record AcceptFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand<Guid>;
|
||||
|
||||
internal sealed class AcceptFriendRequestCommandHandler : ICommandHandler<AcceptFriendRequestCommand, Guid>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public AcceptFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_context = context;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Guid>> Handle(AcceptFriendRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
|
||||
if (friendship == null || friendship.FriendId != request.UserId)
|
||||
{
|
||||
return Result.Failure<Guid>(IdentityErrors.FriendsNotFound);
|
||||
}
|
||||
|
||||
friendship.Accept();
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success(friendship.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record DeclineFriendRequestCommand(Guid UserId, Guid FriendshipId) : ICommand;
|
||||
|
||||
internal sealed class DeclineFriendRequestCommandHandler : ICommandHandler<DeclineFriendRequestCommand>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public DeclineFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_context = context;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(DeclineFriendRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
|
||||
if (friendship == null || friendship.FriendId != request.UserId)
|
||||
{
|
||||
return Result.Failure(IdentityErrors.FriendsNotFound);
|
||||
}
|
||||
|
||||
friendship.Decline();
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record FriendDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
bool IsOnline,
|
||||
DateTime LastSeenAt,
|
||||
Guid FriendshipId
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record FriendUserDto(Guid Id, string Username, string DisplayName, string? Avatar);
|
||||
|
||||
public record FriendRequestDto(
|
||||
Guid Id,
|
||||
FriendUserDto User,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record FriendshipStatusResponse(
|
||||
string Status,
|
||||
Guid? FriendshipId = null,
|
||||
string? Direction = null
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record GetFriendsQuery(Guid UserId) : IQuery<List<FriendDto>>;
|
||||
|
||||
internal sealed class GetFriendsQueryHandler : IQueryHandler<GetFriendsQuery, List<FriendDto>>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetFriendsQueryHandler(IIdentityDbContext context, IUserRepository userRepository)
|
||||
{
|
||||
_context = context;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<FriendDto>>> Handle(GetFriendsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendships = await _context.Friendships
|
||||
.Where(f => (f.UserId == request.UserId || f.FriendId == request.UserId) && f.Status == FriendshipStatus.Accepted)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var friendIds = friendships.Select(f => f.UserId == request.UserId ? f.FriendId : f.UserId).ToList();
|
||||
var friends = new List<FriendDto>();
|
||||
|
||||
foreach (var id in friendIds)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fs = friendships.First(f => f.UserId == id || f.FriendId == id);
|
||||
|
||||
friends.Add(new FriendDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
false, // IsOnline logic shouldn't be here, but we'll leave default for now
|
||||
DateTime.UtcNow,
|
||||
fs.Id
|
||||
));
|
||||
}
|
||||
|
||||
return Result.Success(friends);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record GetFriendshipStatusQuery(Guid CurrentUserId, Guid TargetUserId) : IQuery<FriendshipStatusResponse>;
|
||||
|
||||
internal sealed class GetFriendshipStatusQueryHandler : IQueryHandler<GetFriendshipStatusQuery, FriendshipStatusResponse>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
|
||||
public GetFriendshipStatusQueryHandler(IIdentityDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Result<FriendshipStatusResponse>> Handle(GetFriendshipStatusQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.CurrentUserId == request.TargetUserId)
|
||||
{
|
||||
return Result.Success(new FriendshipStatusResponse("self"));
|
||||
}
|
||||
|
||||
var fs = await _context.Friendships
|
||||
.FirstOrDefaultAsync(f => (f.UserId == request.CurrentUserId && f.FriendId == request.TargetUserId) ||
|
||||
(f.UserId == request.TargetUserId && f.FriendId == request.CurrentUserId), cancellationToken);
|
||||
|
||||
if (fs == null)
|
||||
{
|
||||
return Result.Success(new FriendshipStatusResponse("none"));
|
||||
}
|
||||
|
||||
return Result.Success(new FriendshipStatusResponse(
|
||||
fs.Status.ToString().ToLowerInvariant(),
|
||||
fs.Id,
|
||||
fs.UserId == request.CurrentUserId ? "outgoing" : "incoming"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record GetIncomingRequestsQuery(Guid UserId) : IQuery<List<FriendRequestDto>>;
|
||||
|
||||
internal sealed class GetIncomingRequestsQueryHandler : IQueryHandler<GetIncomingRequestsQuery, List<FriendRequestDto>>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetIncomingRequestsQueryHandler(IIdentityDbContext context, IUserRepository userRepository)
|
||||
{
|
||||
_context = context;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<FriendRequestDto>>> Handle(GetIncomingRequestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendships = await _context.Friendships
|
||||
.Where(f => f.FriendId == request.UserId && f.Status == FriendshipStatus.Pending)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var requestsList = new List<FriendRequestDto>();
|
||||
foreach (var fs in friendships)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(fs.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
requestsList.Add(new FriendRequestDto(
|
||||
fs.Id,
|
||||
new FriendUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
|
||||
fs.CreatedAt
|
||||
));
|
||||
}
|
||||
return Result.Success(requestsList);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record GetOutgoingRequestsQuery(Guid UserId) : IQuery<List<FriendRequestDto>>;
|
||||
|
||||
internal sealed class GetOutgoingRequestsQueryHandler : IQueryHandler<GetOutgoingRequestsQuery, List<FriendRequestDto>>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetOutgoingRequestsQueryHandler(IIdentityDbContext context, IUserRepository userRepository)
|
||||
{
|
||||
_context = context;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<FriendRequestDto>>> Handle(GetOutgoingRequestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendships = await _context.Friendships
|
||||
.Where(f => f.UserId == request.UserId && f.Status == FriendshipStatus.Pending)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var requestsList = new List<FriendRequestDto>();
|
||||
foreach (var fs in friendships)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(fs.FriendId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
requestsList.Add(new FriendRequestDto(
|
||||
fs.Id,
|
||||
new FriendUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
|
||||
fs.CreatedAt
|
||||
));
|
||||
}
|
||||
return Result.Success(requestsList);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record RemoveFriendCommand(Guid UserId, Guid FriendshipId) : ICommand;
|
||||
|
||||
internal sealed class RemoveFriendCommandHandler : ICommandHandler<RemoveFriendCommand>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public RemoveFriendCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_context = context;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(RemoveFriendCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
|
||||
if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId))
|
||||
{
|
||||
return Result.Failure(IdentityErrors.FriendsNotFound);
|
||||
}
|
||||
|
||||
_context.Friendships.Remove(friendship);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Friends;
|
||||
|
||||
public record SendFriendRequestCommand(Guid UserId, Guid FriendId) : ICommand;
|
||||
|
||||
internal sealed class SendFriendRequestCommandHandler : ICommandHandler<SendFriendRequestCommand>
|
||||
{
|
||||
private readonly IIdentityDbContext _context;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public SendFriendRequestCommandHandler(IIdentityDbContext context, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_context = context;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(SendFriendRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.UserId == request.FriendId)
|
||||
{
|
||||
return Result.Failure(IdentityErrors.FriendsSelf);
|
||||
}
|
||||
|
||||
var existing = await _context.Friendships
|
||||
.FirstOrDefaultAsync(f => (f.UserId == request.UserId && f.FriendId == request.FriendId) ||
|
||||
(f.UserId == request.FriendId && f.FriendId == request.UserId), cancellationToken);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
return Result.Failure(IdentityErrors.FriendsExists);
|
||||
}
|
||||
|
||||
var friendship = Friendship.Create(request.UserId, request.FriendId);
|
||||
_context.Friendships.Add(friendship);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Auth;
|
||||
|
||||
public record AuthResponseDto(
|
||||
string Token,
|
||||
AuthUserDto User
|
||||
);
|
||||
|
||||
public record AuthUserDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Email,
|
||||
string? Bio,
|
||||
string? Avatar,
|
||||
DateTime? Birthday,
|
||||
bool IsOnline,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Avatar;
|
||||
|
||||
public sealed record CropAvatarCommand(Guid UserId, Stream FileStream, string FileName, string ContentType, int X, int Y, int Width, int Height) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
|
||||
public CropAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork, IFileStorageService fileStorage)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(CropAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
string avatarUrl;
|
||||
|
||||
using (var image = await SixLabors.ImageSharp.Image.LoadAsync(request.FileStream, cancellationToken))
|
||||
{
|
||||
int startX = Math.Max(0, Math.Min(request.X, image.Width - 1));
|
||||
int startY = Math.Max(0, Math.Min(request.Y, image.Height - 1));
|
||||
int rectWidth = Math.Max(1, Math.Min(request.Width, image.Width - startX));
|
||||
int rectHeight = Math.Max(1, Math.Min(request.Height, image.Height - startY));
|
||||
|
||||
image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight)));
|
||||
image.Mutate(ctx => ctx.Resize(400, 400));
|
||||
|
||||
using var outStream = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(outStream, cancellationToken);
|
||||
outStream.Position = 0;
|
||||
|
||||
var id = await _fileStorage.UploadFileAsync(outStream, request.FileName, "image/jpeg");
|
||||
avatarUrl = $"/api/files/{id}";
|
||||
}
|
||||
|
||||
user.UpdateAvatar(avatarUrl);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Avatar;
|
||||
|
||||
public sealed record DeleteAvatarCommand(Guid UserId) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public DeleteAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
user.UpdateAvatar(null);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Avatar;
|
||||
|
||||
public sealed record UploadAvatarCommand(Guid UserId, Stream FileStream, string FileName, string ContentType) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
|
||||
public UploadAvatarCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork, IFileStorageService fileStorage)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
|
||||
var avatarUrl = $"/api/files/{fileId}";
|
||||
|
||||
user.UpdateAvatar(avatarUrl);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.GetMe;
|
||||
|
||||
public sealed record GetMeQuery(Guid UserId) : IQuery<AuthResponseDto>;
|
||||
|
||||
internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponseDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetMeQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<AuthResponseDto>> Handle(GetMeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var response = new AuthResponseDto(
|
||||
string.Empty,
|
||||
new AuthUserDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Email,
|
||||
user.Bio,
|
||||
user.Avatar,
|
||||
user.Birthday,
|
||||
true,
|
||||
user.CreatedAt
|
||||
)
|
||||
);
|
||||
|
||||
return Result.Success(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.GetUser;
|
||||
|
||||
public sealed record GetUserQuery(Guid Id) : IQuery<UserProfileDto>;
|
||||
|
||||
internal sealed class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetUserQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(GetUserQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt,
|
||||
null,
|
||||
false,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Login;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для входа пользователя. Возвращает AuthResponseDto.
|
||||
/// </summary>
|
||||
public sealed record LoginUserCommand(string Username, string Password) : ICommand<AuthResponseDto>;
|
||||
|
||||
public sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand, AuthResponseDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IJwtTokenProvider _tokenProvider;
|
||||
|
||||
public LoginUserCommandHandler(IUserRepository userRepository, IJwtTokenProvider tokenProvider)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_tokenProvider = tokenProvider;
|
||||
}
|
||||
|
||||
public async Task<Result<AuthResponseDto>> Handle(LoginUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByUsernameAsync(request.Username, cancellationToken);
|
||||
|
||||
if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityInvalidCredentials);
|
||||
}
|
||||
|
||||
string token = _tokenProvider.Generate(user);
|
||||
|
||||
return Result.Success(new AuthResponseDto(
|
||||
token,
|
||||
new AuthUserDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Email,
|
||||
user.Bio,
|
||||
user.Avatar,
|
||||
user.Birthday,
|
||||
true, // IsOnline (placeholder)
|
||||
user.CreatedAt
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Modules.Identity.Application.Users.Auth;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Register;
|
||||
|
||||
/// <summary>
|
||||
/// Команда для регистрации нового пользователя.
|
||||
/// </summary>
|
||||
public sealed record RegisterUserCommand(
|
||||
string Username,
|
||||
string Password,
|
||||
string DisplayName,
|
||||
string? Email,
|
||||
string? Bio) : ICommand<AuthResponseDto>;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик команды регистрации.
|
||||
/// </summary>
|
||||
public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand, AuthResponseDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
private readonly ISettingsService _settings;
|
||||
private readonly IJwtTokenProvider _tokenProvider;
|
||||
|
||||
public RegisterUserCommandHandler(
|
||||
IUserRepository userRepository,
|
||||
IIdentityUnitOfWork unitOfWork,
|
||||
ISettingsService settings,
|
||||
IJwtTokenProvider tokenProvider)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_settings = settings;
|
||||
_tokenProvider = tokenProvider;
|
||||
}
|
||||
|
||||
public async Task<Result<AuthResponseDto>> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_settings.Current.EnableRegistration)
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityRegistrationDisabled);
|
||||
}
|
||||
|
||||
// 1. Проверка уникальности username
|
||||
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
||||
{
|
||||
return Result.Failure<AuthResponseDto>(IdentityErrors.IdentityUsernameNotUnique);
|
||||
}
|
||||
|
||||
// 2. Хеширование пароля (здесь будет вызов сервиса, пока заглушка)
|
||||
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
||||
|
||||
// 3. Создание сущности
|
||||
var user = User.Create(
|
||||
request.Username,
|
||||
passwordHash,
|
||||
request.DisplayName,
|
||||
request.Email,
|
||||
request.Bio);
|
||||
|
||||
// 4. Сохранение
|
||||
_userRepository.Add(user);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
string token = _tokenProvider.Generate(user);
|
||||
|
||||
return Result.Success(new AuthResponseDto(
|
||||
token,
|
||||
new AuthUserDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Email,
|
||||
user.Bio,
|
||||
user.Avatar,
|
||||
user.Birthday,
|
||||
true, // IsOnline (placeholder)
|
||||
user.CreatedAt
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.Search;
|
||||
|
||||
public sealed record SearchUsersQuery(string Query) : IQuery<List<UserDto>>;
|
||||
|
||||
internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery, List<UserDto>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public SearchUsersQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<List<UserDto>>> Handle(SearchUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var users = await _userRepository.SearchUsersAsync(request.Query, cancellationToken);
|
||||
|
||||
var result = users.Select(user => new UserDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
false,
|
||||
DateTime.UtcNow
|
||||
)).ToList();
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.UpdateProfile;
|
||||
|
||||
public sealed record UpdateProfileCommand(Guid UserId, string? DisplayName, string? Bio, DateTime? Birthday) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfileCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public UpdateProfileCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users.UpdateSettings;
|
||||
|
||||
public sealed record UpdateSettingsCommand(Guid UserId, bool? HideStoryViews) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSettingsCommand, UserProfileDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IIdentityUnitOfWork _unitOfWork;
|
||||
|
||||
public UpdateSettingsCommandHandler(IUserRepository userRepository, IIdentityUnitOfWork unitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<UserProfileDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var dto = new UserProfileDto(
|
||||
user.Id,
|
||||
user.Username,
|
||||
user.DisplayName,
|
||||
user.Avatar,
|
||||
user.Bio,
|
||||
user.Birthday,
|
||||
user.CreatedAt,
|
||||
user.HideStoryViews
|
||||
);
|
||||
|
||||
return Result.Success(dto);
|
||||
}
|
||||
}
|
||||
12
backend/src/Modules/Identity/Application/Users/UserDto.cs
Normal file
12
backend/src/Modules/Identity/Application/Users/UserDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users;
|
||||
|
||||
public record UserDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
bool IsOnline,
|
||||
DateTime LastSeen
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Identity.Application.Users;
|
||||
|
||||
public record UserProfileDto(
|
||||
Guid Id,
|
||||
string Username,
|
||||
string DisplayName,
|
||||
string? Avatar,
|
||||
string? Bio,
|
||||
DateTime? Birthday,
|
||||
DateTime CreatedAt,
|
||||
bool? HideStoryViews = null,
|
||||
bool IsOnline = false,
|
||||
DateTime? LastSeen = null
|
||||
);
|
||||
40
backend/src/Modules/Identity/DependencyInjection.cs
Normal file
40
backend/src/Modules/Identity/DependencyInjection.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Infrastructure.Authentication;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Регистрация сервисов модуля Identity.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddIdentityModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Настройка базы данных
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
services.AddDbContext<IdentityDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Регистрация Unit of Work и Репозиториев
|
||||
services.AddScoped<IIdentityUnitOfWork>(sp => sp.GetRequiredService<IdentityDbContext>());
|
||||
services.AddScoped<IIdentityDbContext>(sp => sp.GetRequiredService<IdentityDbContext>());
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IJwtTokenProvider, JwtTokenProvider>();
|
||||
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Identity.Infrastructure.Services.UserDisplayNameProvider>();
|
||||
|
||||
// Регистрация MediatR для этого модуля
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
34
backend/src/Modules/Identity/Domain/Friendship.cs
Normal file
34
backend/src/Modules/Identity/Domain/Friendship.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public enum FriendshipStatus
|
||||
{
|
||||
Pending,
|
||||
Accepted,
|
||||
Declined
|
||||
}
|
||||
|
||||
public sealed class Friendship : Entity<Guid>
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public Guid FriendId { get; private set; }
|
||||
public FriendshipStatus Status { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
private Friendship(Guid id, Guid userId, Guid friendId, FriendshipStatus status) : base(id)
|
||||
{
|
||||
UserId = userId;
|
||||
FriendId = friendId;
|
||||
Status = status;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static Friendship Create(Guid userId, Guid friendId)
|
||||
{
|
||||
return new Friendship(Guid.NewGuid(), userId, friendId, FriendshipStatus.Pending);
|
||||
}
|
||||
|
||||
public void Accept() => Status = FriendshipStatus.Accepted;
|
||||
public void Decline() => Status = FriendshipStatus.Declined;
|
||||
}
|
||||
17
backend/src/Modules/Identity/Domain/IUserRepository.cs
Normal file
17
backend/src/Modules/Identity/Domain/IUserRepository.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс репозитория для работы с пользователями.
|
||||
/// </summary>
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<List<User>> GetByIdsAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken = default);
|
||||
Task<User?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||
Task<bool> IsUsernameUniqueAsync(string username, CancellationToken cancellationToken = default);
|
||||
Task<List<User>> SearchUsersAsync(string query, CancellationToken cancellationToken = default);
|
||||
void Add(User user);
|
||||
void Update(User user);
|
||||
}
|
||||
14
backend/src/Modules/Identity/Domain/IdentityErrors.cs
Normal file
14
backend/src/Modules/Identity/Domain/IdentityErrors.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public static class IdentityErrors
|
||||
{
|
||||
public static readonly Error FriendsNotFound = new Error("Friends.NotFound", "Friendship not found");
|
||||
public static readonly Error FriendsSelf = new Error("Friends.Self", "Cannot add yourself");
|
||||
public static readonly Error FriendsExists = new Error("Friends.Exists", "Friendship already exists");
|
||||
public static readonly Error UserNotFound = new Error("User.NotFound", "User not found");
|
||||
public static readonly Error IdentityInvalidCredentials = new Error("Identity.InvalidCredentials", "Неверное имя пользователя или пароль.");
|
||||
public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator.");
|
||||
public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято.");
|
||||
}
|
||||
74
backend/src/Modules/Identity/Domain/Story.cs
Normal file
74
backend/src/Modules/Identity/Domain/Story.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public class Story : Entity<Guid>
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public string Type { get; private set; } // text, image, video
|
||||
public string? MediaUrl { get; private set; }
|
||||
public string? Content { get; private set; }
|
||||
public string? BgColor { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime ExpiresAt { get; private set; }
|
||||
|
||||
private readonly List<StoryViewer> _viewers = new();
|
||||
private readonly List<StoryReaction> _reactions = new();
|
||||
private readonly List<StoryReply> _replies = new();
|
||||
|
||||
public IReadOnlyCollection<StoryViewer> Viewers => _viewers.AsReadOnly();
|
||||
public IReadOnlyCollection<StoryReaction> Reactions => _reactions.AsReadOnly();
|
||||
public IReadOnlyCollection<StoryReply> Replies => _replies.AsReadOnly();
|
||||
|
||||
protected Story() : base(Guid.NewGuid()) { Type = string.Empty; }
|
||||
|
||||
internal Story(Guid id, Guid userId, string type, string? mediaUrl, string? content, string? bgColor) : base(id)
|
||||
{
|
||||
UserId = userId;
|
||||
Type = type;
|
||||
MediaUrl = mediaUrl;
|
||||
Content = content;
|
||||
BgColor = bgColor;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
ExpiresAt = CreatedAt.AddDays(1);
|
||||
}
|
||||
|
||||
public static Story Create(Guid userId, string type, string? mediaUrl, string? content, string? bgColor)
|
||||
{
|
||||
return new Story(Guid.NewGuid(), userId, type, mediaUrl, content, bgColor);
|
||||
}
|
||||
|
||||
public void AddViewer(Guid userId)
|
||||
{
|
||||
if (_viewers.Any(v => v.UserId == userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_viewers.Add(new StoryViewer(Id, userId));
|
||||
}
|
||||
|
||||
public void AddReaction(Guid userId, string emoji)
|
||||
{
|
||||
if (_reactions.Any(r => r.UserId == userId && r.Emoji == emoji))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_reactions.Add(new StoryReaction(Id, userId, emoji));
|
||||
}
|
||||
|
||||
public void RemoveReaction(Guid userId, string emoji)
|
||||
{
|
||||
var reaction = _reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji);
|
||||
if (reaction != null)
|
||||
{
|
||||
_reactions.Remove(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddReply(Guid userId, string content)
|
||||
{
|
||||
_replies.Add(new StoryReply(Id, userId, content));
|
||||
}
|
||||
}
|
||||
21
backend/src/Modules/Identity/Domain/StoryReaction.cs
Normal file
21
backend/src/Modules/Identity/Domain/StoryReaction.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public sealed class StoryReaction : Entity<Guid>
|
||||
{
|
||||
public Guid StoryId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Emoji { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
private StoryReaction() : base(Guid.NewGuid()) { }
|
||||
|
||||
public StoryReaction(Guid storyId, Guid userId, string emoji) : base(Guid.NewGuid())
|
||||
{
|
||||
StoryId = storyId;
|
||||
UserId = userId;
|
||||
Emoji = emoji;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
21
backend/src/Modules/Identity/Domain/StoryReply.cs
Normal file
21
backend/src/Modules/Identity/Domain/StoryReply.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public sealed class StoryReply : Entity<Guid>
|
||||
{
|
||||
public Guid StoryId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
private StoryReply() : base(Guid.NewGuid()) { }
|
||||
|
||||
public StoryReply(Guid storyId, Guid userId, string content) : base(Guid.NewGuid())
|
||||
{
|
||||
StoryId = storyId;
|
||||
UserId = userId;
|
||||
Content = content;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
17
backend/src/Modules/Identity/Domain/StoryViewer.cs
Normal file
17
backend/src/Modules/Identity/Domain/StoryViewer.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
public sealed class StoryViewer : Entity<Guid>
|
||||
{
|
||||
public Guid StoryId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
public DateTime ViewedAt { get; private set; }
|
||||
|
||||
internal StoryViewer(Guid storyId, Guid userId) : base(Guid.NewGuid())
|
||||
{
|
||||
StoryId = storyId;
|
||||
UserId = userId;
|
||||
ViewedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
60
backend/src/Modules/Identity/Domain/User.cs
Normal file
60
backend/src/Modules/Identity/Domain/User.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Identity.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Сущность пользователя в контексте идентификации (Identity).
|
||||
/// </summary>
|
||||
public sealed class User : AggregateRoot<Guid>
|
||||
{
|
||||
public string Username { get; private set; }
|
||||
public string PasswordHash { get; private set; }
|
||||
public string DisplayName { get; private set; }
|
||||
public string? Email { get; private set; }
|
||||
public string? Bio { get; private set; }
|
||||
public string? Avatar { get; private set; }
|
||||
public DateTime? Birthday { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public bool HideStoryViews { get; private set; }
|
||||
|
||||
private User(Guid id, string username, string passwordHash, string displayName, string? email, string? bio = null)
|
||||
: base(id)
|
||||
{
|
||||
Username = username;
|
||||
PasswordHash = passwordHash;
|
||||
DisplayName = displayName;
|
||||
Email = email;
|
||||
Bio = bio;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Фабричный метод для создания нового пользователя.
|
||||
/// </summary>
|
||||
public static User Create(string username, string passwordHash, string displayName, string? email = null, string? bio = null)
|
||||
{
|
||||
return new User(Guid.NewGuid(), username, passwordHash, displayName, email, bio);
|
||||
}
|
||||
|
||||
public void UpdateProfile(string displayName, string? bio, DateTime? birthday)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
Bio = bio;
|
||||
Birthday = birthday;
|
||||
}
|
||||
|
||||
public void UpdateAvatar(string? avatarUrl)
|
||||
{
|
||||
Avatar = avatarUrl;
|
||||
}
|
||||
|
||||
public void UpdateSettings(bool hideStoryViews)
|
||||
{
|
||||
HideStoryViews = hideStoryViews;
|
||||
}
|
||||
|
||||
public void ChangePassword(string newPasswordHash)
|
||||
{
|
||||
PasswordHash = newPasswordHash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Authentication;
|
||||
|
||||
public sealed class JwtTokenProvider : IJwtTokenProvider
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtTokenProvider(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public string Generate(User user)
|
||||
{
|
||||
var claims = new Claim[]
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.UniqueName, user.Username),
|
||||
new("name", user.DisplayName),
|
||||
new(ClaimTypes.Name, user.DisplayName),
|
||||
new("avatar", user.Avatar ?? string.Empty)
|
||||
};
|
||||
|
||||
var secretKey = _configuration["Jwt:Secret"]!;
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
_configuration["Jwt:Issuer"],
|
||||
_configuration["Jwt:Audience"],
|
||||
claims,
|
||||
null,
|
||||
DateTime.UtcNow.AddMinutes(double.Parse(_configuration["Jwt:ExpiryInMinutes"] ?? "1440")),
|
||||
credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Knot.Modules.Identity.Application.Abstractions;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Контекст базы данных для модуля Identity.
|
||||
/// </summary>
|
||||
public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork, Knot.Modules.Identity.Application.Abstractions.IIdentityDbContext
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
|
||||
public IdentityDbContext(DbContextOptions<IdentityDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
||||
: base(options)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Story> Stories => Set<Story>();
|
||||
public DbSet<StoryViewer> StoryViewers => Set<StoryViewer>();
|
||||
public DbSet<StoryReaction> StoryReactions => Set<StoryReaction>();
|
||||
public DbSet<StoryReply> StoryReplies => Set<StoryReply>();
|
||||
public DbSet<Friendship> Friendships => Set<Friendship>();
|
||||
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("identity");
|
||||
|
||||
modelBuilder.Entity<Friendship>(builder =>
|
||||
{
|
||||
builder.ToTable("Friendships");
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.HasIndex(f => new { f.UserId, f.FriendId }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Story>(builder =>
|
||||
{
|
||||
builder.ToTable("Stories");
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Type).IsRequired();
|
||||
|
||||
builder.Property(s => s.Content)
|
||||
.HasConversion(
|
||||
v => v == null ? null : _encryptionService.EncryptMessage(v),
|
||||
v => v == null ? null : _encryptionService.DecryptMessage(v)!
|
||||
);
|
||||
|
||||
// Configure backing fields for collections
|
||||
|
||||
builder.Metadata.FindNavigation(nameof(Story.Viewers))?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
builder.Metadata.FindNavigation(nameof(Story.Reactions))?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
builder.Metadata.FindNavigation(nameof(Story.Replies))?.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
|
||||
builder.HasMany(s => s.Viewers)
|
||||
.WithOne()
|
||||
.HasForeignKey(v => v.StoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasMany(s => s.Reactions)
|
||||
.WithOne()
|
||||
.HasForeignKey(r => r.StoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasMany(s => s.Replies)
|
||||
.WithOne()
|
||||
.HasForeignKey(r => r.StoryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<StoryViewer>(builder =>
|
||||
{
|
||||
builder.ToTable("StoryViewers");
|
||||
builder.HasKey(v => v.Id);
|
||||
builder.HasIndex(v => new { v.StoryId, v.UserId }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<StoryReaction>(builder =>
|
||||
{
|
||||
builder.ToTable("StoryReactions");
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.Emoji).IsRequired().HasMaxLength(10);
|
||||
builder.HasIndex(r => new { r.StoryId, r.UserId, r.Emoji }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<StoryReply>(builder =>
|
||||
{
|
||||
builder.ToTable("StoryReplies");
|
||||
builder.HasKey(r => r.Id);
|
||||
|
||||
builder.Property(r => r.Content)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasConversion(
|
||||
v => _encryptionService.EncryptMessage(v) ?? string.Empty,
|
||||
v => _encryptionService.DecryptMessage(v) ?? string.Empty
|
||||
);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<User>(builder =>
|
||||
{
|
||||
builder.ToTable("Users");
|
||||
builder.HasKey(u => u.Id);
|
||||
builder.Property(u => u.Username).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(u => u.Username).IsUnique();
|
||||
builder.Property(u => u.PasswordHash).IsRequired();
|
||||
builder.Property(u => u.DisplayName).HasMaxLength(100);
|
||||
builder.Property(u => u.Email).HasMaxLength(255);
|
||||
builder.Property(u => u.Bio).HasMaxLength(500);
|
||||
builder.Property(u => u.Avatar).HasMaxLength(500);
|
||||
builder.Property(u => u.Birthday);
|
||||
builder.Property(u => u.CreatedAt).IsRequired();
|
||||
builder.Property(u => u.HideStoryViews).HasDefaultValue(false);
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var domainEvents = ChangeTracker
|
||||
.Entries<IAggregateRoot>()
|
||||
.SelectMany(x =>
|
||||
|
||||
{
|
||||
if (x.Entity is AggregateRoot<Guid> root)
|
||||
{
|
||||
var events = root.GetDomainEvents().ToList();
|
||||
root.ClearDomainEvents();
|
||||
return events;
|
||||
}
|
||||
return Enumerable.Empty<IDomainEvent>();
|
||||
})
|
||||
.ToList();
|
||||
|
||||
int result = await base.SaveChangesAsync(cancellationToken);
|
||||
|
||||
foreach (var domainEvent in domainEvents)
|
||||
{
|
||||
await _mediator.Publish(domainEvent, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260311180816_InitialIdentity")]
|
||||
partial class InitialIdentity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialIdentity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "identity");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Username = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Username",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260311200839_UpdateIdentityWithNewFieldsAndTables")]
|
||||
partial class UpdateIdentityWithNewFieldsAndTables
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BgColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stories", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateIdentityWithNewFieldsAndTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Avatar",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Bio",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "Birthday",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Friendships",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FriendId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Friendships", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stories",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "text", nullable: false),
|
||||
MediaUrl = table.Column<string>(type: "text", nullable: true),
|
||||
Content = table.Column<string>(type: "text", nullable: true),
|
||||
BgColor = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Friendships_UserId_FriendId",
|
||||
schema: "identity",
|
||||
table: "Friendships",
|
||||
columns: new[] { "UserId", "FriendId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Friendships",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stories",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Avatar",
|
||||
schema: "identity",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Bio",
|
||||
schema: "identity",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Birthday",
|
||||
schema: "identity",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
schema: "identity",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260311215347_AddHideStoryViewsToUser")]
|
||||
partial class AddHideStoryViewsToUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BgColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stories", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHideStoryViewsToUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "HideStoryViews",
|
||||
schema: "identity",
|
||||
table: "Users",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HideStoryViews",
|
||||
schema: "identity",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260312174453_AddHideStoryViewsAndFixStoryViewer")]
|
||||
partial class AddHideStoryViewsAndFixStoryViewer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BgColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stories", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryViewers", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Viewers")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Navigation("Viewers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHideStoryViewsAndFixStoryViewer : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoryViewers",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ViewedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoryViewers", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoryViewers_Stories_StoryId",
|
||||
column: x => x.StoryId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Stories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoryViewers_StoryId_UserId",
|
||||
schema: "identity",
|
||||
table: "StoryViewers",
|
||||
columns: new[] { "StoryId", "UserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoryViewers",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20260313115319_AddStoryReactionsReplies")]
|
||||
partial class AddStoryReactionsReplies
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BgColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stories", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId", "Emoji")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryReactions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("StoryReplies", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryViewers", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Reactions")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Replies")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Viewers")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Navigation("Reactions");
|
||||
|
||||
b.Navigation("Replies");
|
||||
|
||||
b.Navigation("Viewers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStoryReactionsReplies : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoryReactions",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Emoji = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoryReactions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoryReactions_Stories_StoryId",
|
||||
column: x => x.StoryId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Stories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoryReplies",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Content = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoryReplies", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoryReplies_Stories_StoryId",
|
||||
column: x => x.StoryId,
|
||||
principalSchema: "identity",
|
||||
principalTable: "Stories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoryReactions_StoryId_UserId_Emoji",
|
||||
schema: "identity",
|
||||
table: "StoryReactions",
|
||||
columns: new[] { "StoryId", "UserId", "Emoji" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoryReplies_StoryId",
|
||||
schema: "identity",
|
||||
table: "StoryReplies",
|
||||
column: "StoryId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoryReactions",
|
||||
schema: "identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoryReplies",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
partial class IdentityDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("identity")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BgColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stories", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId", "Emoji")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryReactions", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId");
|
||||
|
||||
b.ToTable("StoryReplies", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryViewers", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("Birthday")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("HideStoryViews")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReaction", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Reactions")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryReply", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Replies")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.HasOne("Knot.Modules.Identity.Domain.Story", null)
|
||||
.WithMany("Viewers")
|
||||
.HasForeignKey("StoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Identity.Domain.Story", b =>
|
||||
{
|
||||
b.Navigation("Reactions");
|
||||
|
||||
b.Navigation("Replies");
|
||||
|
||||
b.Navigation("Viewers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация репозитория пользователей с использованием EF Core.
|
||||
/// </summary>
|
||||
public sealed class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly IdentityDbContext _context;
|
||||
|
||||
public UserRepository(IdentityDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Users.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<User>> GetByIdsAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Users.Where(u => ids.Contains(u.Id)).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<User?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Users.FirstOrDefaultAsync(u => u.Username == username, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> IsUsernameUniqueAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return !await _context.Users.AnyAsync(u => u.Username == username, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<User>> SearchUsersAsync(string query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Users
|
||||
.Where(u => u.Username.ToLower().Contains(query.ToLower()) ||
|
||||
(u.DisplayName != null && u.DisplayName.ToLower().Contains(query.ToLower())))
|
||||
.Take(20)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void Add(User user)
|
||||
{
|
||||
_context.Users.Add(user);
|
||||
}
|
||||
|
||||
public void Update(User user)
|
||||
{
|
||||
_context.Users.Update(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Identity.Domain;
|
||||
|
||||
namespace Knot.Modules.Identity.Infrastructure.Services;
|
||||
|
||||
public sealed class UserDisplayNameProvider : IUserDisplayNameProvider
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UserDisplayNameProvider(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<string> GetDisplayNameAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId, ct);
|
||||
return user?.DisplayName ?? "User";
|
||||
}
|
||||
|
||||
public async Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId, ct);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new UserInfo(user.Id, user.Username, user.DisplayName, user.Avatar);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<Guid, UserInfo>> GetUsersInfoAsync(IEnumerable<Guid> userIds, CancellationToken ct = default)
|
||||
{
|
||||
var users = await _userRepository.GetByIdsAsync(userIds, ct);
|
||||
var result = new System.Collections.Generic.Dictionary<Guid, UserInfo>();
|
||||
foreach (var user in users)
|
||||
{
|
||||
result[user.Id] = new UserInfo(user.Id, user.Username, user.DisplayName, user.Avatar);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
31
backend/src/Modules/Identity/Knot.Modules.Identity.csproj
Normal file
31
backend/src/Modules/Identity/Knot.Modules.Identity.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
229
backend/src/Modules/Identity/migrations.sql
Normal file
229
backend/src/Modules/Identity/migrations.sql
Normal file
@@ -0,0 +1,229 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" character varying(150) NOT NULL,
|
||||
"ProductVersion" character varying(32) NOT NULL,
|
||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||
);
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311180816_InitialIdentity') THEN
|
||||
IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'identity') THEN
|
||||
CREATE SCHEMA identity;
|
||||
END IF;
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311180816_InitialIdentity') THEN
|
||||
CREATE TABLE identity."Users" (
|
||||
"Id" uuid NOT NULL,
|
||||
"Username" character varying(50) NOT NULL,
|
||||
"PasswordHash" text NOT NULL,
|
||||
"DisplayName" character varying(100) NOT NULL,
|
||||
"Email" character varying(255),
|
||||
CONSTRAINT "PK_Users" PRIMARY KEY ("Id")
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311180816_InitialIdentity') THEN
|
||||
CREATE UNIQUE INDEX "IX_Users_Username" ON identity."Users" ("Username");
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311180816_InitialIdentity') THEN
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260311180816_InitialIdentity', '10.0.4');
|
||||
END IF;
|
||||
END $EF$;
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
ALTER TABLE identity."Users" ADD "Avatar" character varying(500);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
ALTER TABLE identity."Users" ADD "Bio" character varying(500);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
ALTER TABLE identity."Users" ADD "Birthday" timestamp with time zone;
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
ALTER TABLE identity."Users" ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '-infinity';
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
CREATE TABLE identity."Friendships" (
|
||||
"Id" uuid NOT NULL,
|
||||
"UserId" uuid NOT NULL,
|
||||
"FriendId" uuid NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_Friendships" PRIMARY KEY ("Id")
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
CREATE TABLE identity."Stories" (
|
||||
"Id" uuid NOT NULL,
|
||||
"UserId" uuid NOT NULL,
|
||||
"Type" text NOT NULL,
|
||||
"MediaUrl" text,
|
||||
"Content" text,
|
||||
"BgColor" text,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"ExpiresAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_Stories" PRIMARY KEY ("Id")
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
CREATE UNIQUE INDEX "IX_Friendships_UserId_FriendId" ON identity."Friendships" ("UserId", "FriendId");
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311200839_UpdateIdentityWithNewFieldsAndTables') THEN
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260311200839_UpdateIdentityWithNewFieldsAndTables', '10.0.4');
|
||||
END IF;
|
||||
END $EF$;
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311215347_AddHideStoryViewsToUser') THEN
|
||||
ALTER TABLE identity."Users" ADD "HideStoryViews" boolean NOT NULL DEFAULT FALSE;
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260311215347_AddHideStoryViewsToUser') THEN
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260311215347_AddHideStoryViewsToUser', '10.0.4');
|
||||
END IF;
|
||||
END $EF$;
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260312174453_AddHideStoryViewsAndFixStoryViewer') THEN
|
||||
CREATE TABLE identity."StoryViewers" (
|
||||
"Id" uuid NOT NULL,
|
||||
"StoryId" uuid NOT NULL,
|
||||
"UserId" uuid NOT NULL,
|
||||
"ViewedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_StoryViewers" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_StoryViewers_Stories_StoryId" FOREIGN KEY ("StoryId") REFERENCES identity."Stories" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260312174453_AddHideStoryViewsAndFixStoryViewer') THEN
|
||||
CREATE UNIQUE INDEX "IX_StoryViewers_StoryId_UserId" ON identity."StoryViewers" ("StoryId", "UserId");
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260312174453_AddHideStoryViewsAndFixStoryViewer') THEN
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260312174453_AddHideStoryViewsAndFixStoryViewer', '10.0.4');
|
||||
END IF;
|
||||
END $EF$;
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260313115319_AddStoryReactionsReplies') THEN
|
||||
CREATE TABLE identity."StoryReactions" (
|
||||
"Id" uuid NOT NULL,
|
||||
"StoryId" uuid NOT NULL,
|
||||
"UserId" uuid NOT NULL,
|
||||
"Emoji" character varying(10) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_StoryReactions" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_StoryReactions_Stories_StoryId" FOREIGN KEY ("StoryId") REFERENCES identity."Stories" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260313115319_AddStoryReactionsReplies') THEN
|
||||
CREATE TABLE identity."StoryReplies" (
|
||||
"Id" uuid NOT NULL,
|
||||
"StoryId" uuid NOT NULL,
|
||||
"UserId" uuid NOT NULL,
|
||||
"Content" character varying(500) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_StoryReplies" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_StoryReplies_Stories_StoryId" FOREIGN KEY ("StoryId") REFERENCES identity."Stories" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260313115319_AddStoryReactionsReplies') THEN
|
||||
CREATE UNIQUE INDEX "IX_StoryReactions_StoryId_UserId_Emoji" ON identity."StoryReactions" ("StoryId", "UserId", "Emoji");
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260313115319_AddStoryReactionsReplies') THEN
|
||||
CREATE INDEX "IX_StoryReplies_StoryId" ON identity."StoryReplies" ("StoryId");
|
||||
END IF;
|
||||
END $EF$;
|
||||
|
||||
DO $EF$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260313115319_AddStoryReactionsReplies') THEN
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260313115319_AddStoryReactionsReplies', '10.0.4');
|
||||
END IF;
|
||||
END $EF$;
|
||||
COMMIT;
|
||||
|
||||
Reference in New Issue
Block a user