Правки админки, вход
This commit is contained in:
@@ -10,9 +10,4 @@ public interface IChatQueryService
|
|||||||
Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken);
|
Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUserDeleterService
|
|
||||||
{
|
|
||||||
Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ChatInfo(Guid Id, string? Avatar);
|
public record ChatInfo(Guid Id, string? Avatar);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
|
namespace Knot.Contracts.Conversations.Abstractions;
|
||||||
|
|
||||||
|
public interface IUserDeleterService
|
||||||
|
{
|
||||||
|
Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using BCrypt.Net;
|
||||||
|
using Knot.Contracts.Auth.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Auth.Domain;
|
||||||
|
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Admin.Application.Admin.Commands.CreateUser;
|
||||||
|
|
||||||
|
public record CreateUserCommand(
|
||||||
|
string Username,
|
||||||
|
string Password,
|
||||||
|
string DisplayName,
|
||||||
|
string? Email = null,
|
||||||
|
string? Bio = null
|
||||||
|
) : ICommand<AdminUserDto>;
|
||||||
|
|
||||||
|
internal sealed class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, AdminUserDto>
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IAuthUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public CreateUserCommandHandler(IUserRepository userRepository, IAuthUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<AdminUserDto>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Проверка уникальности username
|
||||||
|
if (!await _userRepository.IsUsernameUniqueAsync(request.Username, cancellationToken))
|
||||||
|
{
|
||||||
|
return Result.Failure<AdminUserDto>(new Error("Admin.CreateUser.UsernameNotUnique", "Username is already taken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хеширование пароля
|
||||||
|
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
||||||
|
|
||||||
|
// Создание контракта пользователя
|
||||||
|
var userContract = new UserContract
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = request.Username,
|
||||||
|
PasswordHash = passwordHash,
|
||||||
|
DisplayName = request.DisplayName,
|
||||||
|
Email = request.Email,
|
||||||
|
Bio = request.Bio,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
IsBanned = false,
|
||||||
|
IsOnline = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_unitOfWork.Add(userContract);
|
||||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Success(new AdminUserDto(
|
||||||
|
userContract.Id,
|
||||||
|
userContract.Username,
|
||||||
|
userContract.DisplayName,
|
||||||
|
userContract.Email,
|
||||||
|
userContract.Avatar,
|
||||||
|
userContract.CreatedAt,
|
||||||
|
userContract.IsOnline,
|
||||||
|
userContract.LastSeen ?? DateTime.UtcNow,
|
||||||
|
userContract.IsBanned));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Knot.Contracts.Conversations.Abstractions;
|
||||||
using Knot.Contracts.Settings.Application.Abstractions;
|
using Knot.Contracts.Settings.Application.Abstractions;
|
||||||
using Knot.Contracts.Settings.Application.DTOs;
|
using Knot.Contracts.Settings.Application.DTOs;
|
||||||
using Knot.Modules.Admin.Application.Admin.Commands;
|
using Knot.Modules.Admin.Application.Admin.Commands;
|
||||||
|
using Knot.Modules.Admin.Application.Admin.Commands.CreateUser;
|
||||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||||
using Knot.Modules.Admin.Application.Admin.Queries;
|
using Knot.Modules.Admin.Application.Admin.Queries;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -56,6 +58,12 @@ public static class AdminEndpoints
|
|||||||
return Results.Ok(result.Value);
|
return Results.Ok(result.Value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.MapPost("users", async ([FromBody] CreateUserCommand command, ISender sender, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await sender.Send(command, ct);
|
||||||
|
return result.IsSuccess ? Results.Created($"/api/admin/users/{result.Value.Id}", result.Value) : Results.BadRequest(new { error = result.Error.Description });
|
||||||
|
});
|
||||||
|
|
||||||
group.MapPost("users/{userId:guid}/reset-password", async ([FromRoute] Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) =>
|
group.MapPost("users/{userId:guid}/reset-password", async ([FromRoute] Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
||||||
@@ -106,6 +114,12 @@ public static class AdminEndpoints
|
|||||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.MapDelete("users/{userId:guid}", async ([FromRoute] Guid userId, IUserDeleterService userDeleter, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var result = await userDeleter.DeleteUserAsync(userId, ct);
|
||||||
|
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||||
|
});
|
||||||
|
|
||||||
group.MapGet("clean/dry-run", async (ISender sender, CancellationToken ct) =>
|
group.MapGet("clean/dry-run", async (ISender sender, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new CleanDryRunQuery(), ct);
|
var result = await sender.Send(new CleanDryRunQuery(), ct);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
|
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
|
||||||
services.AddScoped<ConversationsAbstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
|
services.AddScoped<ConversationsAbstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
|
||||||
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
|
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
|
||||||
|
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserDeleterService, Knot.Modules.Conversations.Infrastructure.Services.UserDeleterService>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.Infrastructure;
|
||||||
|
|
||||||
|
[DbContext(typeof(ChatsDbContext))]
|
||||||
|
public class ChatsDbContextFactory : IDesignTimeDbContextFactory<ChatsDbContext>
|
||||||
|
{
|
||||||
|
public ChatsDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<ChatsDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql("Host=localhost;Database=knot_db;Username=knot;Password=knot_pass");
|
||||||
|
return new ChatsDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-5
@@ -18,11 +18,16 @@ namespace Knot.Modules.Conversations.Infrastructure.Persistence;
|
|||||||
|
|
||||||
public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
|
public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator? _mediator;
|
||||||
private readonly IEncryptionService _encryptionService;
|
private readonly IEncryptionService? _encryptionService;
|
||||||
|
|
||||||
|
public ChatsDbContext(DbContextOptions<ChatsDbContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public ChatsDbContext(DbContextOptions<ChatsDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
public ChatsDbContext(DbContextOptions<ChatsDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
||||||
: base(options)
|
: this(options)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_encryptionService = encryptionService;
|
_encryptionService = encryptionService;
|
||||||
@@ -99,9 +104,12 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli
|
|||||||
|
|
||||||
int result = await base.SaveChangesAsync(cancellationToken);
|
int result = await base.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
foreach (var domainEvent in domainEvents)
|
if (_mediator != null)
|
||||||
{
|
{
|
||||||
await _mediator.Publish(domainEvent, cancellationToken);
|
foreach (var domainEvent in domainEvents)
|
||||||
|
{
|
||||||
|
await _mediator.Publish(domainEvent, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Knot.Contracts.Conversations.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.Infrastructure.Services;
|
||||||
|
|
||||||
|
public sealed class UserDeleterService : IUserDeleterService
|
||||||
|
{
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
public UserDeleterService(ISender sender)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _sender.Send(new DeleteUserCommand(userId), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|||||||
@@ -543,6 +543,9 @@ export default function AdminPage() {
|
|||||||
const [newUser, setNewUser] = useState({ username: '', displayName: '', password: '' });
|
const [newUser, setNewUser] = useState({ username: '', displayName: '', password: '' });
|
||||||
const [generatedPass, setGeneratedPass] = useState('');
|
const [generatedPass, setGeneratedPass] = useState('');
|
||||||
|
|
||||||
|
// Delete User Modal State
|
||||||
|
const [deleteUserModal, setDeleteUserModal] = useState<{ userId: string; username: string } | null>(null);
|
||||||
|
|
||||||
const [timezones, setTimezones] = useState<TimezoneDto[]>([]);
|
const [timezones, setTimezones] = useState<TimezoneDto[]>([]);
|
||||||
const [tzSearch, setTzSearch] = useState('');
|
const [tzSearch, setTzSearch] = useState('');
|
||||||
const [showTzDropdown, setShowTzDropdown] = useState(false);
|
const [showTzDropdown, setShowTzDropdown] = useState(false);
|
||||||
@@ -616,7 +619,7 @@ export default function AdminPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteUser = async (userId: string) => {
|
const handleDeleteUser = async (userId: string) => {
|
||||||
if (!confirm(t.confirmDelete)) return;
|
setDeleteUserModal(null);
|
||||||
try {
|
try {
|
||||||
await httpClient.request(`/admin/users/${userId}`, { method: 'DELETE' });
|
await httpClient.request(`/admin/users/${userId}`, { method: 'DELETE' });
|
||||||
showToast(t.successSave, 'success');
|
showToast(t.successSave, 'success');
|
||||||
@@ -1443,11 +1446,10 @@ export default function AdminPage() {
|
|||||||
<button
|
<button
|
||||||
key={u.id}
|
key={u.id}
|
||||||
onClick={() => fetchUserStats(u.id)}
|
onClick={() => fetchUserStats(u.id)}
|
||||||
className={`flex items-center gap-3 p-4 rounded-2xl transition-all text-left group border shadow-sm ${
|
className={`flex items-center gap-3 p-4 rounded-2xl transition-all text-left group border shadow-sm ${selectedUser?.id === u.id
|
||||||
selectedUser?.id === u.id
|
? 'bg-primary/10 border-primary/30 scale-[1.01] shadow-primary/5'
|
||||||
? 'bg-primary/10 border-primary/30 scale-[1.01] shadow-primary/5'
|
: 'hover:bg-surface-container-highest/10 border-transparent'
|
||||||
: 'hover:bg-surface-container-highest/10 border-transparent'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-black shrink-0 slide-on-ice border border-primary/10">
|
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-black shrink-0 slide-on-ice border border-primary/10">
|
||||||
@@ -1535,7 +1537,7 @@ export default function AdminPage() {
|
|||||||
{t.blockUser}
|
{t.blockUser}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => handleDeleteUser(selectedUser.id)} className="w-full py-4 rounded-2xl bg-error/5 hover:bg-error/10 text-error/60 hover:text-error font-black uppercase text-[10px] tracking-widest transition-all slide-on-ice">
|
<button onClick={() => setDeleteUserModal({ userId: selectedUser.id, username: selectedUser.username })} className="w-full py-4 rounded-2xl bg-error/5 hover:bg-error/10 text-error/60 hover:text-error font-black uppercase text-[10px] tracking-widest transition-all slide-on-ice">
|
||||||
{t.deleteUser}
|
{t.deleteUser}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1566,7 +1568,7 @@ export default function AdminPage() {
|
|||||||
<label className="flex flex-col gap-2">
|
<label className="flex flex-col gap-2">
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-on-surface-variant/40 ml-1">{t.displayName}</span>
|
<span className="text-[10px] font-black uppercase tracking-widest text-on-surface-variant/40 ml-1">{t.displayName}</span>
|
||||||
<div className="knot-input-group">
|
<div className="knot-input-group">
|
||||||
<input value={newUser.displayName} onChange={e => setNewUser({ ...newUser, displayName: e.target.value })} placeholder={t.displayNamePlaceholder} />
|
<input value={newUser.displayName} onChange={e => setNewUser({ ...newUser, displayName: e.target.value })} placeholder={t.displayNamePlaceholder} />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex flex-col gap-2">
|
<label className="flex flex-col gap-2">
|
||||||
@@ -1605,6 +1607,61 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete User Confirmation Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteUserModal && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={() => setDeleteUserModal(null)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.9, opacity: 0 }}
|
||||||
|
className="bg-surface-container-high border border-outline-variant/20 rounded-3xl p-8 max-w-md w-full shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-error/10 flex items-center justify-center">
|
||||||
|
<span className="material-symbols-outlined text-error text-2xl">warning</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black">{lang === 'ru' ? 'Удаление пользователя' : 'Delete User'}</h3>
|
||||||
|
<p className="text-sm text-on-surface-variant/60">{lang === 'ru' ? 'Это действие необратимо' : 'This action is irreversible'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-surface-container-lowest/50 rounded-2xl p-4 mb-6">
|
||||||
|
<p className="text-sm text-on-surface-variant/60 mb-2">{lang === 'ru' ? 'Вы собираетесь удалить:' : 'You are about to delete:'}</p>
|
||||||
|
<p className="text-lg font-black text-on-surface">@{deleteUserModal.username}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-on-surface-variant/40 mb-8">
|
||||||
|
{t.confirmDelete}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteUserModal(null)}
|
||||||
|
className="flex-1 py-4 rounded-2xl bg-surface-container-highest hover:bg-surface-container-high transition-all font-black uppercase text-[12px] tracking-widest"
|
||||||
|
>
|
||||||
|
{t.cancel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteUser(deleteUserModal.userId)}
|
||||||
|
className="flex-1 py-4 rounded-2xl bg-error hover:bg-error/90 text-white font-black uppercase text-[12px] tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
{lang === 'ru' ? 'Удалить' : 'Delete'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1613,11 +1670,10 @@ function NavButton({ icon, label, active, onClick }: { icon: string, label: stri
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`w-full flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-400 ease-ice group relative ${
|
className={`w-full flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-400 ease-ice group relative ${active
|
||||||
active
|
? 'bg-primary/10 text-primary'
|
||||||
? 'bg-primary/10 text-primary'
|
: 'text-on-surface-variant/40 hover:text-on-surface hover:bg-surface-container-highest/10'
|
||||||
: 'text-on-surface-variant/40 hover:text-on-surface hover:bg-surface-container-highest/10'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{active && (
|
{active && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -1638,16 +1694,14 @@ function Toggle({ checked, onChange }: { checked: boolean, onChange: (v: boolean
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange(!checked)}
|
onClick={() => onChange(!checked)}
|
||||||
className={`w-11 h-6 rounded-full relative transition-all duration-300 ${
|
className={`w-11 h-6 rounded-full relative transition-all duration-300 ${checked ? 'bg-primary' : 'bg-surface-container-highest'
|
||||||
checked ? 'bg-primary' : 'bg-surface-container-highest'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ x: checked ? 22 : 3 }}
|
animate={{ x: checked ? 22 : 3 }}
|
||||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||||
className={`absolute top-1 w-4 h-4 rounded-full ${
|
className={`absolute top-1 w-4 h-4 rounded-full ${checked ? 'bg-surface-container-lowest' : 'bg-on-surface-variant/40'
|
||||||
checked ? 'bg-surface-container-lowest' : 'bg-on-surface-variant/40'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,17 +3,37 @@ import type { User } from '../../../core/domain/types';
|
|||||||
|
|
||||||
export class AuthApi {
|
export class AuthApi {
|
||||||
static async login(username: string, password: string) {
|
static async login(username: string, password: string) {
|
||||||
return httpClient.request<{ token: string; user: User }>('/auth/login', {
|
const response = await httpClient.request<{ accessToken: string; userId: string; username: string; displayName: string }>('/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: response.accessToken,
|
||||||
|
user: {
|
||||||
|
id: response.userId,
|
||||||
|
username: response.username,
|
||||||
|
displayName: response.displayName,
|
||||||
|
avatar: null
|
||||||
|
} as User
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async register(username: string, displayName: string, password: string, bio?: string) {
|
static async register(username: string, displayName: string, password: string, bio?: string) {
|
||||||
return httpClient.request<{ token: string; user: User }>('/auth/register', {
|
const response = await httpClient.request<{ accessToken: string; userId: string; username: string; displayName: string }>('/auth/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, displayName, password, bio }),
|
body: JSON.stringify({ username, displayName, password, bio }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: response.accessToken,
|
||||||
|
user: {
|
||||||
|
id: response.userId,
|
||||||
|
username: response.username,
|
||||||
|
displayName: response.displayName,
|
||||||
|
avatar: null
|
||||||
|
} as User
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getMe() {
|
static async getMe() {
|
||||||
|
|||||||
Reference in New Issue
Block a user