Merge pull request #45 from StewKI/feature-join-whiteboard-by-code

Feature join whiteboard by code
This commit is contained in:
2026-03-08 00:31:22 +01:00
committed by GitHub
74 changed files with 1576 additions and 522 deletions

View File

@@ -21,7 +21,6 @@
<ItemGroup>
<Folder Include="Application\Models\Shape\Command\DeleteShape\" />
<Folder Include="Domain\Models\WhiteboardMembership\Validation\" />
<Folder Include="Infrastructure\Persistence\Db\Migrations\" />
</ItemGroup>

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
namespace AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
public record AcceptUserRequestToJoinMessage(AcceptUserRequestToJoinCommand Command) : IMessage;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
public class AcceptUserRequestToJoinMessageHandler : IMessageHandler<AcceptUserRequestToJoinMessage>
{
private readonly IDispatcher _dispatcher;
public AcceptUserRequestToJoinMessageHandler(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task Handle(AcceptUserRequestToJoinMessage message, CancellationToken cancellationToken)
{
await _dispatcher.Execute(message.Command, cancellationToken);
}
}

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
namespace AipsCore.Application.Common.Message.RejectUserRequestToJoin;
public record RejectUserRequestToJoinMessage(RejectUserRequestToJoinCommand Command): IMessage;

View File

@@ -0,0 +1,19 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.MessageBroking;
namespace AipsCore.Application.Common.Message.RejectUserRequestToJoin;
public class RejectUserRequestToJoinMessageHandler : IMessageHandler<RejectUserRequestToJoinMessage>
{
private readonly IDispatcher _dispatcher;
public RejectUserRequestToJoinMessageHandler(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task Handle(RejectUserRequestToJoinMessage message, CancellationToken cancellationToken)
{
await _dispatcher.Execute(message.Command, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
public record AcceptUserRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;

View File

@@ -0,0 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
public class AcceptUserRequestToJoinCommandHandler : ICommandHandler<AcceptUserRequestToJoinCommand>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
public AcceptUserRequestToJoinCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(AcceptUserRequestToJoinCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = new UserId(command.UserId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
membership.UpdateStatus(WhiteboardMembershipStatus.Accepted);
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public record AddUserToWhiteboardCommand(
string UserId,
string WhiteboardId)
: ICommand;

View File

@@ -1,12 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public static class AddUserToWhiteboardCommandErrors
{
public static ValidationError WhiteboardDoesNotExist(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_exists",
Message: $"Whiteboard with id '{whiteboardId}' does not exist.");
}

View File

@@ -1,65 +0,0 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.External;
using AipsCore.Domain.Models.User.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard;
public class AddUserToWhiteboardCommandHandler
: ICommandHandler<AddUserToWhiteboardCommand>
{
private readonly IWhiteboardRepository _whiteboardRepository;
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUserRepository _userRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IDispatcher _dispatcher;
public AddUserToWhiteboardCommandHandler(
IWhiteboardRepository whiteboardRepository,
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUserRepository userRepository,
IUnitOfWork unitOfWork,
IDispatcher dispatcher)
{
_whiteboardRepository = whiteboardRepository;
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_userRepository = userRepository;
_unitOfWork = unitOfWork;
_dispatcher = dispatcher;
}
private Domain.Models.Whiteboard.Whiteboard? _whiteboard;
private Domain.Models.User.User? _user;
public async Task Handle(AddUserToWhiteboardCommand command, CancellationToken cancellationToken = default)
{
_whiteboard = await _whiteboardRepository.GetByIdAsync(new WhiteboardId(command.WhiteboardId), cancellationToken);
_user = await _userRepository.GetByIdAsync(new UserId(command.UserId), cancellationToken);
Validate(command);
await _whiteboard!.AddUserAsync(_user!, _whiteboardMembershipRepository, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
private void Validate(AddUserToWhiteboardCommand command)
{
if (_whiteboard is null)
{
throw new ValidationException(WhiteboardErrors.NotFound(new WhiteboardId(command.WhiteboardId)));
}
if (_user is null)
{
throw new ValidationException(UserErrors.NotFound(new UserId(command.UserId)));
}
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard;
public static class BanUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId.IdValue}' is not a member of whiteboard with id '{whiteboardId.IdValue}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId.IdValue}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.BanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class BanUserFromWhiteboardCommandHandler : ICommandHandler<BanUserFromWh
if (whiteboard is null)
{
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(BanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
whiteboard.BanUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public record JoinWithCodeCommand(string Code): ICommand<JoinWithCodeDto>;

View File

@@ -0,0 +1,65 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public class JoinWithCodeCommandHandler : ICommandHandler<JoinWithCodeCommand, JoinWithCodeDto>
{
private readonly IWhiteboardRepository _whiteboardRepository;
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserContext _userContext;
public JoinWithCodeCommandHandler(
IWhiteboardRepository whiteboardRepository,
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUnitOfWork unitOfWork,
IUserContext userContext)
{
_whiteboardRepository = whiteboardRepository;
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
_userContext = userContext;
}
public async Task<JoinWithCodeDto> Handle(JoinWithCodeCommand command, CancellationToken cancellationToken = default)
{
var userId = _userContext.GetCurrentUserId();
var code = new WhiteboardCode(command.Code);
var whiteboard = await _whiteboardRepository.GetByCodeAsync(code, cancellationToken);
if (whiteboard is null)
{
throw new ValidationException(WhiteboardErrors.NotFound(code));
}
if (!whiteboard.ShouldRequestToJoin(userId))
{
return new JoinWithCodeDto(whiteboard.Id.IdValue, WhiteboardMembershipStatus.Accepted);
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboard.Id, userId, cancellationToken);
if (membership is null)
{
membership = whiteboard.RequestJoin(userId);
}
else
{
whiteboard.RequestReJoin(membership);
}
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return new JoinWithCodeDto(whiteboard.Id.IdValue, membership.Status);
}
}

View File

@@ -0,0 +1,5 @@
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
public record JoinWithCodeDto(string WhiteboardId, WhiteboardMembershipStatus Status);

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard;
public static class KickUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId}' is not a member of whiteboard with id '{whiteboardId}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.KickUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class KickUserFromWhiteboardCommandHandler : ICommandHandler<KickUserFrom
if (whiteboard is null)
{
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(KickUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
whiteboard.KickUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Command;
namespace AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
public record RejectUserRequestToJoinCommand(string WhiteboardId, string UserId): ICommand;

View File

@@ -0,0 +1,40 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
public class RejectUserRequestToJoinCommandHandler : ICommandHandler<RejectUserRequestToJoinCommand>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUnitOfWork _unitOfWork;
public RejectUserRequestToJoinCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_unitOfWork = unitOfWork;
}
public async Task Handle(RejectUserRequestToJoinCommand command, CancellationToken cancellationToken = default)
{
var whiteboardId = new WhiteboardId(command.WhiteboardId);
var userId = new UserId(command.UserId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
membership.UpdateStatus(WhiteboardMembershipStatus.Rejected);
await _whiteboardMembershipRepository.SaveAsync(membership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard;
public static class UnbanUserFromWhiteboardCommandErrors
{
public static ValidationError WhiteboardMembershipNotFound(WhiteboardId whiteboardId, UserId userId)
=> new ValidationError(
Code: "whiteboard_membership_not_found",
Message: $"User with id '{userId}' is not a member of whiteboard with id '{whiteboardId}'");
public static ValidationError WhiteboardNotFound(WhiteboardId whiteboardId)
=> new ValidationError(
Code: "whiteboard_not_found",
Message: $"Whiteboard with id '{whiteboardId}' not found.");
}

View File

@@ -4,8 +4,10 @@ using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.External;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Command.UnbanUserFromWhiteboard;
@@ -37,14 +39,14 @@ public class UnbanUserFromWhiteboardCommandHandler : ICommandHandler<UnbanUserFr
if (whiteboard is null)
{
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardNotFound(whiteboardId));
throw new ValidationException(WhiteboardErrors.NotFound(whiteboardId));
}
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(UnbanUserFromWhiteboardCommandErrors.WhiteboardMembershipNotFound(whiteboardId, userId));
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
whiteboard.UnbanUser(_userContext.GetCurrentUserId(), membership);

View File

@@ -0,0 +1,6 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
public record GetMembershipStatusQuery(string WhiteboardId, string UserId): IQuery<WhiteboardMembershipStatus>;

View File

@@ -0,0 +1,34 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Validation;
namespace AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
public class GetMembershipStatusQueryHandler : IQueryHandler<GetMembershipStatusQuery, WhiteboardMembershipStatus>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
public GetMembershipStatusQueryHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
}
public async Task<WhiteboardMembershipStatus> Handle(GetMembershipStatusQuery query, CancellationToken cancellationToken = default)
{
var userId = new UserId(query.UserId);
var whiteboardId = new WhiteboardId(query.WhiteboardId);
var membership = await _whiteboardMembershipRepository.GetByWhiteboardAndUserAsync(whiteboardId, userId, cancellationToken);
if (membership is null)
{
throw new ValidationException(WhiteboardMembershipErrors.NotFound(whiteboardId, userId));
}
return membership.Status;
}
}

View File

@@ -1,6 +1,7 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
@@ -32,7 +33,7 @@ public class GetRecentWhiteboardsQueryHandler : IQueryHandler<GetRecentWhiteboar
.Include(m => m.Whiteboard)
.Where(m => (
m.UserId == userIdGuid &&
m.IsBanned == false &&
m.Status != WhiteboardMembershipStatus.Banned &&
m.Whiteboard != null &&
m.Whiteboard.State != WhiteboardState.Deleted
))

View File

@@ -34,6 +34,7 @@ public class GetWhiteboardInfoRTQueryHandler
return _context.Whiteboards
.Where(w => w.Id == whiteboardId)
.Include(w => w.Memberships)
.ThenInclude(m => m.User)
.Include(w => w.Owner)
.Include(w => w.Shapes);
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
public record CreateWhiteboardMembershipCommand(
string WhiteboardId,
bool IsBanned,
bool EditingEnabled,
bool CanJoin)
: ICommand<WhiteboardMembershipId>;

View File

@@ -1,42 +0,0 @@
using AipsCore.Application.Abstract.Command;
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
public class CreateWhiteboardMembershipCommandHandler : ICommandHandler<CreateWhiteboardMembershipCommand, WhiteboardMembershipId>
{
private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
private readonly IUserContext _userContext;
private readonly IUnitOfWork _unitOfWork;
public CreateWhiteboardMembershipCommandHandler(
IWhiteboardMembershipRepository whiteboardMembershipRepository,
IUserContext userContext,
IUnitOfWork unitOfWork)
{
_whiteboardMembershipRepository = whiteboardMembershipRepository;
_userContext = userContext;
_unitOfWork = unitOfWork;
}
public async Task<WhiteboardMembershipId> Handle(CreateWhiteboardMembershipCommand command, CancellationToken cancellationToken = default)
{
var userId = _userContext.GetCurrentUserId();
var whiteboardMembership = Domain.Models.WhiteboardMembership.WhiteboardMembership.Create(
command.WhiteboardId,
userId.IdValue,
command.IsBanned,
command.EditingEnabled,
command.CanJoin,
DateTime.UtcNow);
await _whiteboardMembershipRepository.SaveAsync(whiteboardMembership, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return whiteboardMembership.Id;
}
}

View File

@@ -6,5 +6,6 @@ namespace AipsCore.Domain.Models.Whiteboard.External;
public interface IWhiteboardRepository
: IAbstractRepository<Whiteboard, WhiteboardId>, ISoftDeletableRepository<WhiteboardId>
{
Task<bool> WhiteboardCodeExists(WhiteboardCode whiteboardCode);
Task<bool> WhiteboardCodeExistsAsync(WhiteboardCode whiteboardCode);
Task<Whiteboard?> GetByCodeAsync(WhiteboardCode whiteboardCode, CancellationToken cancellationToken = default);
}

View File

@@ -7,6 +7,38 @@ namespace AipsCore.Domain.Models.Whiteboard.Validation;
public class WhiteboardErrors : AbstractErrors<Whiteboard, WhiteboardId>
{
public static ValidationError NotFound(WhiteboardCode whiteboardCode)
{
const string code = "not_found";
string message = $"Whiteboard with code '{whiteboardCode.CodeValue}' was not found!";
return CreateValidationError(code,message);
}
public static ValidationError CannotJoin(WhiteboardCode whiteboardCode)
{
const string code = "cannot_join_whiteboard";
string message = $"Cannot join the whiteboard with code '{whiteboardCode.CodeValue}'";
return CreateValidationError(code,message);
}
public static ValidationError UserBanned(UserId userId)
{
const string code = "user_banned_from_whiteboard";
string message = $"User with id '{userId}' is banned from this whiteboard.";
return CreateValidationError(code,message);
}
public static ValidationError UserAlreadyTryingToJoin(UserId userId)
{
const string code = "user_already_trying_to_join_whiteboard";
string message = $"User with id '{userId}' is already trying to join the whiteboard.";
return CreateValidationError(code,message);
}
public static ValidationError UserAlreadyAdded(UserId userId)
{
string code = "user_already_added";

View File

@@ -36,7 +36,7 @@ public record WhiteboardCode : AbstractValueObject
{
whiteboardCode = Generate();
codeExists = await whiteboardRepository.WhiteboardCodeExists(whiteboardCode);
codeExists = await whiteboardRepository.WhiteboardCodeExistsAsync(whiteboardCode);
} while (codeExists);
return whiteboardCode;

View File

@@ -1,42 +1,47 @@
using System.Runtime.InteropServices.Swift;
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Enums;
using AipsCore.Domain.Models.Whiteboard.Validation;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard : DomainModel<WhiteboardId>
public partial class Whiteboard
{
public async Task AddUserAsync(
User.User user,
IWhiteboardMembershipRepository membershipRepository,
CancellationToken cancellationToken = default)
public WhiteboardMembership.WhiteboardMembership RequestJoin(UserId userId)
{
var membership
= await membershipRepository.GetByWhiteboardAndUserAsync(this.Id, user.Id, cancellationToken);
return WhiteboardMembership.WhiteboardMembership.Create(
Id.IdValue,
userId.IdValue,
false,
DetermineJoinStatus(),
DateTime.UtcNow);
}
if (membership is not null)
public void RequestReJoin(WhiteboardMembership.WhiteboardMembership membership)
{
switch (membership.Status)
{
throw new ValidationException(WhiteboardErrors.UserAlreadyAdded(user.Id));
case WhiteboardMembershipStatus.Banned:
throw new ValidationException(WhiteboardErrors.UserBanned(membership.UserId));
case WhiteboardMembershipStatus.Pending:
throw new ValidationException(WhiteboardErrors.UserAlreadyTryingToJoin(membership.UserId));
case WhiteboardMembershipStatus.Accepted:
break;
default:
membership.UpdateStatus(DetermineJoinStatus());
break;
}
membership = WhiteboardMembership.WhiteboardMembership.Create(
this.Id.IdValue,
user.Id.IdValue,
false,
false,
this.GetCanJoin(),
DateTime.UtcNow
);
await membershipRepository.AddAsync(membership, cancellationToken);
}
private bool GetCanJoin()
private WhiteboardMembershipStatus DetermineJoinStatus()
{
return this.JoinPolicy == WhiteboardJoinPolicy.FreeToJoin;
return JoinPolicy switch
{
WhiteboardJoinPolicy.FreeToJoin => WhiteboardMembershipStatus.Accepted,
WhiteboardJoinPolicy.RequestToJoin => WhiteboardMembershipStatus.Pending,
WhiteboardJoinPolicy.Private => throw new ValidationException(WhiteboardErrors.CannotJoin(Code)),
_ => throw new ArgumentOutOfRangeException()
};
}
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void BanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanBanOtherUsers(currentUserId));
}
whiteboardMembership.Ban();
}
}

View File

@@ -6,6 +6,11 @@ public partial class Whiteboard
{
public bool CanUserDelete(UserId userId)
{
return WhiteboardOwnerId.IdValue == userId.IdValue;
return IsOwner(userId);
}
public bool ShouldRequestToJoin(UserId userId)
{
return !IsOwner(userId);
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void KickUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanKickOtherUsers(currentUserId));
}
whiteboardMembership.Kick();
}
}

View File

@@ -0,0 +1,43 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void BanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanBanOtherUsers(currentUserId));
}
whiteboardMembership.Ban();
}
public void UnbanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanUnbanOtherUsers(currentUserId));
}
whiteboardMembership.Unban();
}
public void KickUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (IsOwner(currentUserId))
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanKickOtherUsers(currentUserId));
}
whiteboardMembership.Kick();
}
private bool IsOwner(UserId userId)
{
return WhiteboardOwnerId.IdValue == userId.IdValue;
}
}

View File

@@ -1,18 +0,0 @@
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.Validation;
namespace AipsCore.Domain.Models.Whiteboard;
public partial class Whiteboard
{
public void UnbanUser(UserId currentUserId, WhiteboardMembership.WhiteboardMembership whiteboardMembership)
{
if (WhiteboardOwnerId != currentUserId)
{
throw new ValidationException(WhiteboardErrors.OnlyOwnerCanUnbanOtherUsers(currentUserId));
}
whiteboardMembership.Unban();
}
}

View File

@@ -0,0 +1,13 @@
namespace AipsCore.Domain.Models.WhiteboardMembership.Enums;
public enum WhiteboardMembershipStatus
{
Pending,
Accepted,
Active,
Inactive,
Rejected,
Cancelled,
Kicked,
Banned
}

View File

@@ -0,0 +1,18 @@
using AipsCore.Domain.Abstract.Validation;
using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership.Validation;
public class WhiteboardMembershipErrors : AbstractErrors<WhiteboardMembership, WhiteboardMembershipId>
{
public static ValidationError NotFound(WhiteboardId whiteboardId, UserId userId)
{
const string code = "whiteboard_membership_not_found";
string message = $"Whiteboard membership with whiteboard id {whiteboardId.IdValue} and user id {userId.IdValue} not found.";
return CreateValidationError(code, message);
}
}

View File

@@ -1,20 +0,0 @@
using AipsCore.Domain.Abstract.Rule;
using AipsCore.Domain.Abstract.ValueObject;
namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
public record WhiteboardMembershipCanJoin : AbstractValueObject
{
public bool CanJoinValue { get; init; }
public WhiteboardMembershipCanJoin(bool CanJoinValue)
{
this.CanJoinValue = CanJoinValue;
Validate();
}
protected override ICollection<IRule> GetValidationRules()
{
return [];
}
}

View File

@@ -1,20 +0,0 @@
using AipsCore.Domain.Abstract.Rule;
using AipsCore.Domain.Abstract.ValueObject;
namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
public record WhiteboardMembershipIsBanned : AbstractValueObject
{
public bool IsBannedValue { get; init; }
public WhiteboardMembershipIsBanned(bool IsBannedValue)
{
this.IsBannedValue = IsBannedValue;
Validate();
}
protected override ICollection<IRule> GetValidationRules()
{
return [];
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Ban()
{
IsBanned = new WhiteboardMembershipIsBanned(true);
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Kick()
{
CanJoin = new WhiteboardMembershipCanJoin(false);
}
}

View File

@@ -0,0 +1,26 @@
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Ban()
{
Status = WhiteboardMembershipStatus.Banned;
}
public void Unban()
{
Status = WhiteboardMembershipStatus.Cancelled;
}
public void Kick()
{
Status = WhiteboardMembershipStatus.Kicked;
}
public void UpdateStatus(WhiteboardMembershipStatus newStatus)
{
Status = newStatus;
}
}

View File

@@ -1,11 +0,0 @@
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
public partial class WhiteboardMembership
{
public void Unban()
{
IsBanned = new WhiteboardMembershipIsBanned(false);
}
}

View File

@@ -1,6 +1,7 @@
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
namespace AipsCore.Domain.Models.WhiteboardMembership;
@@ -9,97 +10,84 @@ public partial class WhiteboardMembership : DomainModel<WhiteboardMembershipId>
{
public WhiteboardId WhiteboardId { get; private set; }
public UserId UserId { get; private set; }
public WhiteboardMembershipIsBanned IsBanned { get; private set; }
public WhiteboardMembershipEditingEnabled EditingEnabled { get; private set; }
public WhiteboardMembershipCanJoin CanJoin { get; private set; }
public WhiteboardMembershipStatus Status { get; private set; }
public WhiteboardMembershipLastInteractedAt LastInteractedAt { get; private set; }
public WhiteboardMembership(
WhiteboardMembershipId id,
Whiteboard.Whiteboard owner,
Whiteboard.Whiteboard whiteboard,
User.User user,
WhiteboardMembershipIsBanned isBanned,
WhiteboardMembershipEditingEnabled editingEnabled,
WhiteboardMembershipCanJoin canJoin,
WhiteboardMembershipStatus status,
WhiteboardMembershipLastInteractedAt lastInteractedAt)
: base(id)
{
WhiteboardId = owner.Id;
WhiteboardId = whiteboard.Id;
UserId = user.Id;
IsBanned = isBanned;
EditingEnabled = editingEnabled;
CanJoin = canJoin;
Status = status;
LastInteractedAt = lastInteractedAt;
}
public WhiteboardMembership(
WhiteboardMembershipId id,
WhiteboardId ownerId,
WhiteboardId whiteboardId,
UserId userId,
WhiteboardMembershipIsBanned isBanned,
WhiteboardMembershipEditingEnabled editingEnabled,
WhiteboardMembershipCanJoin canJoin,
WhiteboardMembershipStatus status,
WhiteboardMembershipLastInteractedAt lastInteractedAt)
: base(id)
{
WhiteboardId = ownerId;
WhiteboardId = whiteboardId;
UserId = userId;
IsBanned = isBanned;
EditingEnabled = editingEnabled;
CanJoin = canJoin;
Status = status;
LastInteractedAt = lastInteractedAt;
}
public static WhiteboardMembership Create(
string id,
string ownerId,
string whiteboardId,
string userId,
bool isBanned,
bool editingEnabled,
bool canJoin,
WhiteboardMembershipStatus status,
DateTime lastInteractedAt)
{
var whiteboardMembershipId = new WhiteboardMembershipId(id);
var whiteboardId = new WhiteboardId(ownerId);
var whiteboardIdVo = new WhiteboardId(whiteboardId);
var userIdVo = new UserId(userId);
var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
return new WhiteboardMembership(
whiteboardMembershipId,
whiteboardId,
userIdVo,
isBannedVo,
whiteboardIdVo,
userIdVo,
editingEnabledVo,
canJoinVo,
status,
lastInteractedAtVo);
}
public static WhiteboardMembership Create(
string ownerId,
string whiteboardId,
string userId,
bool isBanned,
bool editingEnabled,
bool canJoin,
WhiteboardMembershipStatus status,
DateTime lastInteractedAt)
{
var whiteboardMembershipId = WhiteboardMembershipId.Any();
var whiteboardId = new WhiteboardId(ownerId);
var whiteboardIdVo = new WhiteboardId(whiteboardId);
var userIdVo = new UserId(userId);
var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
return new WhiteboardMembership(
whiteboardMembershipId,
whiteboardId,
whiteboardIdVo,
userIdVo,
isBannedVo,
editingEnabledVo,
canJoinVo,
status,
lastInteractedAtVo);
}
}

View File

@@ -0,0 +1,503 @@
// <auto-generated />
using System;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AipsCore.Infrastructure.Persistence.Db.Migrations
{
[DbContext(typeof(AipsDbContext))]
[Migration("20260226153018_ReworkedMembership")]
partial class ReworkedMembership
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AuthorId")
.HasColumnType("uuid");
b.Property<string>("Color")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int?>("EndPositionX")
.HasColumnType("integer");
b.Property<int?>("EndPositionY")
.HasColumnType("integer");
b.Property<int>("PositionX")
.HasColumnType("integer");
b.Property<int>("PositionY")
.HasColumnType("integer");
b.Property<int?>("TextSize")
.HasColumnType("integer");
b.Property<string>("TextValue")
.HasColumnType("text");
b.Property<int?>("Thickness")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<Guid>("WhiteboardId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("WhiteboardId");
b.ToTable("Shapes");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("JoinPolicy")
.HasColumnType("integer");
b.Property<int>("MaxParticipants")
.HasColumnType("integer");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Whiteboards");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EditingEnabled")
.HasColumnType("boolean");
b.Property<DateTime>("LastInteractedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("WhiteboardId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("WhiteboardId");
b.ToTable("WhiteboardMemberships");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Shape.Shape", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Author")
.WithMany("Shapes")
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard")
.WithMany("Shapes")
.HasForeignKey("WhiteboardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
b.Navigation("Whiteboard");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "Owner")
.WithMany("Whiteboards")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.WhiteboardMembership.WhiteboardMembership", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany("Memberships")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", "Whiteboard")
.WithMany("Memberships")
.HasForeignKey("WhiteboardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("Whiteboard");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.User.User", b =>
{
b.Navigation("Memberships");
b.Navigation("Shapes");
b.Navigation("Whiteboards");
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard", b =>
{
b.Navigation("Memberships");
b.Navigation("Shapes");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AipsCore.Infrastructure.Persistence.Db.Migrations
{
/// <inheritdoc />
public partial class ReworkedMembership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CanJoin",
table: "WhiteboardMemberships");
migrationBuilder.DropColumn(
name: "IsBanned",
table: "WhiteboardMemberships");
migrationBuilder.AddColumn<int>(
name: "Status",
table: "WhiteboardMemberships",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Status",
table: "WhiteboardMemberships");
migrationBuilder.AddColumn<bool>(
name: "CanJoin",
table: "WhiteboardMemberships",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsBanned",
table: "WhiteboardMemberships",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Authentication.RefreshToken.RefreshToken", b =>
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -214,18 +214,15 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("CanJoin")
.HasColumnType("boolean");
b.Property<bool>("EditingEnabled")
.HasColumnType("boolean");
b.Property<bool>("IsBanned")
.HasColumnType("boolean");
b.Property<DateTime>("LastInteractedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
@@ -371,7 +368,7 @@ namespace AipsCore.Infrastructure.Persistence.Db.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.Authentication.RefreshToken.RefreshToken", b =>
modelBuilder.Entity("AipsCore.Infrastructure.Persistence.RefreshToken.RefreshToken", b =>
{
b.HasOne("AipsCore.Infrastructure.Persistence.User.User", "User")
.WithMany()

View File

@@ -58,11 +58,18 @@ public class WhiteboardRepository
entity.State = model.State;
}
public async Task<bool> WhiteboardCodeExists(WhiteboardCode whiteboardCode)
public async Task<bool> WhiteboardCodeExistsAsync(WhiteboardCode whiteboardCode)
{
return await Context.Whiteboards.AnyAsync(w => w.Code == whiteboardCode.CodeValue);
}
public async Task<Domain.Models.Whiteboard.Whiteboard?> GetByCodeAsync(WhiteboardCode whiteboardCode, CancellationToken cancellationToken = default)
{
var entity = await Context.Whiteboards.FirstOrDefaultAsync(w => w.Code == whiteboardCode.CodeValue, cancellationToken);
return entity != null ? MapToModel(entity) : null;
}
public async Task SoftDeleteAsync(WhiteboardId id, CancellationToken cancellationToken = default)
{
var entity = await Context.Whiteboards.FindAsync([new Guid(id.IdValue)], cancellationToken);

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership;
@@ -15,11 +16,9 @@ public class WhiteboardMembership
public User.User? User { get; set; } = null!;
public bool IsBanned { get; set; }
public bool EditingEnabled { get; set; }
public bool CanJoin { get; set; }
public WhiteboardMembershipStatus Status { get; set; }
public DateTime LastInteractedAt { get; set; }
}

View File

@@ -24,9 +24,8 @@ public class WhiteboardMembershipRepository
entity.Id.ToString(),
entity.WhiteboardId.ToString(),
entity.UserId.ToString(),
entity.IsBanned,
entity.EditingEnabled,
entity.CanJoin,
entity.Status,
entity.LastInteractedAt
);
}
@@ -38,18 +37,16 @@ public class WhiteboardMembershipRepository
Id = new Guid(model.Id.IdValue),
WhiteboardId = new Guid(model.WhiteboardId.IdValue),
UserId = new Guid(model.UserId.IdValue),
IsBanned = model.IsBanned.IsBannedValue,
EditingEnabled = model.EditingEnabled.EditingEnabledValue,
CanJoin = model.CanJoin.CanJoinValue,
Status = model.Status,
LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue
};
}
protected override void UpdateEntity(WhiteboardMembership entity, Domain.Models.WhiteboardMembership.WhiteboardMembership model)
{
entity.IsBanned = model.IsBanned.IsBannedValue;
entity.EditingEnabled = model.EditingEnabled.EditingEnabledValue;
entity.CanJoin = model.CanJoin.CanJoinValue;
entity.Status = model.Status;
entity.LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue;
}

View File

@@ -1,6 +1,10 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsRT.Model.Memberships;
using AipsRT.Model.Whiteboard;
using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Model.Whiteboard.Structs;
@@ -15,27 +19,87 @@ public class WhiteboardHub : Hub
{
private readonly WhiteboardManager _whiteboardManager;
private readonly IMessagingService _messagingService;
private readonly MembershipService _membershipService;
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService)
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService, MembershipService membershipService)
{
_whiteboardManager = whiteboardManager;
_messagingService = messagingService;
_membershipService = membershipService;
}
public async Task JoinWhiteboard(Guid whiteboardId)
{
if (!_whiteboardManager.WhiteboardExists(whiteboardId))
{
await _whiteboardManager.LoadWhiteboard(whiteboardId);
}
await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString());
var whiteboard = _whiteboardManager.GetWhiteboard(whiteboardId)!;
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
var userId = CurrentUserId;
var ownerId = whiteboard.OwnerId;
_whiteboardManager.AddUserToWhiteboard(Guid.Parse(Context.UserIdentifier!), whiteboardId);
WhiteboardMembershipStatus status;
if (userId == ownerId)
{
status = WhiteboardMembershipStatus.Accepted;
}
else
{
status = await _membershipService.GetMembershipStatus(whiteboardId, userId);
}
await Clients.Caller.SendAsync("InitWhiteboard", state);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
.SendAsync("Joined", Context.UserIdentifier!);
if (status == WhiteboardMembershipStatus.Accepted)
{
_whiteboardManager.AddUserToWhiteboard(userId, whiteboardId);
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
await Clients.Caller.SendAsync("InitWhiteboard", state);
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId).SendAsync("Joined", Context.UserIdentifier!);
}
else
{
await Clients.Caller.SendAsync("WaitingForApproval", userId.ToString());
var user = whiteboard.Users.First(u => u.UserId == userId);
await Clients.User(ownerId.ToString()).SendAsync("UserWaitingForApproval", user);
}
}
public async Task AcceptUser(Guid targetUserId)
{
var whiteboard = CurrentWhiteboard;
await _messagingService.AcceptedUser(new AcceptUserRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), targetUserId.ToString()));
await Clients.User(targetUserId.ToString()).SendAsync("Accepted");
await Clients.User(targetUserId.ToString()).SendAsync("InitWhiteboard", whiteboard);
}
public async Task RejectUser(Guid targetUserId)
{
var whiteboard = CurrentWhiteboard;
await _messagingService.RejectedUser(new RejectUserRequestToJoinCommand(whiteboard.WhiteboardId.ToString(), targetUserId.ToString()));
await Clients.User(targetUserId.ToString()).SendAsync("Rejected");
}
public async Task CancelJoinRequest()
{
var userId = CurrentUserId;
var whiteboard = _whiteboardManager.GetWhiteboardForUser(userId);
if (whiteboard != null)
{
await Clients.User(whiteboard.OwnerId.ToString()).SendAsync("UserCanceledJoinRequest", userId.ToString());
}
}
public async Task LeaveWhiteboard(Guid whiteboardId)

View File

@@ -0,0 +1,21 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
namespace AipsRT.Model.Memberships;
public class MembershipService
{
private readonly IDispatcher _dispatcher;
public MembershipService(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public async Task<WhiteboardMembershipStatus> GetMembershipStatus(Guid whiteboardId, Guid userId)
{
var query = new GetMembershipStatusQuery(whiteboardId.ToString(), userId.ToString());
return await _dispatcher.Execute(query);
}
}

View File

@@ -0,0 +1,17 @@
namespace AipsRT.Model.Users;
public class User
{
public Guid UserId { get; private set; }
public string Username { get; private set; }
public string Email { get; private set; }
public User(Guid userId, string username, string email)
{
UserId = userId;
Username = username;
Email = email;
}
}

View File

@@ -1,7 +1,9 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT;
using AipsCore.Domain.Models.Shape.Enums;
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
using AipsRT.Model.Whiteboard.Shapes.Map;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard;
@@ -28,6 +30,11 @@ public class GetWhiteboardService
OwnerId = entity.OwnerId,
};
foreach (var membership in entity.Memberships)
{
whiteboard.AddUser(new User(membership.UserId, membership.User!.UserName!, membership.User.Email!));
}
foreach (var shape in entity.Shapes)
{
switch (shape.Type)

View File

@@ -1,13 +1,16 @@
using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Model.Users;
namespace AipsRT.Model.Whiteboard;
public class Whiteboard
{
public Guid WhiteboardId { get; set; }
public Guid OwnerId { get; set; }
public List<User> Users { get; } = [];
public List<Shape> Shapes { get; } = [];
public List<Rectangle> Rectangles { get; } = [];
@@ -20,22 +23,24 @@ public class Whiteboard
Shapes.Add(shape);
Rectangles.Add(shape);
}
public void AddArrow(Arrow shape)
{
Shapes.Add(shape);
Arrows.Add(shape);
}
public void AddLine(Line shape)
{
Shapes.Add(shape);
Lines.Add(shape);
}
public void AddTextShape(TextShape shape)
{
Shapes.Add(shape);
TextShapes.Add(shape);
}
public void AddUser(User user) => Users.Add(user);
}

View File

@@ -47,9 +47,9 @@ public class WhiteboardManager
return _userInWhiteboards[userId];
}
public void RemoveUserFromWhiteboard(Guid userId, Guid whiteboardId)
public void RemoveUserFromWhiteboard(Guid userId)
{
_userInWhiteboards.TryRemove(whiteboardId, out _);
_userInWhiteboards.TryRemove(userId, out _);
}
public Whiteboard? GetWhiteboardForUser(Guid userId)

View File

@@ -1,6 +1,7 @@
using AipsCore.Application.Common.Message.ErrorMessage;
using AipsCore.Infrastructure.DI;
using AipsRT.Hubs;
using AipsRT.Model.Memberships;
using AipsRT.Model.Whiteboard;
using AipsRT.Services;
using AipsRT.Services.Interfaces;
@@ -21,6 +22,8 @@ builder.Services.AddAipsMessageHandlers();
builder.Services.AddSingleton<IErrorMessageHandleStrategy, RtErrorHandleStrategy>();
builder.Services.AddHostedService<ErrorSubscriberBackgroundService>();
builder.Services.AddTransient<MembershipService>();
builder.Services.AddScoped<GetWhiteboardService>();
builder.Services.AddSingleton<WhiteboardManager>();
builder.Services.AddSingleton<IMessagingService, MessagingService>();

View File

@@ -1,5 +1,7 @@
using AipsCore.Application.Models.Shape.Command.CreateTextShape;
using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsRT.Model.Whiteboard.Shapes;
namespace AipsRT.Services.Interfaces;
@@ -10,6 +12,9 @@ public interface IMessagingService
Task CreatedArrow(Guid whiteboardId, Arrow arrow);
Task CreateLine(Guid whiteboardId, Line line);
Task CreateTextShape(Guid whiteboardId, TextShape textShape);
Task MoveShape(Guid whiteboardId, MoveShapeCommand moveShape);
Task AcceptedUser(AcceptUserRequestToJoinCommand command);
Task RejectedUser(RejectUserRequestToJoinCommand command);
}

View File

@@ -1,14 +1,18 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
using AipsCore.Application.Common.Message.AddArrow;
using AipsCore.Application.Common.Message.AddLine;
using AipsCore.Application.Common.Message.AddRectangle;
using AipsCore.Application.Common.Message.AddTextShape;
using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
using AipsCore.Application.Models.Shape.Command.CreateArrow;
using AipsCore.Application.Models.Shape.Command.CreateLine;
using AipsCore.Application.Models.Shape.Command.CreateRectangle;
using AipsCore.Application.Models.Shape.Command.CreateTextShape;
using AipsCore.Application.Models.Shape.Command.MoveShape;
using AipsCore.Application.Models.Whiteboard.Command.AcceptUserRequestToJoin;
using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
using AipsRT.Model.Whiteboard.Shapes;
using AipsRT.Services.Interfaces;
@@ -100,4 +104,16 @@ public class MessagingService : IMessagingService
var message = new MoveShapeMessage(whiteboardId, moveShape);
await _messagePublisher.PublishAsync(message);
}
public async Task AcceptedUser(AcceptUserRequestToJoinCommand command)
{
var message = new AcceptUserRequestToJoinMessage(command);
await _messagePublisher.PublishAsync(message);
}
public async Task RejectedUser(RejectUserRequestToJoinCommand command)
{
var message = new RejectUserRequestToJoinMessage(command);
await _messagePublisher.PublishAsync(message);
}
}

View File

@@ -1,10 +1,10 @@
using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard;
using AipsCore.Application.Models.Whiteboard.Command.DeleteWhiteboard;
using AipsCore.Application.Models.Whiteboard.Command.JoinWithCode;
using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardHistory;
using AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Whiteboard = AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard;
@@ -69,7 +69,7 @@ public class WhiteboardController : ControllerBase
[Authorize]
[HttpPost("join")]
public async Task<ActionResult> JoinWhiteboard(CreateWhiteboardMembershipCommand command, CancellationToken cancellationToken)
public async Task<ActionResult<JoinWithCodeDto>> JoinWhiteboardWithCode(JoinWithCodeCommand command, CancellationToken cancellationToken)
{
var result = await _dispatcher.Execute(command, cancellationToken);
return Ok(result);

View File

@@ -1,9 +1,11 @@
using AipsCore.Application.Abstract.MessageBroking;
using AipsCore.Application.Common.Message.AcceptUserRequestToJoin;
using AipsCore.Application.Common.Message.AddArrow;
using AipsCore.Application.Common.Message.AddLine;
using AipsCore.Application.Common.Message.AddRectangle;
using AipsCore.Application.Common.Message.AddTextShape;
using AipsCore.Application.Common.Message.MoveShape;
using AipsCore.Application.Common.Message.RejectUserRequestToJoin;
namespace AipsWorker.Messages;
@@ -17,7 +19,9 @@ public class MessageTypesProvider : IMessageTypesProvider
typeof(AddLineMessage),
typeof(AddRectangleMessage),
typeof(AddTextShapeMessage),
typeof(MoveShapeMessage)
typeof(MoveShapeMessage),
typeof(AcceptUserRequestToJoinMessage),
typeof(RejectUserRequestToJoinMessage)
];
}
}

View File

@@ -152,15 +152,15 @@
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
@@ -298,35 +298,35 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
"@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.55.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.55.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.55.0", "@typescript-eslint/types": "^8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0" } }, "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.55.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/utils": "8.55.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.55.0", "", {}, "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.55.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.55.0", "@typescript-eslint/tsconfig-utils": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.55.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
"@volar/language-core": ["@volar/language-core@2.4.27", "", { "dependencies": { "@volar/source-map": "2.4.27" } }, "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ=="],
"@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
"@volar/source-map": ["@volar/source-map@2.4.27", "", {}, "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg=="],
"@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
"@volar/typescript": ["@volar/typescript@2.4.27", "", { "dependencies": { "@volar/language-core": "2.4.27", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg=="],
"@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
"@vue-macros/common": ["@vue-macros/common@3.1.2", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng=="],
@@ -336,35 +336,35 @@
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.5.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@vue/compiler-sfc": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
"@vue/devtools-core": ["@vue/devtools-core@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6", "@vue/devtools-shared": "^8.0.6", "mitt": "^3.0.1", "nanoid": "^5.1.5", "pathe": "^2.0.3", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-fN7iVtpSQQdtMORWwVZ1JiIAKriinhD+lCHqPw9Rr252ae2TczILEmW0zcAZifPW8HfYcbFkn+h7Wv6kQQCayw=="],
"@vue/devtools-core": ["@vue/devtools-core@8.0.7", "", { "dependencies": { "@vue/devtools-kit": "^8.0.7", "@vue/devtools-shared": "^8.0.7" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-PmpiPxvg3Of80ODHVvyckxwEW1Z02VIAvARIZS1xegINn3VuNQLm9iHUmKD+o6cLkMNWV8OG8x7zo0kgydZgdg=="],
"@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
"@vue/devtools-kit": ["@vue/devtools-kit@8.0.7", "", { "dependencies": { "@vue/devtools-shared": "^8.0.7", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw=="],
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.7", "", {}, "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA=="],
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.6.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.35.1", "fast-glob": "^3.3.3", "typescript-eslint": "^8.35.1", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ=="],
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.7.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.56.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.56.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg=="],
"@vue/language-core": ["@vue/language-core@3.2.4", "", { "dependencies": { "@volar/language-core": "2.4.27", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew=="],
"@vue/language-core": ["@vue/language-core@3.2.5", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g=="],
"@vue/reactivity": ["@vue/reactivity@3.5.28", "", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="],
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/runtime-core": "3.5.28", "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/runtime-core": "3.5.29", "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.28", "", { "dependencies": { "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "vue": "3.5.28" } }, "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.29", "", { "dependencies": { "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "vue": "3.5.29" } }, "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g=="],
"@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
@@ -374,7 +374,7 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
@@ -460,7 +460,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
"eslint-plugin-oxlint": ["eslint-plugin-oxlint@1.42.0", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-3hB/TDbS0f+8ZnPffl0Z/wZ7Yc5NeY5slrxG60kEWInAA9047pdqRcv+Ckk/5KiL3HS2vWtHvmkavPKSFZVKxA=="],
@@ -592,7 +592,7 @@
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
@@ -776,7 +776,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.55.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.55.0", "@typescript-eslint/parser": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/utils": "8.55.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw=="],
"typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
@@ -806,19 +806,19 @@
"vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="],
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.0.6", "", { "dependencies": { "@vue/devtools-core": "^8.0.6", "@vue/devtools-kit": "^8.0.6", "@vue/devtools-shared": "^8.0.6", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^5.3.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-IiTCIJDb1ZliOT8fPbYXllyfgARzz1+R1r8RN9ScGIDzAB6o8bDME1a9JjrfdSJibL7i8DIPQH+pGv0U7haBeA=="],
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.0.7", "", { "dependencies": { "@vue/devtools-core": "^8.0.7", "@vue/devtools-kit": "^8.0.7", "@vue/devtools-shared": "^8.0.7", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^5.3.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-BWj/ykGpqVAJVdPyHmSTUm44buz3jPv+6jnvuFdQSRH0kAgP1cEIE4doHiFyqHXOmuB5EQVR/nh2g9YRiRNs9g=="],
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="],
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
"vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="],
"vue-router": ["vue-router@5.0.2", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.0", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w=="],
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="],
"vue-tsc": ["vue-tsc@3.2.4", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.4" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g=="],
"vue-tsc": ["vue-tsc@3.2.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -850,14 +850,34 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0" } }, "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.55.0", "", {}, "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.55.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.55.0", "@typescript-eslint/tsconfig-utils": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.55.0", "", {}, "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w=="],
"@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="],
"@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="],
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
"@vue/devtools-core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -872,9 +892,41 @@
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
"vite-plugin-vue-inspector/@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.55.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.55.0", "@typescript-eslint/types": "^8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.55.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="],
"@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
@@ -885,5 +937,21 @@
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
"vite-plugin-vue-inspector/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
"vite-plugin-vue-inspector/@vue/compiler-dom/@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
}
}

View File

@@ -19,16 +19,16 @@
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"pinia": "^3.0.4",
"vue": "^3.5.27",
"vue-router": "^5.0.1"
"vue": "^3.5.29",
"vue-router": "^5.0.3"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.10.9",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/eslint-config-typescript": "^14.6.0",
"@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
"eslint": "^9.39.4",
"eslint-plugin-oxlint": "~1.42.0",
"eslint-plugin-vue": "~10.7.0",
"jiti": "^2.6.1",
@@ -37,8 +37,8 @@
"sass-embedded": "^1.97.3",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.5",
"vue-tsc": "^3.2.4"
"vite-plugin-vue-devtools": "^8.0.7",
"vue-tsc": "^3.2.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"

View File

@@ -9,3 +9,14 @@ export enum WhiteboardState {
Inactive,
Deleted
}
export enum MembershipStatus {
Pending,
Accepted,
Rejected,
Active,
Inactive,
Cancelled,
Kicked,
Banned
}

View File

@@ -37,6 +37,6 @@ export const authService = {
const userId = raw?.userId ?? ''
const username = raw?.userName ?? raw?.UserName ?? raw?.username ?? raw?.name ?? ''
const email = raw?.email ?? raw?.Email ?? ''
return { userId, username, email }
return { userId: userId, username, email }
},
}

View File

@@ -1,5 +1,6 @@
import { SignalRService } from '@/services/signalr.ts'
import type { Arrow, Line, MoveShapeCommand, Rectangle, TextShape, Whiteboard } from '@/types/whiteboard.ts'
import type {User} from "@/types";
const client = new SignalRService(`/hubs/whiteboard`)
@@ -76,6 +77,38 @@ export const whiteboardHubService = {
client.on<string>('Leaved', callback)
},
onWaitingForApproval(callback: (userId: string) => void) {
client.on<string>('WaitingForApproval', callback)
},
onUserWaitingForApproval(callback: (user: User) => void) {
client.on<User>('UserWaitingForApproval', callback)
},
onAccepted(callback: () => void) {
client.on('Accepted', callback)
},
onRejected(callback: () => void) {
client.on('Rejected', callback)
},
onUserCanceledJoinRequest(callback: (userId: string) => void) {
client.on<string>('UserCanceledJoinRequest', callback)
},
async acceptUser(userId: string) {
await client.invoke('AcceptUser', userId)
},
async rejectUser(userId: string) {
await client.invoke('RejectUser', userId)
},
async cancelJoinRequest() {
await client.invoke('CancelJoinRequest')
},
offAll() {
client.off('InitWhiteboard')
client.off('AddedRectangle')
@@ -85,5 +118,10 @@ export const whiteboardHubService = {
client.off('MovedShape')
client.off('Joined')
client.off('Leaved')
client.off('WaitingForApproval')
client.off('UserWaitingForApproval')
client.off('Accepted')
client.off('Rejected')
client.off('UserCanceledJoinRequest')
},
}

View File

@@ -1,4 +1,4 @@
import type {Whiteboard} from "@/types";
import type {JoinResult, Whiteboard} from "@/types";
import {api} from './api'
export const whiteboardService = {
@@ -22,6 +22,14 @@ export const whiteboardService = {
async deleteWhiteboard(id: string): Promise<void> {
await api.delete(`/api/Whiteboard/${id}`)
},
async joinWhiteboard(code: string): Promise<JoinResult> {
const raw = await api.post<any>(`/api/Whiteboard/join`, {code: code})
return {
whiteboardId: raw.whiteboardId,
status: raw.status
}
}
}

View File

@@ -2,9 +2,14 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { Arrow, Line, Rectangle, Shape, ShapeTool, ShapeType, TextShape, Whiteboard } from '@/types/whiteboard.ts'
import { whiteboardHubService } from '@/services/whiteboardHubService.ts'
import {useWhiteboardsStore} from "@/stores/whiteboards.ts";
import router from "@/router";
import type {User} from "@/types";
export const useWhiteboardStore = defineStore('whiteboard', () => {
const whiteboard = ref<Whiteboard | null>(null)
const pendingUsers = ref<User[]>([])
const selectedTool = ref<ShapeTool>('hand')
const isConnected = ref(false)
const isLoading = ref(false)
@@ -27,6 +32,99 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
}
})
async function initializeSession(id: string) {
isLoading.value = true;
error.value = null;
try{
await whiteboardHubService.connect()
isConnected.value = true;
registerHubEvents()
await whiteboardHubService.joinWhiteboard(id)
} catch (e: any) {
error.value = e?.message ?? 'Failed to join whiteboard'
isLoading.value = false
}
}
function registerHubEvents() {
whiteboardHubService.onInitWhiteboard((wb) => {
deselectShape()
whiteboard.value = wb
isLoading.value = false
})
whiteboardHubService.onAddedRectangle((rectangle) => {
whiteboard.value?.rectangles.push(rectangle)
})
whiteboardHubService.onAddedArrow((arrow) => {
whiteboard.value?.arrows.push(arrow)
})
whiteboardHubService.onAddedLine((line) => {
whiteboard.value?.lines.push(line)
})
whiteboardHubService.onAddedTextShape((textShape) => {
whiteboard.value?.textShapes.push(textShape)
})
whiteboardHubService.onMovedShape((command) => {
applyMoveShape(command.shapeId, command.newPositionX, command.newPositionY)
})
whiteboardHubService.onJoined((userId) => {
console.log('User joined:', userId)
})
whiteboardHubService.onLeaved((userId) => {
console.log('User left:', userId)
})
whiteboardHubService.onWaitingForApproval(() => {
const infoStore = useWhiteboardsStore()
infoStore.startWaitingToJoin()
})
whiteboardHubService.onUserWaitingForApproval((user) => {
if (!pendingUsers.value.includes(user)) {
pendingUsers.value.push(user)
}
})
whiteboardHubService.onAccepted(() => {
const infoStore = useWhiteboardsStore()
infoStore.stopWaitingToJoin()
})
whiteboardHubService.onRejected(() => {
router.push('/')
alert('Your request to join was rejected.')
})
whiteboardHubService.onUserCanceledJoinRequest((userId) => {
pendingUsers.value = pendingUsers.value.filter(user => user.userId !== userId)
})
}
async function approveUser(userId: string) {
await whiteboardHubService.acceptUser(userId)
pendingUsers.value = pendingUsers.value.filter(user => user.userId !== userId)
}
async function rejectUser(userId: string) {
await whiteboardHubService.rejectUser(userId)
pendingUsers.value = pendingUsers.value.filter(user => user.userId !== userId)
}
async function cancelJoinRequest() {
await whiteboardHubService.cancelJoinRequest()
whiteboard.value = null
}
async function joinWhiteboard(id: string) {
isLoading.value = true
error.value = null
@@ -35,39 +133,7 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
await whiteboardHubService.connect()
isConnected.value = true
whiteboardHubService.onInitWhiteboard((wb) => {
deselectShape()
whiteboard.value = wb
isLoading.value = false
})
whiteboardHubService.onAddedRectangle((rectangle) => {
whiteboard.value?.rectangles.push(rectangle)
})
whiteboardHubService.onAddedArrow((arrow) => {
whiteboard.value?.arrows.push(arrow)
})
whiteboardHubService.onAddedLine((line) => {
whiteboard.value?.lines.push(line)
})
whiteboardHubService.onAddedTextShape((textShape) => {
whiteboard.value?.textShapes.push(textShape)
})
whiteboardHubService.onMovedShape((command) => {
applyMoveShape(command.shapeId, command.newPositionX, command.newPositionY)
})
whiteboardHubService.onJoined((userId) => {
console.log('User joined:', userId)
})
whiteboardHubService.onLeaved((userId) => {
console.log('User left:', userId)
})
registerHubEvents()
await whiteboardHubService.joinWhiteboard(id)
} catch (e: any) {
@@ -184,6 +250,10 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
toolColor,
toolThickness,
toolTextSize,
pendingUsers,
approveUser,
rejectUser,
cancelJoinRequest,
joinWhiteboard,
leaveWhiteboard,
addRectangle,

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Whiteboard } from '@/types'
import type {JoinResult, Whiteboard} from '@/types'
import { whiteboardService } from '@/services/whiteboardService'
export const useWhiteboardsStore = defineStore('whiteboards', () => {
@@ -8,6 +8,7 @@ export const useWhiteboardsStore = defineStore('whiteboards', () => {
const recentWhiteboards = ref<Whiteboard[]>([])
const currentWhiteboard = ref<Whiteboard | null>(null)
const isLoading = ref(false)
const isWaitingToJoin = ref(false)
const error = ref<string | null>(null)
async function getWhiteboardHistory() {
@@ -53,6 +54,27 @@ export const useWhiteboardsStore = defineStore('whiteboards', () => {
return newWhiteboard.id;
}
async function joinWhiteboardWithCode(code: string): Promise<JoinResult> {
isLoading.value = true;
try {
return await whiteboardService.joinWhiteboard(code);
} catch (err: any) {
error.value = err.message ?? 'Failed to join whiteboard';
throw err;
} finally {
isLoading.value = false;
}
}
function startWaitingToJoin() {
isWaitingToJoin.value = true;
}
function stopWaitingToJoin() {
isWaitingToJoin.value = false;
}
async function deleteWhiteboard(id: string): Promise<void> {
isLoading.value = true
error.value = null
@@ -92,10 +114,14 @@ export const useWhiteboardsStore = defineStore('whiteboards', () => {
ownedWhiteboards: ownedWhiteboards,
recentWhiteboards: recentWhiteboards,
isLoading,
isWaitingToJoin,
error,
getWhiteboardHistory: getWhiteboardHistory,
getRecentWhiteboards: getRecentWhiteboards,
createNewWhiteboard: createNewWhiteboard,
joinWhiteboardWithCode: joinWhiteboardWithCode,
startWaitingToJoin: startWaitingToJoin,
stopWaitingToJoin: stopWaitingToJoin,
deleteWhiteboard: deleteWhiteboard,
getCurrentWhiteboard: getCurrentWhiteboard,
selectWhiteboard: selectWhiteboard,

View File

@@ -1,4 +1,4 @@
import {type WhiteboardJoinPolicy, WhiteboardState} from "@/enums";
import {MembershipStatus, type WhiteboardJoinPolicy, WhiteboardState} from "@/enums";
export interface User {
userId: string
@@ -22,6 +22,11 @@ export interface AuthResponse {
refreshToken: string
}
export interface JoinResult {
whiteboardId: string
status: MembershipStatus
}
export interface Whiteboard {
id: string
ownerId: string

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import {ref} from 'vue'
import {useRouter} from 'vue-router'
import WhiteboardHistorySidebar from '@/components/WhiteboardHistorySidebar.vue'
import RecentWhiteboardsPanel from '@/components/RecentWhiteboardsPanel.vue'
import { useAuthStore } from '@/stores/auth'
import { useWhiteboardsStore } from "@/stores/whiteboards.ts";
import {useAuthStore} from '@/stores/auth'
import {useWhiteboardsStore} from "@/stores/whiteboards.ts";
import {MembershipStatus} from "@/enums";
const auth = useAuthStore()
const whiteboards = useWhiteboardsStore()
@@ -33,6 +34,27 @@ async function handleCreateNewWhiteboard() {
}
}
async function joinWithCode() {
if (joinCode.value.length !== 8) {
alert('Please enter a valid 8-digit code.')
return
}
try {
const joinResult = await whiteboards.joinWhiteboardWithCode(joinCode.value)
if (joinResult.status === MembershipStatus.Pending) {
whiteboards.startWaitingToJoin()
} else {
whiteboards.stopWaitingToJoin()
}
await router.push({ name: 'whiteboard', params: { id: joinResult.whiteboardId } })
} catch (err: any) {
console.error(err)
}
}
</script>
<template>
@@ -80,7 +102,7 @@ async function handleCreateNewWhiteboard() {
pattern="[0-9]*"
@input="joinCode = joinCode.replace(/\D/g, '')"
/>
<button class="btn btn-primary w-75 mt-2 d-block mx-auto">Join with code</button>
<button class="btn btn-primary w-75 mt-2 d-block mx-auto" @click="joinWithCode">Join with code</button>
<div class="text-center">
<small class="text-muted my-4 d-inline-block">or</small>
</div>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, onBeforeMount, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth.ts'
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
import { useWhiteboardsStore } from "@/stores/whiteboards.ts";
import WhiteboardToolbar from '@/components/whiteboard/WhiteboardToolbar.vue'
@@ -8,6 +9,7 @@ import WhiteboardCanvas from '@/components/whiteboard/WhiteboardCanvas.vue'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const sessionStore = useWhiteboardStore()
const infoStore = useWhiteboardsStore()
@@ -30,13 +32,31 @@ onUnmounted(() => {
})
async function handleLeave() {
await sessionStore.leaveWhiteboard()
if (infoStore.isWaitingToJoin) {
await sessionStore.cancelJoinRequest()
} else {
await sessionStore.leaveWhiteboard()
}
router.back()
}
</script>
<template>
<div v-if="sessionStore.isLoading" class="d-flex flex-column justify-content-center align-items-center vh-100">
<div v-if="infoStore.isWaitingToJoin"
class="d-flex flex-column justify-content-center align-items-center vh-100 text-center">
<div class="spinner-border text-primary mb-4" role="status">
<span class="visually-hidden">Waiting...</span>
</div>
<h5 class="mb-3">Waiting for owner's approval</h5>
<button class="btn btn-outline-danger"
@click="handleLeave">
Cancel
</button>
</div>
<div v-else-if="sessionStore.isLoading" class="d-flex flex-column justify-content-center align-items-center vh-100">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
@@ -54,5 +74,21 @@ async function handleLeave() {
<div v-else class="d-flex vh-100">
<WhiteboardToolbar @leave="handleLeave" />
<WhiteboardCanvas />
<div v-if="sessionStore.whiteboard?.ownerId === authStore.user?.userId && sessionStore.pendingUsers.length > 0"
class="position-fixed top-0 end-0 m-4 p-3 bg-dark border border-primary rounded shadow-lg"
style="z-index: 1050; width: 300px;">
<h6 class="text-primary mb-3">Pending Join Requests ({{ sessionStore.pendingUsers.length }})</h6>
<div class="list-group list-group-flush bg-transparent">
<div v-for="user in sessionStore.pendingUsers" :key="user.userId"
class="list-group-item bg-transparent text-light border-secondary d-flex justify-content-between align-items-center px-0">
<small class="text-truncate" :title="user.username">{{ user.username }}</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-success" @click="sessionStore.approveUser(user.userId)"></button>
<button class="btn btn-danger" @click="sessionStore.rejectUser(user.userId)"></button>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -17,6 +17,9 @@ export default defineConfig({
},
server: {
host: true,
strictPort: false,
allowedHosts: ['.ngrok-free.app'],
proxy: {
'/api': {
target: 'http://localhost:5266',