feat: добавил интеграционные тесты
This commit is contained in:
@@ -5,31 +5,87 @@ using UniVerse.Application.Interfaces;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Управление определениями достижений системы геймификации.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/achievements")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class AchievementsController : ControllerBase
|
||||
{
|
||||
private readonly IAchievementService _achievements;
|
||||
|
||||
public AchievementsController(IAchievementService achievements) => _achievements = achievements;
|
||||
|
||||
/// <summary>Получить список всех достижений.</summary>
|
||||
/// <remarks>Возвращает определения достижений (без информации о получении конкретным пользователем).
|
||||
/// Для достижений конкретного пользователя используйте GET /api/v1/users/{id}/achievements.</remarks>
|
||||
/// <response code="200">Список достижений.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<AchievementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetAll() => Ok(await _achievements.GetAllAsync());
|
||||
|
||||
/// <summary>Получить достижение по ID.</summary>
|
||||
/// <param name="id">ID достижения.</param>
|
||||
/// <response code="200">Данные достижения.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Достижение не найдено.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(AchievementDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AchievementDto>> Get(int id) => Ok(await _achievements.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Создать новое достижение.</summary>
|
||||
/// <remarks>Только Admin. Достижения автоматически присваиваются студентам при выполнении условий.</remarks>
|
||||
/// <param name="req">Название, описание, иконка, награда в XP/монетах и условие получения.</param>
|
||||
/// <response code="201">Достижение создано.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(AchievementDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<AchievementDto>> Create([FromBody] CreateAchievementRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _achievements.CreateAsync(req));
|
||||
|
||||
/// <summary>Обновить достижение по ID.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="id">ID достижения.</param>
|
||||
/// <param name="req">Обновляемые поля достижения.</param>
|
||||
/// <response code="200">Обновлённые данные достижения.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Достижение не найдено.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(AchievementDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AchievementDto>> Update(int id, [FromBody] UpdateAchievementRequest req) =>
|
||||
Ok(await _achievements.UpdateAsync(id, req));
|
||||
|
||||
/// <summary>Удалить достижение по ID.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Удаление не отзывает достижение у уже получивших его пользователей.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID достижения.</param>
|
||||
/// <response code="204">Достижение удалено.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Достижение не найдено.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id) { await _achievements.DeleteAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _achievements.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,23 +8,36 @@ using System.Security.Claims;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Аутентификация и управление сессией пользователя.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/auth")]
|
||||
[Produces("application/json")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _auth;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
private const string MicrosoftStateCookieName = "msAuthState";
|
||||
private const string MicrosoftStateCookieName = "msAuthState";
|
||||
private const string MicrosoftReturnUrlCookieName = "msAuthReturnUrl";
|
||||
|
||||
public AuthController(IAuthService auth, IConfiguration config)
|
||||
{
|
||||
_auth = auth;
|
||||
_auth = auth;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>Вход через Microsoft Entra ID (SPA/PKCE flow).</summary>
|
||||
/// <remarks>
|
||||
/// Фронтенд самостоятельно обрабатывает редирект к Microsoft и передаёт сюда
|
||||
/// полученный authorization code. В ответ возвращается пара JWT-токенов;
|
||||
/// refresh token устанавливается в HttpOnly cookie.
|
||||
/// </remarks>
|
||||
/// <param name="request">Authorization code и redirect URI из Microsoft OAuth2.</param>
|
||||
/// <response code="200">Успешный вход — возвращает access token и данные пользователя.</response>
|
||||
/// <response code="400">Неверный или просроченный authorization code.</response>
|
||||
[HttpPost("login/microsoft")]
|
||||
[ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<AuthResponse>> LoginMicrosoft([FromBody] LoginMicrosoftRequest request)
|
||||
{
|
||||
var result = await _auth.LoginWithMicrosoftAsync(request.AuthorizationCode, request.RedirectUri);
|
||||
@@ -32,10 +45,19 @@ public class AuthController : ControllerBase
|
||||
return Ok(result.Response);
|
||||
}
|
||||
|
||||
// Server-driven auth flow: frontend just navigates here; backend builds Microsoft authorize URL.
|
||||
// Optional returnUrl is stored in a short-lived cookie and used by callback.
|
||||
/// <summary>Инициация server-driven входа через Microsoft (редирект-flow).</summary>
|
||||
/// <remarks>
|
||||
/// Браузер переходит на этот URL; backend строит Microsoft authorize URL с CSRF state
|
||||
/// и редиректит пользователя на `login.microsoftonline.com`.
|
||||
/// После успешного входа Microsoft редиректит на `GET /api/v1/auth/callback/microsoft`.
|
||||
/// </remarks>
|
||||
/// <param name="returnUrl">URL для редиректа после успешного входа (опционально).</param>
|
||||
/// <response code="302">Редирект на Microsoft authorize endpoint.</response>
|
||||
/// <response code="500">Microsoft authentication не настроен (AzureAd:TenantId/ClientId отсутствуют).</response>
|
||||
[HttpGet("login/microsoft")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult LoginMicrosoftRedirect([FromQuery] string? returnUrl = null)
|
||||
{
|
||||
var tenantId = _config["AzureAd:TenantId"];
|
||||
@@ -51,9 +73,9 @@ public class AuthController : ControllerBase
|
||||
Response.Cookies.Append(MicrosoftStateCookieName, state, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = Request.IsHttps,
|
||||
Secure = Request.IsHttps,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(10)
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(10)
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl) && IsAllowedReturnUrl(returnUrl))
|
||||
@@ -61,9 +83,9 @@ public class AuthController : ControllerBase
|
||||
Response.Cookies.Append(MicrosoftReturnUrlCookieName, returnUrl, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = Request.IsHttps,
|
||||
Secure = Request.IsHttps,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(10)
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(10)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,19 +94,37 @@ public class AuthController : ControllerBase
|
||||
|
||||
var authorizeUrl = QueryHelpers.AddQueryString(authorizeEndpoint, new Dictionary<string, string?>
|
||||
{
|
||||
["client_id"] = clientId,
|
||||
["client_id"] = clientId,
|
||||
["response_type"] = "code",
|
||||
["redirect_uri"] = redirectUri,
|
||||
["redirect_uri"] = redirectUri,
|
||||
["response_mode"] = "query",
|
||||
["scope"] = scope,
|
||||
["state"] = state
|
||||
["scope"] = scope,
|
||||
["state"] = state
|
||||
});
|
||||
|
||||
return Redirect(authorizeUrl);
|
||||
}
|
||||
|
||||
/// <summary>OAuth2 callback — обмен code на токены (server-driven flow).</summary>
|
||||
/// <remarks>
|
||||
/// Microsoft редиректит браузер сюда после успешного входа.
|
||||
/// Backend валидирует CSRF state, обменивает code на токены,
|
||||
/// устанавливает refresh token cookie и редиректит на `returnUrl` с access token в URL-фрагменте.
|
||||
/// </remarks>
|
||||
/// <param name="code">Authorization code от Microsoft.</param>
|
||||
/// <param name="state">CSRF state для верификации.</param>
|
||||
/// <param name="error">Код ошибки от Microsoft (если вход не удался).</param>
|
||||
/// <param name="errorDescription">Описание ошибки от Microsoft.</param>
|
||||
/// <response code="302">Успешный вход — редирект на returnUrl с токеном в URL-фрагменте.</response>
|
||||
/// <response code="200">Если returnUrl не задан — возвращает JSON с токенами (удобно для тестирования).</response>
|
||||
/// <response code="400">Отсутствует authorization code.</response>
|
||||
/// <response code="401">Ошибка от Microsoft или невалидный CSRF state.</response>
|
||||
[HttpGet("callback/microsoft")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> CallbackMicrosoft(
|
||||
[FromQuery] string? code = null,
|
||||
[FromQuery] string? state = null,
|
||||
@@ -129,7 +169,17 @@ public class AuthController : ControllerBase
|
||||
return Ok(result.Response);
|
||||
}
|
||||
|
||||
/// <summary>Dev-only вход без OAuth (только в Development-окружении).</summary>
|
||||
/// <remarks>
|
||||
/// Создаёт или находит пользователя по email без реального OAuth flow.
|
||||
/// Возвращает 404 в Production и Staging.
|
||||
/// </remarks>
|
||||
/// <param name="request">Email, отображаемое имя и роль тестового пользователя.</param>
|
||||
/// <response code="200">Успешный вход.</response>
|
||||
/// <response code="404">Endpoint недоступен вне Development.</response>
|
||||
[HttpPost("login/dev")]
|
||||
[ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AuthResponse>> DevLogin([FromBody] DevLoginRequest request)
|
||||
{
|
||||
if (!HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
|
||||
@@ -139,7 +189,16 @@ public class AuthController : ControllerBase
|
||||
return Ok(result.Response);
|
||||
}
|
||||
|
||||
/// <summary>Обновление access token по refresh token из HttpOnly cookie.</summary>
|
||||
/// <remarks>
|
||||
/// Refresh token читается из HttpOnly cookie `refreshToken` (устанавливается при входе).
|
||||
/// Возвращает новую пару токенов и обновляет cookie.
|
||||
/// </remarks>
|
||||
/// <response code="200">Новая пара токенов.</response>
|
||||
/// <response code="401">Refresh token отсутствует, просрочен или отозван.</response>
|
||||
[HttpPost("refresh")]
|
||||
[ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<AuthResponse>> Refresh()
|
||||
{
|
||||
var refreshToken = Request.Cookies["refreshToken"];
|
||||
@@ -149,8 +208,17 @@ public class AuthController : ControllerBase
|
||||
return Ok(result.Response);
|
||||
}
|
||||
|
||||
/// <summary>Выход из системы — отзыв refresh token.</summary>
|
||||
/// <remarks>
|
||||
/// Инвалидирует текущий refresh token в БД и удаляет cookie.
|
||||
/// После этого вызова access token остаётся валидным до истечения его TTL (30 минут).
|
||||
/// </remarks>
|
||||
/// <response code="204">Выход выполнен успешно.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[Authorize]
|
||||
[HttpPost("logout")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var refreshToken = Request.Cookies["refreshToken"];
|
||||
@@ -160,8 +228,15 @@ public class AuthController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Получение профиля текущего авторизованного пользователя.</summary>
|
||||
/// <response code="200">Данные текущего пользователя.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Пользователь не найден в БД (рассинхронизация токена).</response>
|
||||
[Authorize]
|
||||
[HttpGet("me")]
|
||||
[ProducesResponseType(typeof(UniVerse.Application.DTOs.Users.UserDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Me()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
@@ -175,7 +250,7 @@ public class AuthController : ControllerBase
|
||||
Response.Cookies.Append("refreshToken", token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict,
|
||||
Expires = DateTime.UtcNow.AddDays(30)
|
||||
Expires = DateTime.UtcNow.AddDays(30)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,132 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using UniVerse.Application.DTOs.Common;
|
||||
using UniVerse.Application.DTOs.Courses;
|
||||
using UniVerse.Application.Interfaces;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Управление курсами (дисциплинами) и их тегами.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/courses")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class CoursesController : ControllerBase
|
||||
{
|
||||
private readonly ICourseService _courses;
|
||||
|
||||
public CoursesController(ICourseService courses) => _courses = courses;
|
||||
|
||||
/// <summary>Получить список курсов с фильтрацией и пагинацией.</summary>
|
||||
/// <param name="filter">Фильтры: tagId, search, isSynced; параметры пагинации.</param>
|
||||
/// <response code="200">Список курсов (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResult<CourseDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetAll([FromQuery] CourseFilterRequest filter) =>
|
||||
Ok(await _courses.GetAllAsync(filter));
|
||||
|
||||
/// <summary>Получить курс по ID (включая теги).</summary>
|
||||
/// <param name="id">ID курса.</param>
|
||||
/// <response code="200">Данные курса с тегами.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Курс не найден.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(CourseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CourseDto>> Get(int id) => Ok(await _courses.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Создать новый курс.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="req">Название и описание курса.</param>
|
||||
/// <response code="201">Курс создан.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CourseDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<CourseDto>> Create([FromBody] CreateCourseRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _courses.CreateAsync(req));
|
||||
|
||||
/// <summary>Обновить курс по ID.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="id">ID курса.</param>
|
||||
/// <param name="req">Новое название и/или описание.</param>
|
||||
/// <response code="200">Обновлённые данные курса.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Курс не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(CourseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CourseDto>> Update(int id, [FromBody] UpdateCourseRequest req) =>
|
||||
Ok(await _courses.UpdateAsync(id, req));
|
||||
|
||||
/// <summary>Удалить курс по ID.</summary>
|
||||
/// <remarks>Только Admin. Удаление курса каскадно удаляет связанные лекции.</remarks>
|
||||
/// <param name="id">ID курса.</param>
|
||||
/// <response code="204">Курс удалён.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Курс не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id) { await _courses.DeleteAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _courses.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Привязать тег к курсу.</summary>
|
||||
/// <remarks>Только Admin. Тег должен существовать в системе.</remarks>
|
||||
/// <param name="id">ID курса.</param>
|
||||
/// <param name="tagId">ID тега.</param>
|
||||
/// <response code="204">Тег привязан.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Курс или тег не найден.</response>
|
||||
/// <response code="409">Тег уже привязан к курсу.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost("{id:int}/tags")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> AddTag(int id, [FromBody] int tagId)
|
||||
{ await _courses.AddTagAsync(id, tagId); return NoContent(); }
|
||||
{
|
||||
await _courses.AddTagAsync(id, tagId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Отвязать тег от курса.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="id">ID курса.</param>
|
||||
/// <param name="tagId">ID тега.</param>
|
||||
/// <response code="204">Тег отвязан.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Курс или тег не найден, либо связь не существует.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}/tags/{tagId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> RemoveTag(int id, int tagId)
|
||||
{ await _courses.RemoveTagAsync(id, tagId); return NoContent(); }
|
||||
{
|
||||
await _courses.RemoveTagAsync(id, tagId);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,59 +7,197 @@ using System.Security.Claims;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Каталог лекций — просмотр, управление, запись и отзывы.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/lectures")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class LecturesController : ControllerBase
|
||||
{
|
||||
private readonly ILectureService _lectures;
|
||||
private readonly IReviewService _reviews;
|
||||
public LecturesController(ILectureService lectures, IReviewService reviews)
|
||||
{ _lectures = lectures; _reviews = reviews; }
|
||||
private int CurrentUserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||
private readonly IReviewService _reviews;
|
||||
|
||||
public LecturesController(ILectureService lectures, IReviewService reviews)
|
||||
{
|
||||
_lectures = lectures;
|
||||
_reviews = reviews;
|
||||
}
|
||||
|
||||
private int CurrentUserId => int.Parse(
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||
|
||||
/// <summary>Получить каталог лекций с фильтрацией и пагинацией.</summary>
|
||||
/// <param name="filter">
|
||||
/// Фильтры: dateFrom, dateTo, courseId, teacherId, format (Online/Offline),
|
||||
/// isOpen, tagId, search; параметры пагинации.
|
||||
/// </param>
|
||||
/// <response code="200">Список лекций (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResult<LectureDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetAll([FromQuery] LectureFilterRequest filter) =>
|
||||
Ok(await _lectures.GetAllAsync(filter));
|
||||
|
||||
/// <summary>Получить детальную карточку лекции по ID.</summary>
|
||||
/// <remarks>
|
||||
/// Включает флаг `isEnrolled` — записан ли текущий пользователь на эту лекцию.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <response code="200">Детальные данные лекции.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(LectureDetailDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Get(int id) =>
|
||||
Ok(await _lectures.GetByIdAsync(id, CurrentUserId));
|
||||
|
||||
/// <summary>Создать новую лекцию.</summary>
|
||||
/// <remarks>Только Admin. Курс задаётся при создании и не может быть изменён.</remarks>
|
||||
/// <param name="req">Данные лекции: курс, преподаватель, локация, время, формат, вместимость.</param>
|
||||
/// <response code="201">Лекция создана.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(LectureDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<LectureDto>> Create([FromBody] CreateLectureRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _lectures.CreateAsync(req));
|
||||
|
||||
/// <summary>Обновить лекцию по ID.</summary>
|
||||
/// <remarks>Admin или Teacher. CourseId изменить нельзя.</remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <param name="req">Обновляемые поля: преподаватель, локация, время, формат, описание.</param>
|
||||
/// <response code="200">Обновлённые данные лекции.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin или Teacher.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
[Authorize(Roles = "Admin,Teacher")]
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(LectureDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LectureDto>> Update(int id, [FromBody] UpdateLectureRequest req) =>
|
||||
Ok(await _lectures.UpdateAsync(id, req));
|
||||
|
||||
/// <summary>Удалить лекцию по ID.</summary>
|
||||
/// <remarks>Только Admin. Каскадно удаляет записи и отзывы.</remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <response code="204">Лекция удалена.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id) { await _lectures.DeleteAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _lectures.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Записаться на лекцию.</summary>
|
||||
/// <remarks>
|
||||
/// Только Student. Проверяет наличие свободных мест и отсутствие повторной записи.
|
||||
/// После посещения начисляются монеты через gamification.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <response code="204">Запись выполнена.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Student.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
/// <response code="409">Студент уже записан или мест нет.</response>
|
||||
[Authorize(Roles = "Student")]
|
||||
[HttpPost("{id:int}/enroll")]
|
||||
public async Task<IActionResult> Enroll(int id) { await _lectures.EnrollAsync(id, CurrentUserId); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Enroll(int id)
|
||||
{
|
||||
await _lectures.EnrollAsync(id, CurrentUserId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Отменить запись на лекцию.</summary>
|
||||
/// <remarks>Только Student. Отменить можно только свою запись.</remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <response code="204">Запись отменена.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Student.</response>
|
||||
/// <response code="404">Лекция или запись не найдена.</response>
|
||||
[Authorize(Roles = "Student")]
|
||||
[HttpDelete("{id:int}/enroll")]
|
||||
public async Task<IActionResult> Unenroll(int id) { await _lectures.UnenrollAsync(id, CurrentUserId); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Unenroll(int id)
|
||||
{
|
||||
await _lectures.UnenrollAsync(id, CurrentUserId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Отметить посещение студента на лекции.</summary>
|
||||
/// <remarks>
|
||||
/// Admin или Teacher. При отметке `attended=true` начисляются монеты за посещение
|
||||
/// через gamification service.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <param name="userId">ID студента.</param>
|
||||
/// <param name="attended">true — посетил, false — не посетил.</param>
|
||||
/// <response code="204">Посещение отмечено.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin или Teacher.</response>
|
||||
/// <response code="404">Лекция или запись студента не найдена.</response>
|
||||
[Authorize(Roles = "Admin,Teacher")]
|
||||
[HttpPatch("{id:int}/attendance/{userId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Attendance(int id, int userId, [FromBody] bool attended)
|
||||
{ await _lectures.MarkAttendanceAsync(id, userId, attended); return NoContent(); }
|
||||
{
|
||||
await _lectures.MarkAttendanceAsync(id, userId, attended);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Получить список записавшихся студентов на лекцию.</summary>
|
||||
/// <remarks>Только Admin или Teacher. Включает флаг посещения (`attended`).</remarks>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">Список записей (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin или Teacher.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
[Authorize(Roles = "Admin,Teacher")]
|
||||
[HttpGet("{id:int}/enrollments")]
|
||||
[ProducesResponseType(typeof(PagedResult<EnrollmentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Enrollments(int id, [FromQuery] PaginationRequest pagination) =>
|
||||
Ok(await _lectures.GetEnrollmentsAsync(id, pagination));
|
||||
|
||||
/// <summary>Получить отзывы к лекции.</summary>
|
||||
/// <param name="id">ID лекции.</param>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">Список отзывов (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
[HttpGet("{id:int}/reviews")]
|
||||
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Reviews.ReviewDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) =>
|
||||
Ok(await _reviews.GetByLectureAsync(id, pagination));
|
||||
|
||||
}
|
||||
|
||||
@@ -5,31 +5,85 @@ using UniVerse.Application.Interfaces;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Управление локациями проведения лекций (аудитории, онлайн-площадки).</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/locations")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class LocationsController : ControllerBase
|
||||
{
|
||||
private readonly ILocationService _locations;
|
||||
|
||||
public LocationsController(ILocationService locations) => _locations = locations;
|
||||
|
||||
/// <summary>Получить список всех локаций.</summary>
|
||||
/// <response code="200">Список локаций.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<LocationDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetAll() => Ok(await _locations.GetAllAsync());
|
||||
|
||||
/// <summary>Получить локацию по ID.</summary>
|
||||
/// <param name="id">ID локации.</param>
|
||||
/// <response code="200">Данные локации.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Локация не найдена.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(LocationDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LocationDto>> Get(int id) => Ok(await _locations.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Создать новую локацию.</summary>
|
||||
/// <remarks>Только Admin. Локации также создаются автоматически при синхронизации с Modeus.</remarks>
|
||||
/// <param name="req">Название, корпус, аудитория и/или адрес.</param>
|
||||
/// <response code="201">Локация создана.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(LocationDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<LocationDto>> Create([FromBody] CreateLocationRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _locations.CreateAsync(req));
|
||||
|
||||
/// <summary>Обновить локацию по ID.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="id">ID локации.</param>
|
||||
/// <param name="req">Обновляемые поля: название, корпус, аудитория, адрес.</param>
|
||||
/// <response code="200">Обновлённые данные локации.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Локация не найдена.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(LocationDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LocationDto>> Update(int id, [FromBody] UpdateLocationRequest req) =>
|
||||
Ok(await _locations.UpdateAsync(id, req));
|
||||
|
||||
/// <summary>Удалить локацию по ID.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. При удалении локации у связанных лекций поле `locationId` становится null.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID локации.</param>
|
||||
/// <response code="204">Локация удалена.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Локация не найдена.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id) { await _locations.DeleteAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _locations.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,40 +7,123 @@ using System.Security.Claims;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Отзывы студентов на лекции с LLM-анализом и модерацией.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/reviews")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class ReviewsController : ControllerBase
|
||||
{
|
||||
private readonly IReviewService _reviews;
|
||||
public ReviewsController(IReviewService reviews) => _reviews = reviews;
|
||||
private int CurrentUserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||
|
||||
public ReviewsController(IReviewService reviews) => _reviews = reviews;
|
||||
|
||||
private int CurrentUserId => int.Parse(
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||
|
||||
/// <summary>Создать отзыв к лекции.</summary>
|
||||
/// <remarks>
|
||||
/// Только Student. После создания отзыв помещается в очередь LLM-анализа
|
||||
/// (статус `Pending`). LLM оценивает содержательность и начисляет монеты
|
||||
/// скрытно от пользователя.
|
||||
/// </remarks>
|
||||
/// <param name="req">ID лекции, оценка (Like/Neutral/Dislike) и текст отзыва.</param>
|
||||
/// <response code="201">Отзыв создан и поставлен в очередь на LLM-анализ.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Student.</response>
|
||||
/// <response code="404">Лекция не найдена.</response>
|
||||
/// <response code="409">Студент уже оставил отзыв к этой лекции.</response>
|
||||
[Authorize(Roles = "Student")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ReviewDto>> Create([FromBody] CreateReviewRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req));
|
||||
|
||||
/// <summary>Получить отзыв по ID.</summary>
|
||||
/// <param name="id">ID отзыва.</param>
|
||||
/// <response code="200">Данные отзыва (включая LLM-статус и сентимент).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Отзыв не найден.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ReviewDto>> Get(int id) => Ok(await _reviews.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Обновить отзыв.</summary>
|
||||
/// <remarks>
|
||||
/// Разрешено любому авторизованному пользователю, но сервис проверяет владельца.
|
||||
/// Изменение текста сбрасывает LLM-статус в `Pending` (повторный анализ).
|
||||
/// </remarks>
|
||||
/// <param name="id">ID отзыва.</param>
|
||||
/// <param name="req">Новая оценка и/или текст.</param>
|
||||
/// <response code="200">Обновлённые данные отзыва.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Отзыв принадлежит другому пользователю.</response>
|
||||
/// <response code="404">Отзыв не найден.</response>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ReviewDto>> Update(int id, [FromBody] UpdateReviewRequest req) =>
|
||||
Ok(await _reviews.UpdateAsync(id, CurrentUserId, req));
|
||||
|
||||
/// <summary>Удалить отзыв.</summary>
|
||||
/// <remarks>Владелец может удалить свой отзыв. Admin может удалить любой.</remarks>
|
||||
/// <param name="id">ID отзыва.</param>
|
||||
/// <response code="204">Отзыв удалён.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Нет прав на удаление (не владелец и не Admin).</response>
|
||||
/// <response code="404">Отзыв не найден.</response>
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _reviews.DeleteAsync(id, CurrentUserId, User.IsInRole("Admin"));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Получить список отзывов, ожидающих LLM-анализа.</summary>
|
||||
/// <remarks>Только Admin. Используется для мониторинга очереди обработки.</remarks>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">Список отзывов со статусом Pending (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpGet("pending")]
|
||||
[ProducesResponseType(typeof(PagedResult<ReviewDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> Pending([FromQuery] PaginationRequest pagination) =>
|
||||
Ok(await _reviews.GetPendingAsync(pagination));
|
||||
|
||||
/// <summary>Запустить повторный LLM-анализ отзыва.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Сбрасывает статус отзыва на `Pending` и ставит его
|
||||
/// в очередь на повторную обработку фоновым сервисом.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID отзыва.</param>
|
||||
/// <response code="204">Повторный анализ запланирован.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Отзыв не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost("{id:int}/reanalyze")]
|
||||
public async Task<IActionResult> Reanalyze(int id) { await _reviews.ReanalyzeAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Reanalyze(int id)
|
||||
{
|
||||
await _reviews.ReanalyzeAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,74 @@ using UniVerse.Application.Interfaces;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Синхронизация данных из внешней системы расписания Modeus (только Admin).</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/sync")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[Produces("application/json")]
|
||||
public class SyncController : ControllerBase
|
||||
{
|
||||
private readonly IScheduleSyncService _sync;
|
||||
|
||||
public SyncController(IScheduleSyncService sync) => _sync = sync;
|
||||
|
||||
/// <summary>Запустить синхронизацию расписания лекций из Modeus.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Выполняет upsert лекций и связанных курсов на основе данных
|
||||
/// из внешнего API `schedule.rdcenter.ru`. Поддерживает фильтрацию по специальности,
|
||||
/// периоду и типу занятий.
|
||||
/// </remarks>
|
||||
/// <param name="req">Параметры синхронизации: specialtyCode, timeMin/timeMax, typeId.</param>
|
||||
/// <response code="200">Результат синхронизации: кол-во созданных, обновлённых и пропущенных записей.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[HttpPost("schedule")]
|
||||
[ProducesResponseType(typeof(SyncResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<SyncResultDto>> SyncSchedule([FromBody] SyncScheduleRequest req) =>
|
||||
Ok(await _sync.SyncScheduleAsync(req));
|
||||
|
||||
/// <summary>Получить статус последней синхронизации.</summary>
|
||||
/// <remarks>Только Admin. Возвращает время и результат последней успешной синхронизации.</remarks>
|
||||
/// <response code="200">Статус синхронизации.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[HttpGet("status")]
|
||||
[ProducesResponseType(typeof(SyncStatusDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<SyncStatusDto>> Status() =>
|
||||
Ok(await _sync.GetLastSyncStatusAsync());
|
||||
|
||||
/// <summary>Синхронизировать аудитории (локации) из Modeus.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Импортирует аудитории из `schedule.rdcenter.ru` и создаёт
|
||||
/// соответствующие записи в таблице locations.
|
||||
/// </remarks>
|
||||
/// <response code="200">Результат синхронизации аудиторий.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[HttpPost("rooms")]
|
||||
[ProducesResponseType(typeof(SyncResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<SyncResultDto>> SyncRooms() =>
|
||||
Ok(await _sync.SyncRoomsAsync());
|
||||
|
||||
/// <summary>Поиск преподавателей в Modeus по ФИО.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Ищет преподавателей через внешнее API и возвращает список
|
||||
/// для ручного импорта. Найденные преподаватели не создаются автоматически.
|
||||
/// </remarks>
|
||||
/// <param name="fullname">Полное имя или часть имени преподавателя для поиска.</param>
|
||||
/// <response code="200">Список найденных преподавателей.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[HttpPost("employees")]
|
||||
[ProducesResponseType(typeof(List<EmployeeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> SearchEmployees([FromQuery] string fullname) =>
|
||||
Ok(await _sync.SearchEmployeesAsync(fullname));
|
||||
|
||||
}
|
||||
|
||||
@@ -6,35 +6,101 @@ using UniVerse.Domain.Enums;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Управление тегами для категоризации курсов (институты, факультеты, темы и др.).</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/tags")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class TagsController : ControllerBase
|
||||
{
|
||||
private readonly ITagService _tags;
|
||||
|
||||
public TagsController(ITagService tags) => _tags = tags;
|
||||
|
||||
/// <summary>Получить список тегов с опциональной фильтрацией по типу и родителю.</summary>
|
||||
/// <param name="type">Тип тега: Institute, Faculty, Subject, Organization, Topic, Other.</param>
|
||||
/// <param name="parentId">ID родительского тега (фильтрация дочерних).</param>
|
||||
/// <response code="200">Список тегов.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<TagDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetAll([FromQuery] TagType? type, [FromQuery] int? parentId) =>
|
||||
Ok(await _tags.GetAllAsync(type, parentId));
|
||||
|
||||
/// <summary>Получить тег по ID.</summary>
|
||||
/// <param name="id">ID тега.</param>
|
||||
/// <response code="200">Данные тега.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Тег не найден.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(TagDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TagDto>> Get(int id) => Ok(await _tags.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Получить иерархическое дерево всех тегов.</summary>
|
||||
/// <remarks>
|
||||
/// Возвращает корневые теги с вложенными дочерними тегами.
|
||||
/// Полезно для построения фильтрующих UI-компонентов.
|
||||
/// </remarks>
|
||||
/// <response code="200">Иерархический список тегов.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet("tree")]
|
||||
[ProducesResponseType(typeof(List<TagTreeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> GetTree() => Ok(await _tags.GetTreeAsync());
|
||||
|
||||
/// <summary>Создать новый тег.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="req">Название, тип и опциональный родительский тег.</param>
|
||||
/// <response code="201">Тег создан.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(TagDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<TagDto>> Create([FromBody] CreateTagRequest req) =>
|
||||
CreatedAtAction(nameof(Get), new { id = 0 }, await _tags.CreateAsync(req));
|
||||
|
||||
/// <summary>Обновить тег по ID.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="id">ID тега.</param>
|
||||
/// <param name="req">Новое название, тип и/или родительский тег.</param>
|
||||
/// <response code="200">Обновлённые данные тега.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Тег не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(TagDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TagDto>> Update(int id, [FromBody] UpdateTagRequest req) =>
|
||||
Ok(await _tags.UpdateAsync(id, req));
|
||||
|
||||
/// <summary>Удалить тег по ID.</summary>
|
||||
/// <remarks>
|
||||
/// Только Admin. Удаление тега каскадно удаляет привязки к курсам (`course_tags`).
|
||||
/// Дочерние теги остаются, но их `parentId` становится null.
|
||||
/// </remarks>
|
||||
/// <param name="id">ID тега.</param>
|
||||
/// <response code="204">Тег удалён.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Тег не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id) { await _tags.DeleteAsync(id); return NoContent(); }
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _tags.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,34 +8,76 @@ using System.Security.Claims;
|
||||
|
||||
namespace UniVerse.Api.Controllers;
|
||||
|
||||
/// <summary>Управление пользователями, профилями и геймификацией.</summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/users")]
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserService _users;
|
||||
private readonly IReviewService _reviews;
|
||||
private readonly IUserService _users;
|
||||
private readonly IReviewService _reviews;
|
||||
private readonly IGamificationService _gamification;
|
||||
|
||||
public UsersController(IUserService users, IReviewService reviews, IGamificationService gamification)
|
||||
{
|
||||
_users = users; _reviews = reviews; _gamification = gamification;
|
||||
}
|
||||
|
||||
private int CurrentUserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? "0");
|
||||
|
||||
/// <summary>Получить профиль пользователя по ID.</summary>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <response code="200">Данные пользователя.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Пользователь не найден.</response>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<UserDto>> Get(int id) => Ok(await _users.GetByIdAsync(id));
|
||||
|
||||
/// <summary>Обновить профиль пользователя (displayName, avatarUrl).</summary>
|
||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="req">Обновляемые поля профиля.</param>
|
||||
/// <response code="200">Обновлённые данные пользователя.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Нет прав — только владелец профиля или Admin.</response>
|
||||
/// <response code="404">Пользователь не найден.</response>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<UserDto>> Update(int id, [FromBody] UpdateUserRequest req)
|
||||
{
|
||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
||||
return Ok(await _users.UpdateProfileAsync(id, req));
|
||||
}
|
||||
|
||||
/// <summary>Получить статистику пользователя (XP, монеты, уровень, посещения).</summary>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <response code="200">Статистика пользователя.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="404">Пользователь не найден.</response>
|
||||
[HttpGet("{id:int}/stats")]
|
||||
[ProducesResponseType(typeof(UserStatsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<UserStatsDto>> Stats(int id) => Ok(await _users.GetStatsAsync(id));
|
||||
|
||||
/// <summary>Получить список записей пользователя на лекции.</summary>
|
||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">Список записей (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Нет прав — только владелец или Admin.</response>
|
||||
[HttpGet("{id:int}/enrollments")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> Enrollments(int id, [FromQuery] PaginationRequest pagination)
|
||||
{
|
||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
||||
@@ -43,36 +85,92 @@ public class UsersController : ControllerBase
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>Получить отзывы пользователя.</summary>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">Список отзывов (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet("{id:int}/reviews")]
|
||||
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Reviews.ReviewDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) =>
|
||||
Ok(await _reviews.GetByUserAsync(id, pagination));
|
||||
|
||||
/// <summary>Получить достижения пользователя.</summary>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <response code="200">Список полученных достижений.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
[HttpGet("{id:int}/achievements")]
|
||||
[ProducesResponseType(typeof(List<UniVerse.Application.DTOs.Achievements.UserAchievementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult> Achievements(int id) =>
|
||||
Ok(await _gamification.GetUserAchievementsAsync(id));
|
||||
|
||||
/// <summary>Получить историю транзакций монет пользователя.</summary>
|
||||
/// <remarks>Разрешено только самому пользователю или Admin.</remarks>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="pagination">Параметры пагинации.</param>
|
||||
/// <response code="200">История транзакций (пагинированная).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Нет прав — только владелец или Admin.</response>
|
||||
[HttpGet("{id:int}/transactions")]
|
||||
[ProducesResponseType(typeof(PagedResult<UniVerse.Application.DTOs.Gamification.CoinTransactionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> Transactions(int id, [FromQuery] PaginationRequest pagination)
|
||||
{
|
||||
if (CurrentUserId != id && !User.IsInRole("Admin")) return Forbid();
|
||||
return Ok(await _gamification.GetTransactionsAsync(id, pagination));
|
||||
}
|
||||
|
||||
/// <summary>Получить список всех пользователей с фильтрацией и пагинацией.</summary>
|
||||
/// <remarks>Только Admin.</remarks>
|
||||
/// <param name="filter">Параметры фильтрации (поиск, роль, активность) и пагинации.</param>
|
||||
/// <response code="200">Список пользователей (пагинированный).</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResult<UserDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> GetAll([FromQuery] UserFilterRequest filter) =>
|
||||
Ok(await _users.GetAllAsync(filter));
|
||||
|
||||
/// <summary>Изменить роль пользователя.</summary>
|
||||
/// <remarks>Только Admin. Доступные роли: Student, Teacher, Admin.</remarks>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="role">Новая роль.</param>
|
||||
/// <response code="204">Роль успешно изменена.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Пользователь не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPatch("{id:int}/role")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetRole(int id, [FromBody] UserRole role)
|
||||
{
|
||||
await _users.SetRoleAsync(id, role);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Активировать или деактивировать аккаунт пользователя.</summary>
|
||||
/// <remarks>Только Admin. Деактивированный пользователь не может войти в систему.</remarks>
|
||||
/// <param name="id">ID пользователя.</param>
|
||||
/// <param name="isActive">true — активировать, false — деактивировать.</param>
|
||||
/// <response code="204">Статус успешно изменён.</response>
|
||||
/// <response code="401">Требуется аутентификация.</response>
|
||||
/// <response code="403">Требуется роль Admin.</response>
|
||||
/// <response code="404">Пользователь не найден.</response>
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPatch("{id:int}/active")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetActive(int id, [FromBody] bool isActive)
|
||||
{
|
||||
await _users.SetActiveAsync(id, isActive);
|
||||
|
||||
Reference in New Issue
Block a user