Dev #11
@@ -41,21 +41,26 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
|
|||||||
yield return E("auth/logout [AnyAuth]", "POST", "api/v1/auth/logout", "Student");
|
yield return E("auth/logout [AnyAuth]", "POST", "api/v1/auth/logout", "Student");
|
||||||
yield return E("auth/me [AnyAuth]", "GET", "api/v1/auth/me", "Student");
|
yield return E("auth/me [AnyAuth]", "GET", "api/v1/auth/me", "Student");
|
||||||
|
|
||||||
// ── Users — any auth ──────────────────────────────────────────────────
|
// ── Users — current user ──────────────────────────────────────────────
|
||||||
yield return E("users/{id} GET [AnyAuth]", "GET", "api/v1/users/1", "Student");
|
yield return E("users/me GET [AnyAuth]", "GET", "api/v1/users/me", "Student");
|
||||||
yield return E("users/{id} PUT [AnyAuth/self]", "PUT", "api/v1/users/1", "Student",
|
yield return E("users/me PUT [AnyAuth]", "PUT", "api/v1/users/me", "Student",
|
||||||
body: """{"displayName":"Test","avatarUrl":null}""");
|
body: """{"displayName":"Test","avatarUrl":null}""");
|
||||||
yield return E("users/{id}/stats [AnyAuth]", "GET", "api/v1/users/1/stats", "Student");
|
yield return E("users/me/stats [AnyAuth]", "GET", "api/v1/users/me/stats", "Student");
|
||||||
yield return E("users/{id}/enrollments [AnyAuth]", "GET", "api/v1/users/1/enrollments", "Student");
|
yield return E("users/me/enrollments [AnyAuth]", "GET", "api/v1/users/me/enrollments", "Student");
|
||||||
yield return E("users/{id}/achievements [AnyAuth]","GET", "api/v1/users/1/achievements","Student");
|
yield return E("users/me/reviews [AnyAuth]", "GET", "api/v1/users/me/reviews", "Student");
|
||||||
yield return E("users/{id}/transactions [AnyAuth/self]","GET","api/v1/users/1/transactions","Student");
|
yield return E("users/me/achievements [AnyAuth]", "GET", "api/v1/users/me/achievements", "Student");
|
||||||
|
yield return E("users/me/transactions [AnyAuth]", "GET", "api/v1/users/me/transactions", "Student");
|
||||||
// ── Users — Admin OR Teacher ─────────────────────────────────────────
|
|
||||||
yield return E("users/{id}/reviews [Admin]", "GET", "api/v1/users/1/reviews","Admin", forbidden: ["Student"]);
|
|
||||||
yield return E("users/{id}/reviews [Teacher]", "GET", "api/v1/users/1/reviews","Teacher", forbidden: ["Student"]);
|
|
||||||
|
|
||||||
// ── Users — Admin only ────────────────────────────────────────────────
|
// ── Users — Admin only ────────────────────────────────────────────────
|
||||||
yield return E("users GET [Admin]", "GET", "api/v1/users", "Admin", forbidden: ["Student", "Teacher"]);
|
yield return E("users GET [Admin]", "GET", "api/v1/users", "Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id} GET [Admin]", "GET", "api/v1/users/1", "Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id} PUT [Admin]", "PUT", "api/v1/users/1", "Admin", forbidden: ["Student", "Teacher"],
|
||||||
|
body: """{"displayName":"Test","avatarUrl":null}""");
|
||||||
|
yield return E("users/{id}/stats [Admin]", "GET", "api/v1/users/1/stats", "Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id}/enrollments [Admin]", "GET", "api/v1/users/1/enrollments", "Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id}/reviews [Admin]", "GET", "api/v1/users/1/reviews","Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id}/achievements [Admin]", "GET", "api/v1/users/1/achievements","Admin", forbidden: ["Student", "Teacher"]);
|
||||||
|
yield return E("users/{id}/transactions [Admin]", "GET", "api/v1/users/1/transactions","Admin", forbidden: ["Student", "Teacher"]);
|
||||||
yield return E("users/{id}/role PATCH [Admin]", "PATCH", "api/v1/users/1/role", "Admin", forbidden: ["Student", "Teacher"],
|
yield return E("users/{id}/role PATCH [Admin]", "PATCH", "api/v1/users/1/role", "Admin", forbidden: ["Student", "Teacher"],
|
||||||
body: "\"Student\"");
|
body: "\"Student\"");
|
||||||
yield return E("users/{id}/active PATCH [Admin]", "PATCH", "api/v1/users/1/active", "Admin", forbidden: ["Student", "Teacher"],
|
yield return E("users/{id}/active PATCH [Admin]", "PATCH", "api/v1/users/1/active", "Admin", forbidden: ["Student", "Teacher"],
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
var stub = Substitute.For<IAuthService>();
|
var stub = Substitute.For<IAuthService>();
|
||||||
var authResult = new AuthResult(
|
var authResult = new AuthResult(
|
||||||
new AuthResponse("access_token", DateTime.UtcNow.AddHours(1),
|
new AuthResponse("access_token", DateTime.UtcNow.AddHours(1),
|
||||||
new UserAuthDto(1, "test@test.com", "Test User", [UserRole.Student])),
|
new UserAuthDto("test@test.com", "Test User", [UserRole.Student])),
|
||||||
"refresh_token");
|
"refresh_token");
|
||||||
stub.LoginWithMicrosoftAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<string?>())
|
stub.LoginWithMicrosoftAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<string?>())
|
||||||
.Returns(authResult);
|
.Returns(authResult);
|
||||||
@@ -123,7 +123,7 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
.Returns(authResult);
|
.Returns(authResult);
|
||||||
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
||||||
stub.GetCurrentUserAsync(Arg.Any<int>())
|
stub.GetCurrentUserAsync(Arg.Any<int>())
|
||||||
.Returns(new CurrentUserDto(1, "test@test.com", "Test", null, [UserRole.Student], 0, 0, 1, DateTime.UtcNow));
|
.Returns(new CurrentUserDto("test@test.com", "Test", null, [UserRole.Student], 0, 0, 1, DateTime.UtcNow));
|
||||||
return stub;
|
return stub;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +153,16 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
var stub = Substitute.For<IUserService>();
|
var stub = Substitute.For<IUserService>();
|
||||||
var userDto = new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow);
|
var userDto = new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow);
|
||||||
var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
|
var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
|
||||||
|
var lectureDto = new LectureDto(1, 1, "Course", null, null, null, null,
|
||||||
|
"Title", null, LectureFormat.Offline,
|
||||||
|
DateTime.UtcNow, DateTime.UtcNow.AddHours(2),
|
||||||
|
true, 30, 0, null, DateTime.UtcNow, true);
|
||||||
|
var pagedLectures = PagedResult<LectureDto>.Create([lectureDto], 1, 1, 20);
|
||||||
|
|
||||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
||||||
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
||||||
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0, 0, 100));
|
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0, 0, 100));
|
||||||
|
stub.GetEnrollmentsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedLectures);
|
||||||
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
||||||
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
||||||
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||||
|
|||||||
@@ -26,73 +26,163 @@ public class UsersController : ControllerBase
|
|||||||
|
|
||||||
private int CurrentUserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
private int CurrentUserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||||
|
|
||||||
|
private static CurrentUserDto ToCurrentUserDto(UserDto user) => new(
|
||||||
|
user.Email,
|
||||||
|
user.DisplayName,
|
||||||
|
user.AvatarUrl,
|
||||||
|
user.Roles,
|
||||||
|
user.Xp,
|
||||||
|
user.Coins,
|
||||||
|
user.Level,
|
||||||
|
user.CreatedAt);
|
||||||
|
|
||||||
|
/// <summary>Получить профиль текущего пользователя.</summary>
|
||||||
|
/// <response code="200">Данные текущего пользователя.</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[HttpGet("me")]
|
||||||
|
[ProducesResponseType(typeof(CurrentUserDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<CurrentUserDto>> GetMe() =>
|
||||||
|
Ok(ToCurrentUserDto(await _users.GetByIdAsync(CurrentUserId)));
|
||||||
|
|
||||||
|
/// <summary>Обновить профиль текущего пользователя (displayName, avatarUrl).</summary>
|
||||||
|
/// <param name="req">Обновляемые поля профиля.</param>
|
||||||
|
/// <response code="200">Обновлённые данные текущего пользователя.</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[HttpPut("me")]
|
||||||
|
[ProducesResponseType(typeof(CurrentUserDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<CurrentUserDto>> UpdateMe([FromBody] UpdateUserRequest req) =>
|
||||||
|
Ok(ToCurrentUserDto(await _users.UpdateProfileAsync(CurrentUserId, req)));
|
||||||
|
|
||||||
|
/// <summary>Получить статистику текущего пользователя.</summary>
|
||||||
|
/// <response code="200">Статистика текущего пользователя.</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[HttpGet("me/stats")]
|
||||||
|
[ProducesResponseType(typeof(UserStatsDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<UserStatsDto>> MyStats() =>
|
||||||
|
Ok(await _users.GetStatsAsync(CurrentUserId));
|
||||||
|
|
||||||
|
/// <summary>Получить список записей текущего пользователя на лекции.</summary>
|
||||||
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
|
/// <response code="200">Список записей (пагинированный).</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[HttpGet("me/enrollments")]
|
||||||
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Lectures.LectureDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult> MyEnrollments([FromQuery] PaginationRequest pagination) =>
|
||||||
|
Ok(await _users.GetEnrollmentsAsync(CurrentUserId, pagination));
|
||||||
|
|
||||||
|
/// <summary>Получить отзывы текущего пользователя.</summary>
|
||||||
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
|
/// <response code="200">Список отзывов (пагинированный).</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
[HttpGet("me/reviews")]
|
||||||
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Reviews.ReviewDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<ActionResult> MyReviews([FromQuery] PaginationRequest pagination) =>
|
||||||
|
Ok(await _reviews.GetByUserAsync(CurrentUserId, pagination));
|
||||||
|
|
||||||
|
/// <summary>Получить достижения текущего пользователя.</summary>
|
||||||
|
/// <response code="200">Список полученных достижений.</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
[HttpGet("me/achievements")]
|
||||||
|
[ProducesResponseType(typeof(List<UniVerse.Application.DTOs.Achievements.UserAchievementDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<ActionResult> MyAchievements() =>
|
||||||
|
Ok(await _gamification.GetUserAchievementsAsync(CurrentUserId));
|
||||||
|
|
||||||
|
/// <summary>Получить историю транзакций монет текущего пользователя.</summary>
|
||||||
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
|
/// <response code="200">История транзакций (пагинированная).</response>
|
||||||
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
[HttpGet("me/transactions")]
|
||||||
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Gamification.CoinTransactionDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<ActionResult> MyTransactions([FromQuery] PaginationRequest pagination) =>
|
||||||
|
Ok(await _gamification.GetTransactionsAsync(CurrentUserId, pagination));
|
||||||
|
|
||||||
/// <summary>Получить профиль пользователя по ID.</summary>
|
/// <summary>Получить профиль пользователя по ID.</summary>
|
||||||
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <response code="200">Данные пользователя.</response>
|
/// <response code="200">Данные пользователя.</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
/// <response code="404">Пользователь не найден.</response>
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<UserDto>> Get(int id) => Ok(await _users.GetByIdAsync(id));
|
public async Task<ActionResult<UserDto>> Get(int id) => Ok(await _users.GetByIdAsync(id));
|
||||||
|
|
||||||
/// <summary>Обновить профиль пользователя (displayName, avatarUrl).</summary>
|
/// <summary>Обновить профиль пользователя (displayName, avatarUrl).</summary>
|
||||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
/// <remarks>Только Admin. Для текущего пользователя используйте PUT /api/v1/users/me.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <param name="req">Обновляемые поля профиля.</param>
|
/// <param name="req">Обновляемые поля профиля.</param>
|
||||||
/// <response code="200">Обновлённые данные пользователя.</response>
|
/// <response code="200">Обновлённые данные пользователя.</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
/// <response code="403">Нет прав — только владелец профиля или Admin.</response>
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
/// <response code="404">Пользователь не найден.</response>
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpPut("{id:int}")]
|
[HttpPut("{id:int}")]
|
||||||
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<UserDto>> Update(int id, [FromBody] UpdateUserRequest req)
|
public async Task<ActionResult<UserDto>> Update(int id, [FromBody] UpdateUserRequest req) =>
|
||||||
{
|
Ok(await _users.UpdateProfileAsync(id, req));
|
||||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
|
||||||
return Ok(await _users.UpdateProfileAsync(id, req));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получить статистику пользователя (XP, монеты, уровень, посещения).</summary>
|
/// <summary>Получить статистику пользователя (XP, монеты, уровень, посещения).</summary>
|
||||||
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/stats.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <response code="200">Статистика пользователя.</response>
|
/// <response code="200">Статистика пользователя.</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
/// <response code="404">Пользователь не найден.</response>
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}/stats")]
|
[HttpGet("{id:int}/stats")]
|
||||||
[ProducesResponseType(typeof(UserStatsDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(UserStatsDto), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<UserStatsDto>> Stats(int id) => Ok(await _users.GetStatsAsync(id));
|
public async Task<ActionResult<UserStatsDto>> Stats(int id) => Ok(await _users.GetStatsAsync(id));
|
||||||
|
|
||||||
/// <summary>Получить список записей пользователя на лекции.</summary>
|
/// <summary>Получить список записей пользователя на лекции.</summary>
|
||||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/enrollments.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <param name="pagination">Параметры пагинации.</param>
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
/// <response code="200">Список записей (пагинированный).</response>
|
/// <response code="200">Список записей (пагинированный).</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
/// <response code="403">Нет прав — только владелец или Admin.</response>
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}/enrollments")]
|
[HttpGet("{id:int}/enrollments")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Lectures.LectureDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> Enrollments(int id, [FromQuery] PaginationRequest pagination)
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
{
|
public async Task<ActionResult> Enrollments(int id, [FromQuery] PaginationRequest pagination) =>
|
||||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
Ok(await _users.GetEnrollmentsAsync(id, pagination));
|
||||||
// Delegate to lecture service would be more proper, but returning reviews for now
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получить отзывы пользователя.</summary>
|
/// <summary>Получить отзывы пользователя.</summary>
|
||||||
/// <remarks>Только Admin или Teacher.</remarks>
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/reviews.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <param name="pagination">Параметры пагинации.</param>
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
/// <response code="200">Список отзывов (пагинированный).</response>
|
/// <response code="200">Список отзывов (пагинированный).</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
/// <response code="403">Требуется роль Admin или Teacher.</response>
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
[Authorize(Roles = "Admin,Teacher")]
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}/reviews")]
|
[HttpGet("{id:int}/reviews")]
|
||||||
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Reviews.ReviewDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Reviews.ReviewDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
@@ -101,31 +191,33 @@ public class UsersController : ControllerBase
|
|||||||
Ok(await _reviews.GetByUserAsync(id, pagination));
|
Ok(await _reviews.GetByUserAsync(id, pagination));
|
||||||
|
|
||||||
/// <summary>Получить достижения пользователя.</summary>
|
/// <summary>Получить достижения пользователя.</summary>
|
||||||
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/achievements.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <response code="200">Список полученных достижений.</response>
|
/// <response code="200">Список полученных достижений.</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}/achievements")]
|
[HttpGet("{id:int}/achievements")]
|
||||||
[ProducesResponseType(typeof(List<UniVerse.Application.DTOs.Achievements.UserAchievementDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(List<UniVerse.Application.DTOs.Achievements.UserAchievementDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> Achievements(int id) =>
|
public async Task<ActionResult> Achievements(int id) =>
|
||||||
Ok(await _gamification.GetUserAchievementsAsync(id));
|
Ok(await _gamification.GetUserAchievementsAsync(id));
|
||||||
|
|
||||||
/// <summary>Получить историю транзакций монет пользователя.</summary>
|
/// <summary>Получить историю транзакций монет пользователя.</summary>
|
||||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
/// <remarks>Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/transactions.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <param name="pagination">Параметры пагинации.</param>
|
/// <param name="pagination">Параметры пагинации.</param>
|
||||||
/// <response code="200">История транзакций (пагинированная).</response>
|
/// <response code="200">История транзакций (пагинированная).</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
/// <response code="403">Нет прав — только владелец или Admin.</response>
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpGet("{id:int}/transactions")]
|
[HttpGet("{id:int}/transactions")]
|
||||||
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Gamification.CoinTransactionDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Gamification.CoinTransactionDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> Transactions(int id, [FromQuery] PaginationRequest pagination)
|
public async Task<ActionResult> Transactions(int id, [FromQuery] PaginationRequest pagination) =>
|
||||||
{
|
Ok(await _gamification.GetTransactionsAsync(id, pagination));
|
||||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
|
||||||
return Ok(await _gamification.GetTransactionsAsync(id, pagination));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получить список всех пользователей с фильтрацией и пагинацией.</summary>
|
/// <summary>Получить список всех пользователей с фильтрацией и пагинацией.</summary>
|
||||||
/// <remarks>Только Admin.</remarks>
|
/// <remarks>Только Admin.</remarks>
|
||||||
|
|||||||
@@ -3506,32 +3506,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/users/{id}": {
|
"/api/v1/users/me": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить профиль пользователя по ID.",
|
"summary": "Получить профиль текущего пользователя.",
|
||||||
"description": "**Required:** any authenticated user",
|
"description": "**Required:** any authenticated user",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"description": "ID пользователя.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Данные пользователя.",
|
"description": "Данные текущего пользователя.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserDto"
|
"$ref": "#/components/schemas/CurrentUserDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3563,12 +3551,401 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Обновить профиль текущего пользователя (displayName, avatarUrl).",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Обновляемые поля профиля.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUserRequest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUserRequest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/*+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUserRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Обновлённые данные текущего пользователя.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CurrentUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Пользователь не найден.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/me/stats": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить статистику текущего пользователя.",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Статистика текущего пользователя.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserStatsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Пользователь не найден.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/me/enrollments": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить список записей текущего пользователя на лекции.",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Page",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PageSize",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Список записей (пагинированный).",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LectureDtoPagedResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Пользователь не найден.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/me/reviews": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить отзывы текущего пользователя.",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Page",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PageSize",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Список отзывов (пагинированный).",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ReviewDtoPagedResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/me/achievements": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить достижения текущего пользователя.",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Список полученных достижений.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserAchievementDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/me/transactions": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить историю транзакций монет текущего пользователя.",
|
||||||
|
"description": "**Required:** any authenticated user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Page",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PageSize",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "История транзакций (пагинированная).",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CoinTransactionDtoPagedResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/users/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Получить профиль пользователя по ID.",
|
||||||
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me.\n\n**Required roles:** Admin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID пользователя.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Данные пользователя.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Требуется аутентификация.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Требуется роль Admin.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Пользователь не найден.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": [ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Обновить профиль пользователя (displayName, avatarUrl).",
|
"summary": "Обновить профиль пользователя (displayName, avatarUrl).",
|
||||||
"description": "Разрешено только самому пользователю или Admin.\n\n**Required:** any authenticated user",
|
"description": "Только Admin. Для текущего пользователя используйте PUT /api/v1/users/me.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3623,7 +4000,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Нет прав — только владелец профиля или Admin.",
|
"description": "Требуется роль Admin.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -3656,7 +4033,7 @@
|
|||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить статистику пользователя (XP, монеты, уровень, посещения).",
|
"summary": "Получить статистику пользователя (XP, монеты, уровень, посещения).",
|
||||||
"description": "**Required:** any authenticated user",
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/stats.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3690,6 +4067,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Требуется роль Admin.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Пользователь не найден.",
|
"description": "Пользователь не найден.",
|
||||||
"content": {
|
"content": {
|
||||||
@@ -3714,7 +4101,7 @@
|
|||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить список записей пользователя на лекции.",
|
"summary": "Получить список записей пользователя на лекции.",
|
||||||
"description": "Разрешено только самому пользователю или Admin.\n\n**Required:** any authenticated user",
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/enrollments.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3745,7 +4132,14 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Список записей (пагинированный)."
|
"description": "Список записей (пагинированный).",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LectureDtoPagedResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Требуется аутентификация.",
|
"description": "Требуется аутентификация.",
|
||||||
@@ -3758,7 +4152,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Нет прав — только владелец или Admin.",
|
"description": "Требуется роль Admin.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Пользователь не найден.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -3781,7 +4185,7 @@
|
|||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить отзывы пользователя.",
|
"summary": "Получить отзывы пользователя.",
|
||||||
"description": "Только Admin или Teacher.\n\n**Required roles:** Admin, Teacher",
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/reviews.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3832,7 +4236,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Требуется роль Admin или Teacher.",
|
"description": "Требуется роль Admin.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -3855,7 +4259,7 @@
|
|||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить достижения пользователя.",
|
"summary": "Получить достижения пользователя.",
|
||||||
"description": "**Required:** any authenticated user",
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/achievements.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3891,6 +4295,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Требуется роль Admin.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3906,7 +4320,7 @@
|
|||||||
"Users"
|
"Users"
|
||||||
],
|
],
|
||||||
"summary": "Получить историю транзакций монет пользователя.",
|
"summary": "Получить историю транзакций монет пользователя.",
|
||||||
"description": "Разрешено только самому пользователю или Admin.\n\n**Required:** any authenticated user",
|
"description": "Только Admin. Для текущего пользователя используйте GET /api/v1/users/me/transactions.\n\n**Required roles:** Admin",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@@ -3957,7 +4371,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Нет прав — только владелец или Admin.",
|
"description": "Требуется роль Admin.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -4586,10 +5000,6 @@
|
|||||||
"CurrentUserDto": {
|
"CurrentUserDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
@@ -5494,10 +5904,6 @@
|
|||||||
"UserAuthDto": {
|
"UserAuthDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace UniVerse.Application.DTOs.Auth;
|
|||||||
public record AuthResponse(string AccessToken, DateTime ExpiresAt, UserAuthDto User);
|
public record AuthResponse(string AccessToken, DateTime ExpiresAt, UserAuthDto User);
|
||||||
public record AuthResult(AuthResponse Response, string RefreshToken);
|
public record AuthResult(AuthResponse Response, string RefreshToken);
|
||||||
|
|
||||||
public record UserAuthDto(int Id, string Email, string? DisplayName, IReadOnlyList<UserRole> Roles);
|
public record UserAuthDto(string Email, string? DisplayName, IReadOnlyList<UserRole> Roles);
|
||||||
|
|
||||||
public record LoginMicrosoftRequest(string AuthorizationCode, string? RedirectUri = null);
|
public record LoginMicrosoftRequest(string AuthorizationCode, string? RedirectUri = null);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ public record UserDto(
|
|||||||
);
|
);
|
||||||
|
|
||||||
public record CurrentUserDto(
|
public record CurrentUserDto(
|
||||||
int Id,
|
|
||||||
string Email,
|
string Email,
|
||||||
string? DisplayName,
|
string? DisplayName,
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using UniVerse.Application.DTOs.Common;
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Lectures;
|
||||||
using UniVerse.Application.DTOs.Users;
|
using UniVerse.Application.DTOs.Users;
|
||||||
using UniVerse.Domain.Enums;
|
using UniVerse.Domain.Enums;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ public interface IUserService
|
|||||||
Task<UserDto> GetByIdAsync(int id);
|
Task<UserDto> GetByIdAsync(int id);
|
||||||
Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request);
|
Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request);
|
||||||
Task<UserStatsDto> GetStatsAsync(int id);
|
Task<UserStatsDto> GetStatsAsync(int id);
|
||||||
|
Task<PagedResult<LectureDto>> GetEnrollmentsAsync(int id, PaginationRequest pagination);
|
||||||
Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter);
|
Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter);
|
||||||
Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles);
|
Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles);
|
||||||
Task SetActiveAsync(int id, bool isActive);
|
Task SetActiveAsync(int id, bool isActive);
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ public static class MappingExtensions
|
|||||||
);
|
);
|
||||||
|
|
||||||
public static CurrentUserDto ToCurrentUserDto(this User user, int level) => new(
|
public static CurrentUserDto ToCurrentUserDto(this User user, int level) => new(
|
||||||
user.Id, user.Email, user.DisplayName, user.AvatarUrl,
|
user.Email, user.DisplayName, user.AvatarUrl,
|
||||||
user.Roles.Select(r => r.Role).OrderBy(r => r).ToList(), user.Xp, user.Coins, level, user.CreatedAt
|
user.Roles.Select(r => r.Role).OrderBy(r => r).ToList(), user.Xp, user.Coins, level, user.CreatedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
public static UserAuthDto ToAuthDto(this User user) => new(
|
public static UserAuthDto ToAuthDto(this User user) => new(
|
||||||
user.Id, user.Email, user.DisplayName, user.Roles.Select(r => r.Role).OrderBy(r => r).ToList()
|
user.Email, user.DisplayName, user.Roles.Select(r => r.Role).OrderBy(r => r).ToList()
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Tag ---
|
// --- Tag ---
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using UniVerse.Application.DTOs.Common;
|
using UniVerse.Application.DTOs.Common;
|
||||||
|
using UniVerse.Application.DTOs.Lectures;
|
||||||
using UniVerse.Application.DTOs.Users;
|
using UniVerse.Application.DTOs.Users;
|
||||||
using UniVerse.Application.Interfaces;
|
using UniVerse.Application.Interfaces;
|
||||||
using UniVerse.Application.Mappings;
|
using UniVerse.Application.Mappings;
|
||||||
@@ -65,6 +66,36 @@ public class UserService : IUserService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<LectureDto>> GetEnrollmentsAsync(int id, PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
if (!await _db.Users.AnyAsync(u => u.Id == id))
|
||||||
|
throw new NotFoundException("User", id);
|
||||||
|
|
||||||
|
var query = _db.LectureEnrollments
|
||||||
|
.Where(e => e.UserId == id)
|
||||||
|
.Include(e => e.Lecture)
|
||||||
|
.ThenInclude(l => l.Course)
|
||||||
|
.Include(e => e.Lecture)
|
||||||
|
.ThenInclude(l => l.Teacher)
|
||||||
|
.Include(e => e.Lecture)
|
||||||
|
.ThenInclude(l => l.Location)
|
||||||
|
.Include(e => e.Lecture)
|
||||||
|
.ThenInclude(l => l.Enrollments);
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var enrollments = await query
|
||||||
|
.OrderBy(e => e.Lecture.StartsAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return PagedResult<LectureDto>.Create(
|
||||||
|
enrollments.Select(e => e.Lecture.ToDto(isEnrolled: true)).ToList(),
|
||||||
|
total,
|
||||||
|
pagination.Page,
|
||||||
|
pagination.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
|
public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
|
||||||
{
|
{
|
||||||
var query = _db.Users.AsQueryable();
|
var query = _db.Users.AsQueryable();
|
||||||
|
|||||||
@@ -59,6 +59,32 @@ export const lecturesApi = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
|
me: () => apiRequest<CurrentUserDto>('/users/me'),
|
||||||
|
updateMe: (payload: { displayName?: string | null; avatarUrl?: string | null }) =>
|
||||||
|
apiRequest<CurrentUserDto>('/users/me', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}),
|
||||||
|
myStats: () => apiRequest<UserStatsDto>('/users/me/stats'),
|
||||||
|
async myEnrollments() {
|
||||||
|
const payload = await apiRequest<PagedResult<LectureDto> | LectureDto[] | undefined>(
|
||||||
|
'/users/me/enrollments',
|
||||||
|
)
|
||||||
|
return extractItems(payload)
|
||||||
|
},
|
||||||
|
async myAchievements() {
|
||||||
|
const payload = await apiRequest<
|
||||||
|
PagedResult<UserAchievementDto> | UserAchievementDto[] | AchievementDto[]
|
||||||
|
>('/users/me/achievements')
|
||||||
|
if (Array.isArray(payload)) return payload
|
||||||
|
return payload.items ?? []
|
||||||
|
},
|
||||||
|
async myTransactions() {
|
||||||
|
const payload = await apiRequest<PagedResult<CoinTransactionDto> | CoinTransactionDto[]>(
|
||||||
|
'/users/me/transactions',
|
||||||
|
)
|
||||||
|
return extractItems(payload)
|
||||||
|
},
|
||||||
get: (id: string | number) => apiRequest<UserDto>(`/users/${id}`),
|
get: (id: string | number) => apiRequest<UserDto>(`/users/${id}`),
|
||||||
async list(query: UserQuery = {}) {
|
async list(query: UserQuery = {}) {
|
||||||
const payload = await apiRequest<PagedResult<UserDto> | UserDto[]>('/users', {
|
const payload = await apiRequest<PagedResult<UserDto> | UserDto[]>('/users', {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ function getDefaultActiveRole(roles: UserRole[]): UserRole {
|
|||||||
export function mapApiUser(user: UserAuthDto | UserDto | CurrentUserDto, stats?: UserStatsDto): User {
|
export function mapApiUser(user: UserAuthDto | UserDto | CurrentUserDto, stats?: UserStatsDto): User {
|
||||||
const roles = mapApiRoles(user.roles)
|
const roles = mapApiRoles(user.roles)
|
||||||
return {
|
return {
|
||||||
id: String(user.id),
|
|
||||||
name: user.displayName || user.email || 'Пользователь UniVerse',
|
name: user.displayName || user.email || 'Пользователь UniVerse',
|
||||||
email: user.email || '',
|
email: user.email || '',
|
||||||
roles,
|
roles,
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ export interface LoginMicrosoftRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAuthDto {
|
export interface UserAuthDto {
|
||||||
id: number
|
|
||||||
email: string
|
email: string
|
||||||
displayName?: string | null
|
displayName?: string | null
|
||||||
roles: ApiUserRole[]
|
roles: ApiUserRole[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDto extends UserAuthDto {
|
export interface UserDto extends UserAuthDto {
|
||||||
|
id: number
|
||||||
avatarUrl?: string | null
|
avatarUrl?: string | null
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
xp: number
|
xp: number
|
||||||
|
|||||||
@@ -46,17 +46,17 @@ export const useLecturesStore = defineStore('lectures', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRegisteredForUser(userId: string) {
|
async function fetchRegisteredForCurrentUser() {
|
||||||
try {
|
try {
|
||||||
const enrollments = await usersApi.enrollments(userId)
|
const enrollments = await usersApi.myEnrollments()
|
||||||
const mapped = enrollments.map(mapApiLecture)
|
const mapped = enrollments.map(mapApiLecture)
|
||||||
|
registered.value = mapped.map(lecture => lecture.id)
|
||||||
if (mapped.length) {
|
if (mapped.length) {
|
||||||
mapped.forEach(lecture => {
|
mapped.forEach(lecture => {
|
||||||
const index = lectures.value.findIndex(item => item.id === lecture.id)
|
const index = lectures.value.findIndex(item => item.id === lecture.id)
|
||||||
if (index >= 0) lectures.value[index] = { ...lectures.value[index], ...lecture, registered: true }
|
if (index >= 0) lectures.value[index] = { ...lectures.value[index], ...lecture, registered: true }
|
||||||
else lectures.value.push({ ...lecture, registered: true })
|
else lectures.value.push({ ...lecture, registered: true })
|
||||||
})
|
})
|
||||||
registered.value = mapped.map(lecture => lecture.id)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Some backend builds return an empty 200 for this endpoint; catalog detail still carries isEnrolled.
|
// Some backend builds return an empty 200 for this endpoint; catalog detail still carries isEnrolled.
|
||||||
@@ -108,7 +108,7 @@ export const useLecturesStore = defineStore('lectures', () => {
|
|||||||
registeredLectures,
|
registeredLectures,
|
||||||
fetchLectures,
|
fetchLectures,
|
||||||
fetchLecture,
|
fetchLecture,
|
||||||
fetchRegisteredForUser,
|
fetchRegisteredForCurrentUser,
|
||||||
fetchReviews,
|
fetchReviews,
|
||||||
register,
|
register,
|
||||||
unregister,
|
unregister,
|
||||||
|
|||||||
@@ -12,18 +12,17 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
async function fetchStudentData(userId?: string) {
|
async function fetchStudentData() {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const id = userId ?? auth.user?.id
|
if (!auth.user) return
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const [stats, achievementPayload, transactions] = await Promise.all([
|
const [stats, achievementPayload, transactions] = await Promise.all([
|
||||||
usersApi.stats(id),
|
usersApi.myStats(),
|
||||||
usersApi.achievements(id),
|
usersApi.myAchievements(),
|
||||||
usersApi.transactions(id),
|
usersApi.myTransactions(),
|
||||||
])
|
])
|
||||||
const [achievementCatalog, notificationPayload] = await Promise.all([
|
const [achievementCatalog, notificationPayload] = await Promise.all([
|
||||||
achievementsApi.list(),
|
achievementsApi.list(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export type UserRole = 'student' | 'teacher' | 'admin'
|
export type UserRole = 'student' | 'teacher' | 'admin'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
roles: UserRole[]
|
roles: UserRole[]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const unlocked = computed(() => userStore.achievements.filter(a => a.unlocked))
|
|||||||
const locked = computed(() => userStore.achievements.filter(a => !a.unlocked))
|
const locked = computed(() => userStore.achievements.filter(a => !a.unlocked))
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (auth.user) void userStore.fetchStudentData(auth.user.id)
|
if (auth.user) void userStore.fetchStudentData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ const levelProgressText = computed(() =>
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
lectures.all.length ? Promise.resolve() : lectures.fetchLectures(),
|
lectures.all.length ? Promise.resolve() : lectures.fetchLectures(),
|
||||||
userStore.fetchStudentData(user.value.id),
|
userStore.fetchStudentData(),
|
||||||
])
|
])
|
||||||
await lectures.fetchRegisteredForUser(user.value.id)
|
await lectures.fetchRegisteredForCurrentUser()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const history = computed(() => lecturesStore.all.filter(l => l.status === 'compl
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!lecturesStore.all.length) await lecturesStore.fetchLectures()
|
if (!lecturesStore.all.length) await lecturesStore.fetchLectures()
|
||||||
if (auth.user) await lecturesStore.fetchRegisteredForUser(auth.user.id)
|
if (auth.user) await lecturesStore.fetchRegisteredForCurrentUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
function openCancel(id: string) {
|
function openCancel(id: string) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const interestTags = ref([
|
|||||||
const notificationSettings = ref({ email: true })
|
const notificationSettings = ref({ email: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void userStore.fetchStudentData(user.value.id)
|
void userStore.fetchStudentData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user