fix: скрыл отзывы от студентов
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m17s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 29s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 9s

This commit is contained in:
2026-05-13 19:16:48 +03:00
parent 7761238719
commit f6aaf0b923
7 changed files with 61 additions and 7 deletions
@@ -47,10 +47,13 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
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/{id}/stats [AnyAuth]", "GET", "api/v1/users/1/stats", "Student");
yield return E("users/{id}/enrollments [AnyAuth]", "GET", "api/v1/users/1/enrollments", "Student"); yield return E("users/{id}/enrollments [AnyAuth]", "GET", "api/v1/users/1/enrollments", "Student");
yield return E("users/{id}/reviews [AnyAuth]", "GET", "api/v1/users/1/reviews","Student");
yield return E("users/{id}/achievements [AnyAuth]","GET", "api/v1/users/1/achievements","Student"); yield return E("users/{id}/achievements [AnyAuth]","GET", "api/v1/users/1/achievements","Student");
yield return E("users/{id}/transactions [AnyAuth/self]","GET","api/v1/users/1/transactions","Student"); yield return E("users/{id}/transactions [AnyAuth/self]","GET","api/v1/users/1/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}/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"],
@@ -75,7 +78,6 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
// ── Lectures — any auth ─────────────────────────────────────────────── // ── Lectures — any auth ───────────────────────────────────────────────
yield return E("lectures GET [AnyAuth]", "GET", "api/v1/lectures", "Student"); yield return E("lectures GET [AnyAuth]", "GET", "api/v1/lectures", "Student");
yield return E("lectures/{id} GET [AnyAuth]", "GET", "api/v1/lectures/1", "Student"); yield return E("lectures/{id} GET [AnyAuth]", "GET", "api/v1/lectures/1", "Student");
yield return E("lectures/{id}/reviews GET [AnyAuth]","GET", "api/v1/lectures/1/reviews","Student");
// ── Lectures — Admin only ───────────────────────────────────────────── // ── Lectures — Admin only ─────────────────────────────────────────────
yield return E("lectures POST [Admin]", "POST", "api/v1/lectures", "Admin", forbidden: ["Student", "Teacher"], yield return E("lectures POST [Admin]", "POST", "api/v1/lectures", "Admin", forbidden: ["Student", "Teacher"],
@@ -93,17 +95,22 @@ public class EndpointAuthorizationTests : IClassFixture<ApiWebApplicationFactory
body: "true"); body: "true");
yield return E("lectures/{id}/enrollments GET [Admin]", "GET","api/v1/lectures/1/enrollments","Admin", forbidden: ["Student"]); yield return E("lectures/{id}/enrollments GET [Admin]", "GET","api/v1/lectures/1/enrollments","Admin", forbidden: ["Student"]);
yield return E("lectures/{id}/enrollments GET [Teacher]","GET","api/v1/lectures/1/enrollments","Teacher",forbidden: ["Student"]); yield return E("lectures/{id}/enrollments GET [Teacher]","GET","api/v1/lectures/1/enrollments","Teacher",forbidden: ["Student"]);
yield return E("lectures/{id}/reviews GET [Admin]", "GET","api/v1/lectures/1/reviews","Admin", forbidden: ["Student"]);
yield return E("lectures/{id}/reviews GET [Teacher]", "GET","api/v1/lectures/1/reviews","Teacher",forbidden: ["Student"]);
// ── Lectures — Student only ─────────────────────────────────────────── // ── Lectures — Student only ───────────────────────────────────────────
yield return E("lectures/{id}/enroll POST [Student]", "POST", "api/v1/lectures/1/enroll", "Student", forbidden: ["Admin", "Teacher"]); yield return E("lectures/{id}/enroll POST [Student]", "POST", "api/v1/lectures/1/enroll", "Student", forbidden: ["Admin", "Teacher"]);
yield return E("lectures/{id}/enroll DELETE [Student]", "DELETE","api/v1/lectures/1/enroll", "Student", forbidden: ["Admin", "Teacher"]); yield return E("lectures/{id}/enroll DELETE [Student]", "DELETE","api/v1/lectures/1/enroll", "Student", forbidden: ["Admin", "Teacher"]);
// ── Reviews — any auth ──────────────────────────────────────────────── // ── Reviews — any auth ────────────────────────────────────────────────
yield return E("reviews/{id} GET [AnyAuth]", "GET", "api/v1/reviews/1", "Student");
yield return E("reviews/{id} PUT [AnyAuth]", "PUT", "api/v1/reviews/1", "Student", yield return E("reviews/{id} PUT [AnyAuth]", "PUT", "api/v1/reviews/1", "Student",
body: """{"rating":"Like","text":"Updated"}"""); body: """{"rating":"Like","text":"Updated"}""");
yield return E("reviews/{id} DELETE [AnyAuth]", "DELETE", "api/v1/reviews/1", "Student"); yield return E("reviews/{id} DELETE [AnyAuth]", "DELETE", "api/v1/reviews/1", "Student");
// ── Reviews — Admin OR Teacher ───────────────────────────────────────
yield return E("reviews/{id} GET [Admin]", "GET", "api/v1/reviews/1", "Admin", forbidden: ["Student"]);
yield return E("reviews/{id} GET [Teacher]", "GET", "api/v1/reviews/1", "Teacher", forbidden: ["Student"]);
// ── Reviews — Student only ──────────────────────────────────────────── // ── Reviews — Student only ────────────────────────────────────────────
yield return E("reviews POST [Student]", "POST", "api/v1/reviews", "Student", forbidden: ["Admin", "Teacher"], yield return E("reviews POST [Student]", "POST", "api/v1/reviews", "Student", forbidden: ["Admin", "Teacher"],
body: """{"lectureId":1,"rating":"Like","text":"Great lecture!"}"""); body: """{"lectureId":1,"rating":"Like","text":"Great lecture!"}""");
@@ -189,14 +189,18 @@ public class LecturesController : ControllerBase
Ok(await _lectures.GetEnrollmentsAsync(id, pagination)); Ok(await _lectures.GetEnrollmentsAsync(id, pagination));
/// <summary>Получить отзывы к лекции.</summary> /// <summary>Получить отзывы к лекции.</summary>
/// <remarks>Только Admin или Teacher.</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="404">Лекция не найдена.</response> /// <response code="404">Лекция не найдена.</response>
[Authorize(Roles = "Admin,Teacher")]
[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)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) => public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) =>
Ok(await _reviews.GetByLectureAsync(id, pagination)); Ok(await _reviews.GetByLectureAsync(id, pagination));
@@ -44,13 +44,17 @@ public class ReviewsController : ControllerBase
CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req)); CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req));
/// <summary>Получить отзыв по ID.</summary> /// <summary>Получить отзыв по ID.</summary>
/// <remarks>Только Admin или Teacher.</remarks>
/// <param name="id">ID отзыва.</param> /// <param name="id">ID отзыва.</param>
/// <response code="200">Данные отзыва (включая LLM-статус и сентимент).</response> /// <response code="200">Данные отзыва (включая LLM-статус и сентимент).</response>
/// <response code="401">Требуется аутентификация.</response> /// <response code="401">Требуется аутентификация.</response>
/// <response code="403">Требуется роль Admin или Teacher.</response>
/// <response code="404">Отзыв не найден.</response> /// <response code="404">Отзыв не найден.</response>
[Authorize(Roles = "Admin,Teacher")]
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
[ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ReviewDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ReviewDto>> Get(int id) => Ok(await _reviews.GetByIdAsync(id)); public async Task<ActionResult<ReviewDto>> Get(int id) => Ok(await _reviews.GetByIdAsync(id));
@@ -86,13 +86,17 @@ public class UsersController : ControllerBase
} }
/// <summary>Получить отзывы пользователя.</summary> /// <summary>Получить отзывы пользователя.</summary>
/// <remarks>Только Admin или Teacher.</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>
[Authorize(Roles = "Admin,Teacher")]
[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)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) => public async Task<ActionResult> Reviews(int id, [FromQuery] PaginationRequest pagination) =>
Ok(await _reviews.GetByUserAsync(id, pagination)); Ok(await _reviews.GetByUserAsync(id, pagination));
+33 -3
View File
@@ -1827,7 +1827,7 @@
"Lectures" "Lectures"
], ],
"summary": "Получить отзывы к лекции.", "summary": "Получить отзывы к лекции.",
"description": "**Required:** any authenticated user", "description": "Только Admin или Teacher.\n\n**Required roles:** Admin, Teacher",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@@ -1877,6 +1877,16 @@
} }
} }
}, },
"403": {
"description": "Требуется роль Admin или Teacher.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": { "404": {
"description": "Лекция не найдена.", "description": "Лекция не найдена.",
"content": { "content": {
@@ -2501,7 +2511,7 @@
"Reviews" "Reviews"
], ],
"summary": "Получить отзыв по ID.", "summary": "Получить отзыв по ID.",
"description": "**Required:** any authenticated user", "description": "Только Admin или Teacher.\n\n**Required roles:** Admin, Teacher",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@@ -2535,6 +2545,16 @@
} }
} }
}, },
"403": {
"description": "Требуется роль Admin или Teacher.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": { "404": {
"description": "Отзыв не найден.", "description": "Отзыв не найден.",
"content": { "content": {
@@ -3679,7 +3699,7 @@
"Users" "Users"
], ],
"summary": "Получить отзывы пользователя.", "summary": "Получить отзывы пользователя.",
"description": "**Required:** any authenticated user", "description": "Только Admin или Teacher.\n\n**Required roles:** Admin, Teacher",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@@ -3728,6 +3748,16 @@
} }
} }
} }
},
"403": {
"description": "Требуется роль Admin или Teacher.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
} }
}, },
"security": [ "security": [
+5 -1
View File
@@ -6,6 +6,7 @@ import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{ const props = defineProps<{
lecture: Lecture lecture: Lecture
registered?: boolean registered?: boolean
showRating?: boolean
}>() }>()
const emit = defineEmits<{ register: [id: string] }>() const emit = defineEmits<{ register: [id: string] }>()
const router = useRouter() const router = useRouter()
@@ -78,7 +79,7 @@ function goDetail() {
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="rating"> <div v-if="showRating !== false" class="rating">
<span class="stars">{{ starsHtml(lecture.rating) }}</span> <span class="stars">{{ starsHtml(lecture.rating) }}</span>
<span class="rating-value">{{ lecture.rating }}</span> <span class="rating-value">{{ lecture.rating }}</span>
<span class="text-secondary text-sm">({{ lecture.reviewCount }})</span> <span class="text-secondary text-sm">({{ lecture.reviewCount }})</span>
@@ -178,6 +179,9 @@ function goDetail() {
justify-content: space-between; justify-content: space-between;
margin-top: 4px; margin-top: 4px;
} }
.card-footer button {
margin-left: auto;
}
.rating { .rating {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -205,6 +205,7 @@ async function registerLecture(id: string) {
:key="l.id" :key="l.id"
:lecture="l" :lecture="l"
:registered="lecturesStore.registeredIds.includes(l.id)" :registered="lecturesStore.registeredIds.includes(l.id)"
:show-rating="false"
@register="registerLecture" @register="registerLecture"
/> />
</div> </div>