feat: добавил поддержку подписки на календарь и экспорт расписания лекций в формате .ics
Backend CI / build-and-test (push) Successful in 57s
Frontend CI / build-and-check (push) Failing after 26s
🚀 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 2m33s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 33s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 8s

This commit is contained in:
2026-06-02 21:26:48 +03:00
parent 7050851bd4
commit 136bcce7db
16 changed files with 639 additions and 8 deletions
@@ -5,6 +5,7 @@ using UniVerse.Application.DTOs.Users;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Enums;
using System.Security.Claims;
using System.Text;
namespace UniVerse.Api.Controllers;
@@ -83,6 +84,52 @@ public class UsersController : ControllerBase
public async Task<ActionResult> MyEnrollments([FromQuery] PaginationRequest pagination) =>
Ok(await _users.GetEnrollmentsAsync(CurrentUserId, pagination));
[HttpGet("me/enrollments/calendar-subscription")]
[ProducesResponseType(typeof(CalendarSubscriptionDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<CalendarSubscriptionDto>> CalendarSubscription()
{
var token = await _users.GetCalendarSubscriptionTokenAsync(CurrentUserId);
var feedUrl = Url.Action(
nameof(CalendarEnrollmentsIcs),
null,
new { token },
Request.Scheme)
?? $"{Request.Scheme}://{Request.Host}/api/v1/users/calendar/enrollments/{token}.ics";
return Ok(new CalendarSubscriptionDto(feedUrl));
}
[AllowAnonymous]
[HttpGet("calendar/enrollments/{token}.ics")]
[Produces("text/calendar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<FileContentResult> CalendarEnrollmentsIcs(string token)
{
var ics = await _users.GetEnrollmentsIcsBySubscriptionTokenAsync(token);
return File(Encoding.UTF8.GetBytes(ics), "text/calendar; charset=utf-8", "my-lectures.ics");
}
[HttpGet("me/enrollments.ics")]
[Produces("text/calendar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<FileContentResult> MyEnrollmentsIcs()
{
var ics = await _users.GetMyEnrollmentsIcsAsync(CurrentUserId);
return File(Encoding.UTF8.GetBytes(ics), "text/calendar; charset=utf-8", "my-lectures.ics");
}
[HttpGet("me/enrollments/{lectureId:int}.ics")]
[Produces("text/calendar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<FileContentResult> EnrollmentIcs(int lectureId)
{
var ics = await _users.GetEnrollmentIcsAsync(CurrentUserId, lectureId);
return File(Encoding.UTF8.GetBytes(ics), "text/calendar; charset=utf-8", $"lecture-{lectureId}.ics");
}
/// <summary>Получить отзывы текущего пользователя.</summary>
/// <param name="pagination">Параметры пагинации.</param>
/// <response code="200">Список отзывов (пагинированный).</response>
+150
View File
@@ -3789,6 +3789,146 @@
]
}
},
"/api/v1/users/me/enrollments/calendar-subscription": {
"get": {
"tags": [
"Users"
],
"description": "**Required:** any authenticated user",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CalendarSubscriptionDto"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
},
"security": [
{
"Bearer": [ ]
}
]
}
},
"/api/v1/users/calendar/enrollments/{token}.ics": {
"get": {
"tags": [
"Users"
],
"parameters": [
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"403": {
"description": "Forbidden",
"content": {
"text/calendar": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/v1/users/me/enrollments.ics": {
"get": {
"tags": [
"Users"
],
"description": "**Required:** any authenticated user",
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized — JWT token missing or invalid"
}
},
"security": [
{
"Bearer": [ ]
}
]
}
},
"/api/v1/users/me/enrollments/{lectureId}.ics": {
"get": {
"tags": [
"Users"
],
"description": "**Required:** any authenticated user",
"parameters": [
{
"name": "lectureId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"404": {
"description": "Not Found",
"content": {
"text/calendar": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"401": {
"description": "Unauthorized — JWT token missing or invalid"
}
},
"security": [
{
"Bearer": [ ]
}
]
}
},
"/api/v1/users/me/reviews": {
"get": {
"tags": [
@@ -4843,6 +4983,16 @@
},
"additionalProperties": false
},
"CalendarSubscriptionDto": {
"type": "object",
"properties": {
"feedUrl": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"CoinTransactionDto": {
"type": "object",
"properties": {