diff --git a/ModeusSchedule.Abstractions/DTO/AttendeesDTO.cs b/ModeusSchedule.Abstractions/DTO/AttendeesDTO.cs new file mode 100644 index 0000000..95a2ecd --- /dev/null +++ b/ModeusSchedule.Abstractions/DTO/AttendeesDTO.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +namespace ModeusSchedule.Abstractions.DTO; + +public partial class Attendees +{ + public static List FromJson(string json) + { + return JsonSerializer.Deserialize>(json); + } +} + +public partial class Attendees +{ + [JsonPropertyName("id")] public Guid Id { get; set; } + + [JsonPropertyName("roleId")] public string RoleId { get; set; } + + [JsonPropertyName("roleName")] public string RoleName { get; set; } + + [JsonPropertyName("roleNamePlural")] public string RoleNamePlural { get; set; } + + [JsonPropertyName("roleDisplayOrder")] public long RoleDisplayOrder { get; set; } + + [JsonPropertyName("personId")] public Guid PersonId { get; set; } + + [JsonPropertyName("lastName")] public string LastName { get; set; } + + [JsonPropertyName("firstName")] public string FirstName { get; set; } + + [JsonPropertyName("middleName")] public string MiddleName { get; set; } + + [JsonPropertyName("fullName")] public string FullName { get; set; } + + [JsonPropertyName("studentId")] public Guid? StudentId { get; set; } + + [JsonPropertyName("specialtyCode")] public string SpecialtyCode { get; set; } + + [JsonPropertyName("specialtyName")] public string SpecialtyName { get; set; } + + [JsonPropertyName("specialtyProfile")] public string SpecialtyProfile { get; set; } +} + +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 \ No newline at end of file diff --git a/SfeduSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs b/ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs similarity index 59% rename from SfeduSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs rename to ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs index acc06bf..7fb1fd1 100644 --- a/SfeduSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs +++ b/ModeusSchedule.Abstractions/DTO/ModeusScheduleRequestDTO.cs @@ -1,9 +1,9 @@ using System.ComponentModel; -namespace SfeduSchedule.Abstractions; +namespace ModeusSchedule.Abstractions.DTO; /// -/// DTO для запроса расписания в Modeus. +/// DTO для запроса расписания в Modeus. /// public class ModeusScheduleRequest( int size, @@ -20,28 +20,28 @@ public class ModeusScheduleRequest( List? typeId) { /// - /// Количество элементов в ответе. + /// Количество элементов в ответе. /// [DefaultValue(10)] public int Size { get; set; } = size; /// - /// Начальная дата и время. + /// Начальная дата и время. /// public DateTime TimeMin { get; set; } = timeMin; /// - /// Конечная дата и время. + /// Конечная дата и время. /// public DateTime TimeMax { get; set; } = timeMax; /// - /// Список идентификаторов аудиторий. (Guid) + /// Список идентификаторов аудиторий. (Guid) /// public List? RoomId { get; set; } = roomId; /// - /// Список идентификаторов участников. + /// Список идентификаторов участников. /// public List? AttendeePersonId { get; set; } = attendeePersonId; @@ -49,66 +49,66 @@ public class ModeusScheduleRequest( public List? CycleRealizationId { get; set; } = cycleRealizationId; /// - /// Список кодов специальностей. + /// Список кодов специальностей. /// - [DefaultValue(new string[] { "09.03.04" })] + [DefaultValue(new[] { "09.03.04" })] public List? SpecialtyCode { get; set; } = specialtyCode; /// - /// Список годов начала обучения. + /// Список годов начала обучения. /// - [DefaultValue(new int[] { 2022, 2023, 2024, 2025 })] + [DefaultValue(new[] { 2022, 2023, 2024, 2025 })] public List? LearningStartYear { get; set; } = learningStartYear; /// - /// Список названий профилей подготовки. + /// Список названий профилей подготовки. /// - [DefaultValue(new string[] { "Методы и средства разработки программного обеспечения" })] + [DefaultValue(new[] { "Методы и средства разработки программного обеспечения" })] public List? ProfileName { get; set; } = profileName; /// - /// Список идентификаторов учебных планов. + /// Список идентификаторов учебных планов. /// public List? CurriculumId { get; set; } = curriculumId; /// - /// Список типов мероприятий. + /// Список типов мероприятий. /// - [DefaultValue(new string[] { "MID_CHECK", "CONS", "LAB", "LECT", "SEMI", "EVENT_OTHER", "SELF", "CUR_CHECK" })] + [DefaultValue(new[] { "MID_CHECK", "CONS", "LAB", "LECT", "SEMI", "EVENT_OTHER", "SELF", "CUR_CHECK" })] public List? TypeId { get; set; } = typeId; } /// -/// DTO для поиска аудиторий. +/// DTO для поиска аудиторий. /// public class RoomSearchRequest { /// - /// Название аудитории. + /// Название аудитории. /// [DefaultValue("")] public string Name { get; set; } = ""; /// - /// Сортировка. + /// Сортировка. /// [DefaultValue("+building.name,+name")] public string Sort { get; set; } = "+building.name,+name"; /// - /// Количество элементов в ответе. + /// Количество элементов в ответе. /// [DefaultValue(10)] public int Size { get; set; } = 10; /// - /// Номер страницы. (пагинация) + /// Номер страницы. (пагинация) /// [DefaultValue(0)] public int Page { get; set; } = 0; /// - /// Исключать архивные аудитории. false = да, true = нет + /// Исключать архивные аудитории. false = да, true = нет /// [DefaultValue(false)] public bool Deleted { get; set; } = false; diff --git a/ModeusSchedule.Abstractions/DTO/ScheduleDTO.CS b/ModeusSchedule.Abstractions/DTO/ScheduleDTO.CS new file mode 100644 index 0000000..e2a0d9d --- /dev/null +++ b/ModeusSchedule.Abstractions/DTO/ScheduleDTO.CS @@ -0,0 +1,591 @@ +// +// Вот этим сайтом https://app.quicktype.io/?l=csharp +// Не является точной копией ответа, могут быть отличия + +#nullable enable +#pragma warning disable CS8618 +#pragma warning disable CS8601 +#pragma warning disable CS8603 + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; + +namespace SfeduSchedule; + +public partial class Schedule +{ + [JsonPropertyName("_embedded")] public Embedded Embedded { get; set; } + + [JsonPropertyName("page")] public Page Page { get; set; } +} + +public partial class Embedded +{ + [JsonPropertyName("events")] public Event[] Events { get; set; } + + [JsonPropertyName("course-unit-realizations")] + public CourseUnitRealization[] CourseUnitRealizations { get; set; } + + [JsonPropertyName("cycle-realizations")] + public CycleRealization[] CycleRealizations { get; set; } + + [JsonPropertyName("lesson-realization-teams")] + public LessonRealizationTeam[] LessonRealizationTeams { get; set; } + + [JsonPropertyName("lesson-realizations")] + public LessonRealization[] LessonRealizations { get; set; } + + [JsonPropertyName("event-locations")] public EventLocation[] EventLocations { get; set; } + + [JsonPropertyName("durations")] public Duration[] Durations { get; set; } + + [JsonPropertyName("event-rooms")] public EventRoom[] EventRooms { get; set; } + + [JsonPropertyName("rooms")] public Room[] Rooms { get; set; } + + [JsonPropertyName("buildings")] public BuildingElement[] Buildings { get; set; } + + [JsonPropertyName("event-teams")] public EventTeam[] EventTeams { get; set; } + + [JsonPropertyName("event-organizers")] public EventOrganizer[] EventOrganizers { get; set; } + + [JsonPropertyName("event-attendees")] public EventAttendee[] EventAttendees { get; set; } + + [JsonPropertyName("persons")] public Person[] Persons { get; set; } +} + +public partial class BuildingElement +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("address")] public string Address { get; set; } + + [JsonPropertyName("searchableAddress")] + public string SearchableAddress { get; set; } + + [JsonPropertyName("displayOrder")] public long DisplayOrder { get; set; } + + [JsonPropertyName("_links")] public BuildingLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class BuildingLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } +} + +public partial class Self +{ + [JsonPropertyName("href")] public string Href { get; set; } +} + +public partial class CourseUnitRealization +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("prototypeId")] public Guid PrototypeId { get; set; } + + [JsonPropertyName("_links")] public CourseUnitRealizationLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class CourseUnitRealizationLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("planning-period")] public Self PlanningPeriod { get; set; } +} + +public partial class CycleRealization +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("code")] public string Code { get; set; } + + [JsonPropertyName("courseUnitRealizationNameShort")] + public string CourseUnitRealizationNameShort { get; set; } + + [JsonPropertyName("_links")] public CycleRealizationLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class CycleRealizationLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("course-unit-realization")] + public Self CourseUnitRealization { get; set; } +} + +public partial class Duration +{ + [JsonPropertyName("eventId")] public Guid EventId { get; set; } + + [JsonPropertyName("value")] public long Value { get; set; } + + [JsonPropertyName("timeUnitId")] public string TimeUnitId { get; set; } + + [JsonPropertyName("minutes")] public long Minutes { get; set; } + + [JsonPropertyName("_links")] public DurationLinks Links { get; set; } +} + +public partial class DurationLinks +{ + [JsonPropertyName("self")] public Self[] Self { get; set; } + + [JsonPropertyName("time-unit")] public Self TimeUnit { get; set; } +} + +public partial class EventAttendee +{ + [JsonPropertyName("roleId")] public string RoleId { get; set; } + + [JsonPropertyName("roleName")] public string RoleName { get; set; } + + [JsonPropertyName("roleNamePlural")] public string RoleNamePlural { get; set; } + + [JsonPropertyName("roleDisplayOrder")] public long RoleDisplayOrder { get; set; } + + [JsonPropertyName("_links")] public EventAttendeeLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class EventAttendeeLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("event")] public Self Event { get; set; } + + [JsonPropertyName("person")] public Self Person { get; set; } +} + +public partial class EventLocation +{ + [JsonPropertyName("eventId")] public Guid EventId { get; set; } + + [JsonPropertyName("customLocation")] public string CustomLocation { get; set; } + + [JsonPropertyName("_links")] public EventLocationLinks Links { get; set; } +} + +public partial class EventLocationLinks +{ + [JsonPropertyName("self")] public Self[] Self { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("event-rooms")] + public Self EventRooms { get; set; } +} + +public partial class EventOrganizer +{ + [JsonPropertyName("eventId")] public Guid EventId { get; set; } + + [JsonPropertyName("_links")] public EventOrganizerLinks Links { get; set; } +} + +public partial class EventOrganizerLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("event")] public Self Event { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("event-attendees")] + public EventAttendees? EventAttendees { get; set; } +} + +public partial class EventRoom +{ + [JsonPropertyName("_links")] public EventRoomLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class EventRoomLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("event")] public Self Event { get; set; } + + [JsonPropertyName("room")] public Self Room { get; set; } +} + +public partial class EventTeam +{ + [JsonPropertyName("eventId")] public Guid EventId { get; set; } + + [JsonPropertyName("size")] public long Size { get; set; } + + [JsonPropertyName("_links")] public EventTeamLinks Links { get; set; } +} + +public partial class EventTeamLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("event")] public Self Event { get; set; } +} + +public partial class Event +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("description")] public object Description { get; set; } + + [JsonPropertyName("typeId")] public string TypeId { get; set; } + + [JsonPropertyName("formatId")] public string FormatId { get; set; } + + [JsonPropertyName("start")] public DateTime Start { get; set; } + + [JsonPropertyName("end")] public DateTime End { get; set; } + + [JsonPropertyName("startsAtLocal")] public DateTime StartsAtLocal { get; set; } + + [JsonPropertyName("endsAtLocal")] public DateTime EndsAtLocal { get; set; } + + [JsonPropertyName("startsAt")] public DateTime StartsAt { get; set; } + + [JsonPropertyName("endsAt")] public DateTime EndsAt { get; set; } + + [JsonPropertyName("holdingStatus")] public HoldingStatus HoldingStatus { get; set; } + + [JsonPropertyName("repeatedLessonRealization")] + public RepeatedLessonRealization RepeatedLessonRealization { get; set; } + + [JsonPropertyName("userRoleIds")] public string[] UserRoleIds { get; set; } + + [JsonPropertyName("lessonTemplateId")] public Guid? LessonTemplateId { get; set; } + + [JsonPropertyName("__version")] public long Version { get; set; } + + [JsonPropertyName("_links")] public Dictionary Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class HoldingStatus +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("audModifiedAt")] public DateTimeOffset? AudModifiedAt { get; set; } + + [JsonPropertyName("audModifiedBy")] public Guid? AudModifiedBy { get; set; } + + [JsonPropertyName("audModifiedBySystem")] + public bool? AudModifiedBySystem { get; set; } +} + +public partial class RepeatedLessonRealization +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("lessonTeamName")] public string LessonTeamName { get; set; } +} + +public partial class LessonRealizationTeam +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("cycleRealizationId")] + public Guid CycleRealizationId { get; set; } + + [JsonPropertyName("_links")] public BuildingLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class LessonRealization +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("prototypeId")] public Guid PrototypeId { get; set; } + + [JsonPropertyName("ordinal")] public long Ordinal { get; set; } + + [JsonPropertyName("_links")] public BuildingLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class Person +{ + [JsonPropertyName("lastName")] public string LastName { get; set; } + + [JsonPropertyName("firstName")] public string FirstName { get; set; } + + [JsonPropertyName("middleName")] public string MiddleName { get; set; } + + [JsonPropertyName("fullName")] public string FullName { get; set; } + + [JsonPropertyName("_links")] public BuildingLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class Room +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("building")] public RoomBuilding Building { get; set; } + + [JsonPropertyName("projectorAvailable")] + public bool ProjectorAvailable { get; set; } + + [JsonPropertyName("totalCapacity")] public long TotalCapacity { get; set; } + + [JsonPropertyName("workingCapacity")] public long WorkingCapacity { get; set; } + + [JsonPropertyName("deletedAtUtc")] public object DeletedAtUtc { get; set; } + + [JsonPropertyName("_links")] public RoomLinks Links { get; set; } + + [JsonPropertyName("id")] public Guid Id { get; set; } +} + +public partial class RoomBuilding +{ + [JsonPropertyName("id")] public Guid Id { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("nameShort")] public string NameShort { get; set; } + + [JsonPropertyName("address")] public string Address { get; set; } + + [JsonPropertyName("displayOrder")] public long DisplayOrder { get; set; } +} + +public partial class RoomLinks +{ + [JsonPropertyName("self")] public Self Self { get; set; } + + [JsonPropertyName("type")] public Self Type { get; set; } + + [JsonPropertyName("building")] public Self Building { get; set; } +} + +public partial class Page +{ + [JsonPropertyName("size")] public long Size { get; set; } + + [JsonPropertyName("totalElements")] public long TotalElements { get; set; } + + [JsonPropertyName("totalPages")] public long TotalPages { get; set; } + + [JsonPropertyName("number")] public long Number { get; set; } +} + +public partial struct EventAttendees +{ + public Self Self; + public Self[] SelfArray; + + public static implicit operator EventAttendees(Self Self) => new EventAttendees { Self = Self }; + public static implicit operator EventAttendees(Self[] SelfArray) => new EventAttendees { SelfArray = SelfArray }; +} + +public partial class Schedule +{ + public static Schedule FromJson(string json) => + JsonSerializer.Deserialize(json, SfeduSchedule.Converter.Settings); +} + +public static class Serialize +{ + public static string ToJson(this Schedule self) => JsonSerializer.Serialize(self, SfeduSchedule.Converter.Settings); +} + +internal static class Converter +{ + public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) + { + Converters = + { + EventAttendeesConverter.Singleton, + new DateOnlyConverter(), + new TimeOnlyConverter(), + IsoDateTimeOffsetConverter.Singleton + }, + }; +} + +internal class EventAttendeesConverter : JsonConverter +{ + public override bool CanConvert(Type t) => t == typeof(EventAttendees); + + public override EventAttendees Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + var objectValue = JsonSerializer.Deserialize(ref reader, options); + return new EventAttendees { Self = objectValue }; + case JsonTokenType.StartArray: + var arrayValue = JsonSerializer.Deserialize(ref reader, options); + return new EventAttendees { SelfArray = arrayValue }; + } + + throw new Exception("Cannot unmarshal type EventAttendees"); + } + + public override void Write(Utf8JsonWriter writer, EventAttendees value, JsonSerializerOptions options) + { + if (value.SelfArray != null) + { + JsonSerializer.Serialize(writer, value.SelfArray, options); + return; + } + + if (value.Self != null) + { + JsonSerializer.Serialize(writer, value.Self, options); + return; + } + + throw new Exception("Cannot marshal type EventAttendees"); + } + + public static readonly EventAttendeesConverter Singleton = new EventAttendeesConverter(); +} + +public class DateOnlyConverter : JsonConverter +{ + private readonly string serializationFormat; + + public DateOnlyConverter() : this(null) + { + } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); +} + +public class TimeOnlyConverter : JsonConverter +{ + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) + { + } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); +} + +internal class IsoDateTimeOffsetConverter : JsonConverter +{ + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); +} +#pragma warning restore CS8618 +#pragma warning restore CS8601 +#pragma warning restore CS8603 \ No newline at end of file diff --git a/ModeusSchedule.Abstractions/GlobalConsts.cs b/ModeusSchedule.Abstractions/GlobalConsts.cs new file mode 100644 index 0000000..10ac76e --- /dev/null +++ b/ModeusSchedule.Abstractions/GlobalConsts.cs @@ -0,0 +1,12 @@ +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace ModeusSchedule.Abstractions; + +public static class GlobalConsts +{ + public static readonly JsonSerializerOptions JsonSerializerOptions = new() + { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + + public static string JwtFilePath { get; set; } = "data/jwt.txt"; +} \ No newline at end of file diff --git a/SfeduSchedule.Abstractions/IPlugin.cs b/ModeusSchedule.Abstractions/IPlugin.cs similarity index 92% rename from SfeduSchedule.Abstractions/IPlugin.cs rename to ModeusSchedule.Abstractions/IPlugin.cs index 4f5325c..1af4538 100644 --- a/SfeduSchedule.Abstractions/IPlugin.cs +++ b/ModeusSchedule.Abstractions/IPlugin.cs @@ -1,4 +1,4 @@ -namespace SfeduSchedule.Abstractions; +namespace ModeusSchedule.Abstractions; // Базовый контракт плагина (общий для хоста и плагинов) public interface IPlugin diff --git a/SfeduSchedule.Abstractions/SfeduSchedule.Abstractions.csproj b/ModeusSchedule.Abstractions/ModeusSchedule.Abstractions.csproj similarity index 86% rename from SfeduSchedule.Abstractions/SfeduSchedule.Abstractions.csproj rename to ModeusSchedule.Abstractions/ModeusSchedule.Abstractions.csproj index 5091114..828a1e4 100644 --- a/SfeduSchedule.Abstractions/SfeduSchedule.Abstractions.csproj +++ b/ModeusSchedule.Abstractions/ModeusSchedule.Abstractions.csproj @@ -5,6 +5,7 @@ enable enable Library + ModeusSchedule.Abstractions diff --git a/SfeduSchedule.Abstractions/DTO/AttendeesDTO.cs b/SfeduSchedule.Abstractions/DTO/AttendeesDTO.cs deleted file mode 100644 index bdbf41d..0000000 --- a/SfeduSchedule.Abstractions/DTO/AttendeesDTO.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -#nullable enable -#pragma warning disable CS8618 -#pragma warning disable CS8601 -#pragma warning disable CS8603 - -namespace SfeduSchedule.Abstractions; - -public partial class Attendees -{ - public static List FromJson(string json) => JsonSerializer.Deserialize>(json); -} - -public partial class Attendees -{ - [JsonPropertyName("id")] - public Guid Id { get; set; } - - [JsonPropertyName("roleId")] - public string RoleId { get; set; } - - [JsonPropertyName("roleName")] - public string RoleName { get; set; } - - [JsonPropertyName("roleNamePlural")] - public string RoleNamePlural { get; set; } - - [JsonPropertyName("roleDisplayOrder")] - public long RoleDisplayOrder { get; set; } - - [JsonPropertyName("personId")] - public Guid PersonId { get; set; } - - [JsonPropertyName("lastName")] - public string LastName { get; set; } - - [JsonPropertyName("firstName")] - public string FirstName { get; set; } - - [JsonPropertyName("middleName")] - public string MiddleName { get; set; } - - [JsonPropertyName("fullName")] - public string FullName { get; set; } - - [JsonPropertyName("studentId")] - public Guid? StudentId { get; set; } - - [JsonPropertyName("specialtyCode")] - public string SpecialtyCode { get; set; } - - [JsonPropertyName("specialtyName")] - public string SpecialtyName { get; set; } - - [JsonPropertyName("specialtyProfile")] - public string SpecialtyProfile { get; set; } -} - -#pragma warning restore CS8618 -#pragma warning restore CS8601 -#pragma warning restore CS8603 \ No newline at end of file diff --git a/SfeduSchedule.Abstractions/DTO/ScheduleDTO.CS b/SfeduSchedule.Abstractions/DTO/ScheduleDTO.CS deleted file mode 100644 index ea78d32..0000000 --- a/SfeduSchedule.Abstractions/DTO/ScheduleDTO.CS +++ /dev/null @@ -1,709 +0,0 @@ -// -// Вот этим сайтом https://app.quicktype.io/?l=csharp -// Не является точной копией ответа, могут быть отличия - -#nullable enable -#pragma warning disable CS8618 -#pragma warning disable CS8601 -#pragma warning disable CS8603 - -namespace SfeduSchedule -{ - using System; - using System.Collections.Generic; - - using System.Text.Json; - using System.Text.Json.Serialization; - using System.Globalization; - - public partial class Schedule - { - [JsonPropertyName("_embedded")] - public Embedded Embedded { get; set; } - - [JsonPropertyName("page")] - public Page Page { get; set; } - } - - public partial class Embedded - { - [JsonPropertyName("events")] - public Event[] Events { get; set; } - - [JsonPropertyName("course-unit-realizations")] - public CourseUnitRealization[] CourseUnitRealizations { get; set; } - - [JsonPropertyName("cycle-realizations")] - public CycleRealization[] CycleRealizations { get; set; } - - [JsonPropertyName("lesson-realization-teams")] - public LessonRealizationTeam[] LessonRealizationTeams { get; set; } - - [JsonPropertyName("lesson-realizations")] - public LessonRealization[] LessonRealizations { get; set; } - - [JsonPropertyName("event-locations")] - public EventLocation[] EventLocations { get; set; } - - [JsonPropertyName("durations")] - public Duration[] Durations { get; set; } - - [JsonPropertyName("event-rooms")] - public EventRoom[] EventRooms { get; set; } - - [JsonPropertyName("rooms")] - public Room[] Rooms { get; set; } - - [JsonPropertyName("buildings")] - public BuildingElement[] Buildings { get; set; } - - [JsonPropertyName("event-teams")] - public EventTeam[] EventTeams { get; set; } - - [JsonPropertyName("event-organizers")] - public EventOrganizer[] EventOrganizers { get; set; } - - [JsonPropertyName("event-attendees")] - public EventAttendee[] EventAttendees { get; set; } - - [JsonPropertyName("persons")] - public Person[] Persons { get; set; } - } - - public partial class BuildingElement - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("address")] - public string Address { get; set; } - - [JsonPropertyName("searchableAddress")] - public string SearchableAddress { get; set; } - - [JsonPropertyName("displayOrder")] - public long DisplayOrder { get; set; } - - [JsonPropertyName("_links")] - public BuildingLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class BuildingLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - } - - public partial class Self - { - [JsonPropertyName("href")] - public string Href { get; set; } - } - - public partial class CourseUnitRealization - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("prototypeId")] - public Guid PrototypeId { get; set; } - - [JsonPropertyName("_links")] - public CourseUnitRealizationLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class CourseUnitRealizationLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("planning-period")] - public Self PlanningPeriod { get; set; } - } - - public partial class CycleRealization - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("code")] - public string Code { get; set; } - - [JsonPropertyName("courseUnitRealizationNameShort")] - public string CourseUnitRealizationNameShort { get; set; } - - [JsonPropertyName("_links")] - public CycleRealizationLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class CycleRealizationLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("course-unit-realization")] - public Self CourseUnitRealization { get; set; } - } - - public partial class Duration - { - [JsonPropertyName("eventId")] - public Guid EventId { get; set; } - - [JsonPropertyName("value")] - public long Value { get; set; } - - [JsonPropertyName("timeUnitId")] - public string TimeUnitId { get; set; } - - [JsonPropertyName("minutes")] - public long Minutes { get; set; } - - [JsonPropertyName("_links")] - public DurationLinks Links { get; set; } - } - - public partial class DurationLinks - { - [JsonPropertyName("self")] - public Self[] Self { get; set; } - - [JsonPropertyName("time-unit")] - public Self TimeUnit { get; set; } - } - - public partial class EventAttendee - { - [JsonPropertyName("roleId")] - public string RoleId { get; set; } - - [JsonPropertyName("roleName")] - public string RoleName { get; set; } - - [JsonPropertyName("roleNamePlural")] - public string RoleNamePlural { get; set; } - - [JsonPropertyName("roleDisplayOrder")] - public long RoleDisplayOrder { get; set; } - - [JsonPropertyName("_links")] - public EventAttendeeLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class EventAttendeeLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("event")] - public Self Event { get; set; } - - [JsonPropertyName("person")] - public Self Person { get; set; } - } - - public partial class EventLocation - { - [JsonPropertyName("eventId")] - public Guid EventId { get; set; } - - [JsonPropertyName("customLocation")] - public string CustomLocation { get; set; } - - [JsonPropertyName("_links")] - public EventLocationLinks Links { get; set; } - } - - public partial class EventLocationLinks - { - [JsonPropertyName("self")] - public Self[] Self { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("event-rooms")] - public Self EventRooms { get; set; } - } - - public partial class EventOrganizer - { - [JsonPropertyName("eventId")] - public Guid EventId { get; set; } - - [JsonPropertyName("_links")] - public EventOrganizerLinks Links { get; set; } - } - - public partial class EventOrganizerLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("event")] - public Self Event { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("event-attendees")] - public EventAttendees? EventAttendees { get; set; } - } - - public partial class EventRoom - { - [JsonPropertyName("_links")] - public EventRoomLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class EventRoomLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("event")] - public Self Event { get; set; } - - [JsonPropertyName("room")] - public Self Room { get; set; } - } - - public partial class EventTeam - { - [JsonPropertyName("eventId")] - public Guid EventId { get; set; } - - [JsonPropertyName("size")] - public long Size { get; set; } - - [JsonPropertyName("_links")] - public EventTeamLinks Links { get; set; } - } - - public partial class EventTeamLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("event")] - public Self Event { get; set; } - } - - public partial class Event - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("description")] - public object Description { get; set; } - - [JsonPropertyName("typeId")] - public string TypeId { get; set; } - - [JsonPropertyName("formatId")] - public string FormatId { get; set; } - - [JsonPropertyName("start")] - public DateTime Start { get; set; } - - [JsonPropertyName("end")] - public DateTime End { get; set; } - - [JsonPropertyName("startsAtLocal")] - public DateTime StartsAtLocal { get; set; } - - [JsonPropertyName("endsAtLocal")] - public DateTime EndsAtLocal { get; set; } - - [JsonPropertyName("startsAt")] - public DateTime StartsAt { get; set; } - - [JsonPropertyName("endsAt")] - public DateTime EndsAt { get; set; } - - [JsonPropertyName("holdingStatus")] - public HoldingStatus HoldingStatus { get; set; } - - [JsonPropertyName("repeatedLessonRealization")] - public RepeatedLessonRealization RepeatedLessonRealization { get; set; } - - [JsonPropertyName("userRoleIds")] - public string[] UserRoleIds { get; set; } - - [JsonPropertyName("lessonTemplateId")] - public Guid? LessonTemplateId { get; set; } - - [JsonPropertyName("__version")] - public long Version { get; set; } - - [JsonPropertyName("_links")] - public Dictionary Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class HoldingStatus - { - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("audModifiedAt")] - public DateTimeOffset? AudModifiedAt { get; set; } - - [JsonPropertyName("audModifiedBy")] - public Guid? AudModifiedBy { get; set; } - - [JsonPropertyName("audModifiedBySystem")] - public bool? AudModifiedBySystem { get; set; } - } - - public partial class RepeatedLessonRealization - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("lessonTeamName")] - public string LessonTeamName { get; set; } - } - - public partial class LessonRealizationTeam - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("cycleRealizationId")] - public Guid CycleRealizationId { get; set; } - - [JsonPropertyName("_links")] - public BuildingLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class LessonRealization - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("prototypeId")] - public Guid PrototypeId { get; set; } - - [JsonPropertyName("ordinal")] - public long Ordinal { get; set; } - - [JsonPropertyName("_links")] - public BuildingLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class Person - { - [JsonPropertyName("lastName")] - public string LastName { get; set; } - - [JsonPropertyName("firstName")] - public string FirstName { get; set; } - - [JsonPropertyName("middleName")] - public string MiddleName { get; set; } - - [JsonPropertyName("fullName")] - public string FullName { get; set; } - - [JsonPropertyName("_links")] - public BuildingLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class Room - { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("building")] - public RoomBuilding Building { get; set; } - - [JsonPropertyName("projectorAvailable")] - public bool ProjectorAvailable { get; set; } - - [JsonPropertyName("totalCapacity")] - public long TotalCapacity { get; set; } - - [JsonPropertyName("workingCapacity")] - public long WorkingCapacity { get; set; } - - [JsonPropertyName("deletedAtUtc")] - public object DeletedAtUtc { get; set; } - - [JsonPropertyName("_links")] - public RoomLinks Links { get; set; } - - [JsonPropertyName("id")] - public Guid Id { get; set; } - } - - public partial class RoomBuilding - { - [JsonPropertyName("id")] - public Guid Id { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("nameShort")] - public string NameShort { get; set; } - - [JsonPropertyName("address")] - public string Address { get; set; } - - [JsonPropertyName("displayOrder")] - public long DisplayOrder { get; set; } - } - - public partial class RoomLinks - { - [JsonPropertyName("self")] - public Self Self { get; set; } - - [JsonPropertyName("type")] - public Self Type { get; set; } - - [JsonPropertyName("building")] - public Self Building { get; set; } - } - - public partial class Page - { - [JsonPropertyName("size")] - public long Size { get; set; } - - [JsonPropertyName("totalElements")] - public long TotalElements { get; set; } - - [JsonPropertyName("totalPages")] - public long TotalPages { get; set; } - - [JsonPropertyName("number")] - public long Number { get; set; } - } - - public partial struct EventAttendees - { - public Self Self; - public Self[] SelfArray; - - public static implicit operator EventAttendees(Self Self) => new EventAttendees { Self = Self }; - public static implicit operator EventAttendees(Self[] SelfArray) => new EventAttendees { SelfArray = SelfArray }; - } - - public partial class Schedule - { - public static Schedule FromJson(string json) => JsonSerializer.Deserialize(json, SfeduSchedule.Converter.Settings); - } - - public static class Serialize - { - public static string ToJson(this Schedule self) => JsonSerializer.Serialize(self, SfeduSchedule.Converter.Settings); - } - - internal static class Converter - { - public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General) - { - Converters = - { - EventAttendeesConverter.Singleton, - new DateOnlyConverter(), - new TimeOnlyConverter(), - IsoDateTimeOffsetConverter.Singleton - }, - }; - } - - internal class EventAttendeesConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(EventAttendees); - - public override EventAttendees Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.StartObject: - var objectValue = JsonSerializer.Deserialize(ref reader, options); - return new EventAttendees { Self = objectValue }; - case JsonTokenType.StartArray: - var arrayValue = JsonSerializer.Deserialize(ref reader, options); - return new EventAttendees { SelfArray = arrayValue }; - } - throw new Exception("Cannot unmarshal type EventAttendees"); - } - - public override void Write(Utf8JsonWriter writer, EventAttendees value, JsonSerializerOptions options) - { - if (value.SelfArray != null) - { - JsonSerializer.Serialize(writer, value.SelfArray, options); - return; - } - if (value.Self != null) - { - JsonSerializer.Serialize(writer, value.Self, options); - return; - } - throw new Exception("Cannot marshal type EventAttendees"); - } - - public static readonly EventAttendeesConverter Singleton = new EventAttendeesConverter(); - } - - public class DateOnlyConverter : JsonConverter - { - private readonly string serializationFormat; - public DateOnlyConverter() : this(null) { } - - public DateOnlyConverter(string? serializationFormat) - { - this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; - } - - public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - return DateOnly.Parse(value!); - } - - public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(serializationFormat)); - } - - public class TimeOnlyConverter : JsonConverter - { - private readonly string serializationFormat; - - public TimeOnlyConverter() : this(null) { } - - public TimeOnlyConverter(string? serializationFormat) - { - this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; - } - - public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - return TimeOnly.Parse(value!); - } - - public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(serializationFormat)); - } - - internal class IsoDateTimeOffsetConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); - - private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; - - private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; - private string? _dateTimeFormat; - private CultureInfo? _culture; - - public DateTimeStyles DateTimeStyles - { - get => _dateTimeStyles; - set => _dateTimeStyles = value; - } - - public string? DateTimeFormat - { - get => _dateTimeFormat ?? string.Empty; - set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; - } - - public CultureInfo Culture - { - get => _culture ?? CultureInfo.CurrentCulture; - set => _culture = value; - } - - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) - { - string text; - - - if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal - || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) - { - value = value.ToUniversalTime(); - } - - text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); - - writer.WriteStringValue(text); - } - - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string? dateText = reader.GetString(); - - if (string.IsNullOrEmpty(dateText) == false) - { - if (!string.IsNullOrEmpty(_dateTimeFormat)) - { - return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); - } - else - { - return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); - } - } - else - { - return default(DateTimeOffset); - } - } - - - public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); - } -} -#pragma warning restore CS8618 -#pragma warning restore CS8601 -#pragma warning restore CS8603 diff --git a/SfeduSchedule.Plugin.Sample/Plugin.cs b/SfeduSchedule.Plugin.Sample/Plugin.cs index 1761fb1..d49a308 100644 --- a/SfeduSchedule.Plugin.Sample/Plugin.cs +++ b/SfeduSchedule.Plugin.Sample/Plugin.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using SfeduSchedule.Abstractions; +using ModeusSchedule.Abstractions; namespace SfeduSchedule.Plugin.Sample; diff --git a/SfeduSchedule.Plugin.Sample/SfeduSchedule.Plugin.Sample.csproj b/SfeduSchedule.Plugin.Sample/SfeduSchedule.Plugin.Sample.csproj index dc7d7eb..8d31fc6 100644 --- a/SfeduSchedule.Plugin.Sample/SfeduSchedule.Plugin.Sample.csproj +++ b/SfeduSchedule.Plugin.Sample/SfeduSchedule.Plugin.Sample.csproj @@ -12,7 +12,7 @@ - + diff --git a/SfeduSchedule.sln b/SfeduSchedule.sln index c832b11..24279d5 100644 --- a/SfeduSchedule.sln +++ b/SfeduSchedule.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule", "SfeduSchedule\SfeduSchedule.csproj", "{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule.Abstractions", "SfeduSchedule.Abstractions\SfeduSchedule.Abstractions.csproj", "{B2E8DAD7-7373-4155-B230-4E53DFC04445}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModeusSchedule.Abstractions", "ModeusSchedule.Abstractions\ModeusSchedule.Abstractions.csproj", "{B2E8DAD7-7373-4155-B230-4E53DFC04445}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule.Plugin.Sample", "SfeduSchedule.Plugin.Sample\SfeduSchedule.Plugin.Sample.csproj", "{B2B6D730-46AE-40ED-815F-81176FB4E545}" EndProject diff --git a/SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs b/SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs index 944b101..9b61bba 100644 --- a/SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs +++ b/SfeduSchedule/Auth/ApiKeyAuthenticationHandler.cs @@ -1,7 +1,7 @@ using System.Security.Claims; +using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; -using System.Security.Cryptography; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; @@ -41,7 +41,7 @@ public class ApiKeyAuthenticationHandler( var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "api-key"), - new Claim(ClaimTypes.Name, "api-key-user"), + new Claim(ClaimTypes.Name, "api-key-user") }; var identity = new ClaimsIdentity(claims, ApiKeyAuthenticationDefaults.Scheme); diff --git a/SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs b/SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs index b3738e1..11ca598 100644 --- a/SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs +++ b/SfeduSchedule/Auth/SwaggerAuthorizeOperationFilter.cs @@ -1,33 +1,33 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using Microsoft.AspNetCore.Authorization; -namespace SfeduSchedule.Auth +namespace SfeduSchedule.Auth; + +public class SwaggerAuthorizeOperationFilter : IOperationFilter { - public class SwaggerAuthorizeOperationFilter : IOperationFilter + public void Apply(OpenApiOperation operation, OperationFilterContext context) { - public void Apply(OpenApiOperation operation, OperationFilterContext context) + var hasAuthorize = context.MethodInfo.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.DeclaringType?.GetCustomAttributes(true).OfType() + .Any() == true; + if (hasAuthorize) { - var hasAuthorize = context.MethodInfo.GetCustomAttributes(true).OfType().Any() || - context.MethodInfo.DeclaringType?.GetCustomAttributes(true).OfType().Any() == true; - if (hasAuthorize) + operation.Security ??= new List(); + operation.Security.Add(new OpenApiSecurityRequirement { - operation.Security ??= new List(); - operation.Security.Add(new OpenApiSecurityRequirement { + new OpenApiSecurityScheme { - new OpenApiSecurityScheme + Reference = new OpenApiReference { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = ApiKeyAuthenticationDefaults.Scheme - } - }, - new List() - } - }); - } + Type = ReferenceType.SecurityScheme, + Id = ApiKeyAuthenticationDefaults.Scheme + } + }, + new List() + } + }); } } -} +} \ No newline at end of file diff --git a/SfeduSchedule/Controllers/ProxyController.cs b/SfeduSchedule/Controllers/ProxyController.cs index 7e6eb05..598f80e 100644 --- a/SfeduSchedule/Controllers/ProxyController.cs +++ b/SfeduSchedule/Controllers/ProxyController.cs @@ -2,7 +2,8 @@ using System.Net; using System.Text.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; -using SfeduSchedule.Abstractions; +using ModeusSchedule.Abstractions; +using ModeusSchedule.Abstractions.DTO; using SfeduSchedule.Services; namespace SfeduSchedule.Controllers; @@ -13,7 +14,7 @@ namespace SfeduSchedule.Controllers; public class ProxyController(ModeusService modeusService, ILogger logger) : ControllerBase { /// - /// Получить расписание по пользовательскому запросу. + /// Получить расписание по пользовательскому запросу. /// /// Объект запроса, содержащий параметры фильтрации расписания. /// Список событий расписания. @@ -32,16 +33,16 @@ public class ProxyController(ModeusService modeusService, ILogger - /// Поиск аудиторий по пользовательскому запросу. + /// Поиск аудиторий по пользовательскому запросу. /// /// Объект запроса, содержащий параметры фильтрации аудиторий. /// Список аудиторий. @@ -59,7 +60,7 @@ public class ProxyController(ModeusService modeusService, ILogger logger) : ControllerBase { /// - /// [УСТАРЕЛО] Получить расписание по пользовательскому запросу. + /// [УСТАРЕЛО] Получить расписание по пользовательскому запросу. /// /// Объект запроса, содержащий параметры фильтрации расписания. /// Список событий расписания. @@ -32,7 +34,7 @@ public class ScheduleController(ModeusService modeusService, ILogger - /// [УСТАРЕЛО] Поиск аудиторий по пользовательскому запросу. + /// [УСТАРЕЛО] Поиск аудиторий по пользовательскому запросу. /// /// Объект запроса, содержащий параметры фильтрации аудиторий. /// Список аудиторий. @@ -59,7 +61,7 @@ public class ScheduleController(ModeusService modeusService, ILogger - /// Получить GUID пользователя по полному имени. (требуется авторизация) + /// Получить GUID пользователя по полному имени. (требуется авторизация) /// /// Полное имя пользователя. /// GUID пользователя. @@ -89,7 +91,7 @@ public class ScheduleController(ModeusService modeusService, ILogger - /// Получить расписание в формате ICS по пользовательскому запросу. + /// Получить расписание в формате ICS по пользовательскому запросу. /// /// Объект запроса, содержащий параметры фильтрации расписания. /// Файл ICS с расписанием за -1 неделя + 1 месяц @@ -104,14 +106,14 @@ public class ScheduleController(ModeusService modeusService, ILogger - /// Получить расписание в формате ICS для указанного пользователя за -1 неделя + 1 месяц. + /// Получить расписание в формате ICS для указанного пользователя за -1 неделя + 1 месяц. /// /// /// Файл ICS с расписанием diff --git a/SfeduSchedule/Controllers/SfeduController.cs b/SfeduSchedule/Controllers/SfeduController.cs index baa9ca8..ecce388 100644 --- a/SfeduSchedule/Controllers/SfeduController.cs +++ b/SfeduSchedule/Controllers/SfeduController.cs @@ -2,41 +2,39 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SfeduSchedule.Services; -namespace SfeduSchedule.Controllers +namespace SfeduSchedule.Controllers; + +[ApiController] +[Route("api/sfedu")] +[Authorize(AuthenticationSchemes = "OpenIdConnect")] +public class SfeduController(ModeusService modeusService) : ControllerBase { - [ApiController] - [Route("api/sfedu")] - [Authorize(AuthenticationSchemes = "OpenIdConnect")] - public class SfeduController(ModeusService modeusService) : ControllerBase + /// + /// Получить GUID пользователя через авторизацию Microsoft. + /// + /// + /// Необязательный параметр. Если указан, произойдет редирект на указанный URI после получения + /// GUID. ([url]/?guid=XXX) + /// + /// Строка GUID пользователя или редирект на указанный URI. + /// Возвращает GUID пользователя + /// Редирект на указанный URI + /// Пользователь не найден + /// Неавторизованный + [HttpGet] + [Route("guid")] + public async Task Get([FromQuery] string? redirectUri) { - /// - /// Получить GUID пользователя через авторизацию Microsoft. - /// - /// Необязательный параметр. Если указан, произойдет редирект на указанный URI после получения GUID. ([url]/?guid=XXX) - /// Строка GUID пользователя или редирект на указанный URI. - /// Возвращает GUID пользователя - /// Редирект на указанный URI - /// Пользователь не найден - /// Неавторизованный - [HttpGet] - [Route("guid")] - public async Task Get([FromQuery] string? redirectUri) - { - var name = User.FindFirst("name")?.Value; - if (string.IsNullOrEmpty(name)) - return StatusCode(StatusCodes.Status500InternalServerError); + var name = User.FindFirst("name")?.Value; + if (string.IsNullOrEmpty(name)) + return StatusCode(StatusCodes.Status500InternalServerError); - var guid = await modeusService.GetGuidAsync(name); - if (string.IsNullOrEmpty(guid)) - return NotFound(); + var guid = await modeusService.GetGuidAsync(name); + if (string.IsNullOrEmpty(guid)) + return NotFound(); - if (!string.IsNullOrEmpty(redirectUri)) - { - return Redirect(redirectUri + "?guid=" + guid); - } - - return Ok(guid); - } + if (!string.IsNullOrEmpty(redirectUri)) return Redirect(redirectUri + "?guid=" + guid); + return Ok(guid); } -} +} \ No newline at end of file diff --git a/SfeduSchedule/GlobalVariables.cs b/SfeduSchedule/GlobalVariables.cs deleted file mode 100644 index dcff718..0000000 --- a/SfeduSchedule/GlobalVariables.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json; - -namespace SfeduSchedule -{ - public static class GlobalVariables - { - public static string JwtFilePath { get; set; } = "data/jwt.txt"; - public static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - } -} \ No newline at end of file diff --git a/SfeduSchedule/Jobs/UpdateJWTJob.cs b/SfeduSchedule/Jobs/UpdateJWTJob.cs index c547eaf..91e1d0a 100644 --- a/SfeduSchedule/Jobs/UpdateJWTJob.cs +++ b/SfeduSchedule/Jobs/UpdateJWTJob.cs @@ -2,12 +2,15 @@ using Quartz; namespace SfeduSchedule.Jobs; -public class UpdateJwtJob(IConfiguration configuration, ILogger logger, IHttpClientFactory httpClientFactory) : IJob +public class UpdateJwtJob( + IConfiguration configuration, + ILogger logger, + IHttpClientFactory httpClientFactory) : IJob { private const int MaxAttempts = 5; // Максимальное число попыток private const int DelaySeconds = 20; // Задержка между попытками в секундах private const int TimeoutSeconds = 60; // Таймаут для каждого запроса в секундах - + public async Task Execute(IJobExecutionContext jobContext) { logger.LogInformation("Начало выполнения UpdateJwtJob"); @@ -25,10 +28,10 @@ public class UpdateJwtJob(IConfiguration configuration, ILogger lo } for (var attempt = 1; attempt <= MaxAttempts; attempt++) - { try { - logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения JWT из {AuthUrl}", attempt, MaxAttempts, authUrl); + logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения JWT из {AuthUrl}", attempt, + MaxAttempts, authUrl); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds)); var response = await client.GetAsync(authUrl, cts.Token); @@ -47,7 +50,7 @@ public class UpdateJwtJob(IConfiguration configuration, ILogger lo continue; } - var body = await response.Content.ReadFromJsonAsync(cancellationToken: jobContext.CancellationToken); + var body = await response.Content.ReadFromJsonAsync(jobContext.CancellationToken); if (body is null || string.IsNullOrWhiteSpace(body.Jwt)) { @@ -69,7 +72,8 @@ public class UpdateJwtJob(IConfiguration configuration, ILogger lo } catch (OperationCanceledException ex) { - logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts); + logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, + MaxAttempts); if (attempt == MaxAttempts) { @@ -91,7 +95,6 @@ public class UpdateJwtJob(IConfiguration configuration, ILogger lo await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); } - } } private sealed record JwtResponse(string Jwt); diff --git a/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs b/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs index 87e008f..38fd852 100644 --- a/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs +++ b/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs @@ -4,15 +4,15 @@ using System.Net.Sockets; namespace SfeduSchedule.Middleware; /// -/// Middleware ограничивает доступ к endpoint'у (сделано для /metrics) только приватными сетями. -/// Допускаются: loopback, RFC1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16, IPv6 link-local), -/// а также уникальные локальные адреса IPv6 (fc00::/7). Любой другой источник получает 403. -/// Только метод GET. +/// Middleware ограничивает доступ к endpoint'у (сделано для /metrics) только приватными сетями. +/// Допускаются: loopback, RFC1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16, IPv6 link-local), +/// а также уникальные локальные адреса IPv6 (fc00::/7). Любой другой источник получает 403. +/// Только метод GET. /// public class LocalNetworksOnlyMiddleware { - private readonly RequestDelegate _next; private readonly ILogger _logger; + private readonly RequestDelegate _next; public LocalNetworksOnlyMiddleware(RequestDelegate next, ILogger logger) { @@ -25,7 +25,8 @@ public class LocalNetworksOnlyMiddleware // Разрешаем только GET if (!HttpMethods.IsGet(context.Request.Method)) { - _logger.LogWarning("Metrics method not allowed: {Method} {Path}", context.Request.Method, context.Request.Path); + _logger.LogWarning("Metrics method not allowed: {Method} {Path}", context.Request.Method, + context.Request.Path); context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; context.Response.Headers["Allow"] = "GET"; await context.Response.WriteAsync("Method Not Allowed. Only GET is supported for metrics."); @@ -35,17 +36,15 @@ public class LocalNetworksOnlyMiddleware // Получаем реальный клиентский IP. Если есть X-Forwarded-For, берём самый первый IP из списка. var ip = ExtractClientIp(context) ?? context.Connection.RemoteIpAddress; // Если пришёл IPv4, инкапсулированный в IPv6 (::ffff:x.y.z.w), разворачиваем в чистый IPv4. - if (ip is { IsIPv4MappedToIPv6: true }) - { - ip = ip.MapToIPv4(); - } + if (ip is { IsIPv4MappedToIPv6: true }) ip = ip.MapToIPv4(); // Проверяем принадлежность IP локальным/приватным сетям. if (ip is null || !IsLocalNetwork(ip)) { // Фиксируем X-Forwarded-For (если есть) для диагностики за обратными прокси. var xff = context.Request.Headers.TryGetValue("X-Forwarded-For", out var xffVal) ? xffVal.ToString() : null; - _logger.LogWarning("Metrics access forbidden. RemoteIP={RemoteIP}, XFF={XFF}, Path={Path}", ip?.ToString() ?? "null", xff, context.Request.Path); + _logger.LogWarning("Metrics access forbidden. RemoteIP={RemoteIP}, XFF={XFF}, Path={Path}", + ip?.ToString() ?? "null", xff, context.Request.Path); context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsync("Forbidden: metrics available only from local networks"); return; @@ -56,7 +55,7 @@ public class LocalNetworksOnlyMiddleware } /// - /// Определяет, принадлежит ли адрес локальным / приватным диапазонам. + /// Определяет, принадлежит ли адрес локальным / приватным диапазонам. /// private static bool IsLocalNetwork(IPAddress ip) { @@ -92,8 +91,8 @@ public class LocalNetworksOnlyMiddleware } /// - /// Извлекает IP клиента из заголовка X-Forwarded-For (если присутствует). Берется первый IP. - /// Возвращает null, если заголовок отсутствует или содержит некорректные значения. + /// Извлекает IP клиента из заголовка X-Forwarded-For (если присутствует). Берется первый IP. + /// Возвращает null, если заголовок отсутствует или содержит некорректные значения. /// private static IPAddress? ExtractClientIp(HttpContext context) { @@ -114,11 +113,8 @@ public class LocalNetworksOnlyMiddleware first = first.Substring(1, first.Length - 2); // Возможен порт через ':' в IPv4, удалим порт если он указан (для IPv6 двоеточия являются частью адреса) - if (first.Count(c => c == ':') == 1 && first.Contains('.') && first.Contains(':')) - { - first = first.Split(':')[0]; - } + if (first.Count(c => c == ':') == 1 && first.Contains('.') && first.Contains(':')) first = first.Split(':')[0]; return IPAddress.TryParse(first, out var parsed) ? parsed : null; } -} +} \ No newline at end of file diff --git a/SfeduSchedule/PluginLoader.cs b/SfeduSchedule/PluginLoader.cs index 1a28244..98f3328 100644 --- a/SfeduSchedule/PluginLoader.cs +++ b/SfeduSchedule/PluginLoader.cs @@ -1,6 +1,6 @@ using System.Reflection; using System.Runtime.Loader; -using SfeduSchedule.Abstractions; +using ModeusSchedule.Abstractions; namespace SfeduSchedule; @@ -17,7 +17,6 @@ public static class PluginLoader return result; foreach (var file in Directory.EnumerateFiles(pluginsDir, "*.plugin.dll", SearchOption.AllDirectories)) - { try { var path = Path.GetFullPath(file); @@ -38,19 +37,18 @@ public static class PluginLoader { Console.WriteLine($"Ошибка загрузки плагина {file}: {ex.Message}"); } - } return result; } } // Отдельный контекст загрузки для изоляции зависимостей плагина -public sealed class PluginLoadContext(string pluginMainAssemblyPath) : AssemblyLoadContext(isCollectible: true) +public sealed class PluginLoadContext(string pluginMainAssemblyPath) : AssemblyLoadContext(true) { private readonly AssemblyDependencyResolver _resolver = new(pluginMainAssemblyPath); // Разрешаем управляемые зависимости плагина из его папки. - // Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, SfeduSchedule.Abstractions). + // Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, ModeusSchedule.Abstractions). protected override Assembly? Load(AssemblyName assemblyName) { var path = _resolver.ResolveAssemblyToPath(assemblyName); diff --git a/SfeduSchedule/Program.cs b/SfeduSchedule/Program.cs index 075c8ec..624ce1a 100644 --- a/SfeduSchedule/Program.cs +++ b/SfeduSchedule/Program.cs @@ -1,41 +1,43 @@ +using System.Net; +using System.Reflection; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Authentication; -using Microsoft.Identity.Web; -using Quartz; -using SfeduSchedule; -using SfeduSchedule.Jobs; -using SfeduSchedule.Services; -using X.Extensions.Logging.Telegram.Extensions; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using SfeduSchedule.Auth; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.OpenApi.Models; +using ModeusSchedule.Abstractions; using Prometheus; +using Quartz; +using SfeduSchedule; +using SfeduSchedule.Auth; +using SfeduSchedule.Jobs; using SfeduSchedule.Middleware; +using SfeduSchedule.Services; +using X.Extensions.Logging.Telegram.Extensions; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; -string? preinstalledJwtToken = configuration["TOKEN"]; -string? tgChatId = configuration["TG_CHAT_ID"]; -string? tgToken = configuration["TG_TOKEN"]; -string updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *"; +var preinstalledJwtToken = configuration["TOKEN"]; +var tgChatId = configuration["TG_CHAT_ID"]; +var tgToken = configuration["TG_TOKEN"]; +var updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *"; // Если не указана TZ, ставим Europe/Moscow if (string.IsNullOrEmpty(configuration["TZ"])) configuration["TZ"] = "Europe/Moscow"; -int permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40; -int timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10; +var permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40; +var timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10; // создать папку data если не существует var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data"); -if (!Directory.Exists(dataDirectory)) -{ - Directory.CreateDirectory(dataDirectory); -} +if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory); -GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt"); +GlobalConsts.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt"); var pluginsPath = Path.Combine(dataDirectory, "Plugins"); builder.Logging.ClearProviders(); @@ -106,7 +108,7 @@ Console.WriteLine("Plugins count: " + loadedPlugins.Count); foreach (var p in loadedPlugins) { Console.WriteLine("Loading plugin: " + p.Instance.Name); - + // DI из плагина p.Instance.ConfigureServices(builder.Services); @@ -135,14 +137,14 @@ if (string.IsNullOrEmpty(preinstalledJwtToken)) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { - var mainXmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var mainXmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var mainXmlPath = Path.Combine(AppContext.BaseDirectory, mainXmlFile); options.IncludeXmlComments(mainXmlPath); - var pluginXmlFile = "SfeduSchedule.Abstractions.xml"; + var pluginXmlFile = "ModeusSchedule.Abstractions.xml"; var pluginXmlPath = Path.Combine(AppContext.BaseDirectory, pluginXmlFile); options.IncludeXmlComments(pluginXmlPath); - + // Добавление документации плагинов foreach (var p in loadedPlugins) { @@ -152,12 +154,13 @@ builder.Services.AddSwaggerGen(options => } // Добавляем только схему авторизации по ApiKey - options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme + options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new OpenApiSecurityScheme { - Description = $"Api Key needed to access the endpoints. {ApiKeyAuthenticationDefaults.HeaderName}: Your_API_Key", + Description = + $"Api Key needed to access the endpoints. {ApiKeyAuthenticationDefaults.HeaderName}: Your_API_Key", Name = ApiKeyAuthenticationDefaults.HeaderName, - In = Microsoft.OpenApi.Models.ParameterLocation.Header, - Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, Scheme = ApiKeyAuthenticationDefaults.Scheme }); options.OperationFilter(); @@ -167,10 +170,11 @@ builder.Services.AddRateLimiter(options => { options.AddPolicy("throttle", httpContext => RateLimitPartition.GetFixedWindowLimiter( - partitionKey: (httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString())) + httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && + !string.IsNullOrWhiteSpace(xff.ToString()) ? xff.ToString().Split(',')[0].Trim() - : (httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"), - factory: _ => new FixedWindowRateLimiterOptions + : httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + _ => new FixedWindowRateLimiterOptions { PermitLimit = permitLimit, Window = TimeSpan.FromSeconds(timeLimit) @@ -185,7 +189,8 @@ builder.Services.AddRateLimiter(options => cancellationToken); var reqLogger = context.HttpContext.RequestServices.GetRequiredService>(); - var clientIp = (context.HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString())) + var clientIp = context.HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && + !string.IsNullOrWhiteSpace(xff.ToString()) ? xff.ToString().Split(',')[0].Trim() : context.HttpContext.Connection.RemoteIpAddress?.ToString(); reqLogger.LogWarning("Rate limit exceeded for IP: {IpAddress}", clientIp); @@ -195,15 +200,15 @@ builder.Services.AddRateLimiter(options => builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto | - ForwardedHeaders.XForwardedHost; + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); - options.KnownNetworks.Add(new IPNetwork(System.Net.IPAddress.Parse("127.0.0.1"), 8)); // localhost - options.KnownNetworks.Add(new IPNetwork(System.Net.IPAddress.Parse("10.0.0.0"), 8)); // 10.x.x.x - options.KnownNetworks.Add(new IPNetwork(System.Net.IPAddress.Parse("192.168.0.0"), 16)); // 192.168.x.x - options.KnownNetworks.Add(new IPNetwork(System.Net.IPAddress.Parse("172.16.0.0"), 12)); // 172.16.x.x - 172.31.x.x + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("127.0.0.1"), 8)); // localhost + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // 10.x.x.x + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); // 192.168.x.x + options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); // 172.16.x.x - 172.31.x.x }); var app = builder.Build(); @@ -219,10 +224,10 @@ if (string.IsNullOrEmpty(preinstalledJwtToken)) var scheduler = await schedulerFactory.GetScheduler(); // Проверить существование файла jwt.txt - if (File.Exists(GlobalVariables.JwtFilePath)) + if (File.Exists(GlobalConsts.JwtFilePath)) { logger.LogInformation("Обнаружена прошлая сессия"); - var lines = await File.ReadAllLinesAsync(GlobalVariables.JwtFilePath); + var lines = await File.ReadAllLinesAsync(GlobalConsts.JwtFilePath); if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate)) { logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate); @@ -246,7 +251,9 @@ if (string.IsNullOrEmpty(preinstalledJwtToken)) } } else + { await scheduler.TriggerJob(jobKey); + } } app.UseSwagger(); @@ -271,17 +278,12 @@ app.MapGet("/", async context => app.MapControllers(); // Ограничим доступ к /metrics только локальными сетями -app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), branch => -{ - branch.UseMiddleware(); -}); +app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), + branch => { branch.UseMiddleware(); }); app.MapMetrics(); // Маршруты Minimal API из плагинов -foreach (var p in loadedPlugins) -{ - p.Instance.MapEndpoints(app); -} +foreach (var p in loadedPlugins) p.Instance.MapEndpoints(app); app.Run(); \ No newline at end of file diff --git a/SfeduSchedule/Services/ModeusService.cs b/SfeduSchedule/Services/ModeusService.cs index 2cee2d4..532bdc8 100644 --- a/SfeduSchedule/Services/ModeusService.cs +++ b/SfeduSchedule/Services/ModeusService.cs @@ -1,278 +1,279 @@ +using System.Text; using System.Text.Json; +using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; using Microsoft.Net.Http.Headers; -using SfeduSchedule.Abstractions; +using ModeusSchedule.Abstractions; +using ModeusSchedule.Abstractions.DTO; -namespace SfeduSchedule.Services +namespace SfeduSchedule.Services; + +public class ModeusService { - public class ModeusService + private readonly IConfiguration _configuration; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public ModeusService(HttpClient httpClient, ILogger logger, IConfiguration configuration) { - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; + _httpClient = httpClient; + _logger = logger; + _configuration = configuration; + _httpClient.BaseAddress = new Uri("https://sfedu.modeus.org/"); + var token = _configuration["TOKEN"]; + _httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {token}"); + } - public ModeusService(HttpClient httpClient, ILogger logger, IConfiguration configuration) + public async Task GetScheduleAsync(ModeusScheduleRequest msr) + { + var request = new HttpRequestMessage(HttpMethod.Post, + $"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}"); + request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions), + Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(request); + _logger.LogInformation("GetScheduleAsync: Ответ получен: {StatusCode}", response.StatusCode); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + public async Task> GetAttendeesAsync(Guid eventId) + { + var request = new HttpRequestMessage(HttpMethod.Get, + $"schedule-calendar-v2/api/calendar/events/{eventId}/attendees"); + var response = await _httpClient.SendAsync(request); + _logger.LogInformation("GetAttendeesAsync: Ответ получен: {StatusCode}", response.StatusCode); + response.EnsureSuccessStatusCode(); + List? attendees; + try { - _httpClient = httpClient; - _logger = logger; - _configuration = configuration; - _httpClient.BaseAddress = new Uri("https://sfedu.modeus.org/"); - var token = _configuration["TOKEN"]; - _httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {token}"); + attendees = Attendees.FromJson(await response.Content.ReadAsStringAsync()); + return attendees; + } + catch (Exception ex) + { + _logger.LogError(ex, "GetAttendeesAsync: Deserialization failed."); } - public async Task GetScheduleAsync(ModeusScheduleRequest msr) + return new List(); + } + + public async Task SearchRoomsAsync(RoomSearchRequest requestDto) + { + var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/campus/rooms/search"); + request.Content = + new StringContent(JsonSerializer.Serialize(requestDto, GlobalConsts.JsonSerializerOptions), + Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(request); + _logger.LogInformation("SearchRoomsAsync: Ответ получен: {StatusCode}", response.StatusCode); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetGuidAsync(string fullName) + { + var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/people/persons/search"); + request.Content = new StringContent(JsonSerializer.Serialize(new { - var request = new HttpRequestMessage(HttpMethod.Post, - $"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}"); - request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions), - System.Text.Encoding.UTF8, "application/json"); - var response = await _httpClient.SendAsync(request); - _logger.LogInformation("GetScheduleAsync: Ответ получен: {StatusCode}", response.StatusCode); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); + fullName, + sort = "+fullName", + size = 10, + page = 0 + }), Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(request); + + _logger.LogInformation("GetGuidAsync: Ответ получен: {StatusCode}", response.StatusCode); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + + string? personId; + try + { + personId = JsonDocument.Parse(json).RootElement + .GetProperty("_embedded") + .GetProperty("persons")[0] + .GetProperty("id") + .GetString(); } - - public async Task> GetAttendeesAsync(Guid eventId) + catch { - var request = new HttpRequestMessage(HttpMethod.Get, - $"schedule-calendar-v2/api/calendar/events/{eventId}/attendees"); - var response = await _httpClient.SendAsync(request); - _logger.LogInformation("GetAttendeesAsync: Ответ получен: {StatusCode}", response.StatusCode); - response.EnsureSuccessStatusCode(); - List? attendees; - try - { - attendees = Attendees.FromJson(await response.Content.ReadAsStringAsync()); - return attendees; - } - catch (Exception ex) - { - _logger.LogError(ex, "GetAttendeesAsync: Deserialization failed."); - } - return new List(); - } - - public async Task SearchRoomsAsync(RoomSearchRequest requestDto) - { - var request = new HttpRequestMessage(HttpMethod.Post, $"schedule-calendar-v2/api/campus/rooms/search"); - request.Content = - new StringContent(JsonSerializer.Serialize(requestDto, GlobalVariables.JsonSerializerOptions), - System.Text.Encoding.UTF8, "application/json"); - var response = await _httpClient.SendAsync(request); - _logger.LogInformation("SearchRoomsAsync: Ответ получен: {StatusCode}", response.StatusCode); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } - - public async Task GetGuidAsync(string fullName) - { - var request = new HttpRequestMessage(HttpMethod.Post, $"schedule-calendar-v2/api/people/persons/search"); - request.Content = new StringContent(JsonSerializer.Serialize(new - { - fullName = fullName, - sort = "+fullName", - size = 10, - page = 0 - }), System.Text.Encoding.UTF8, "application/json"); - var response = await _httpClient.SendAsync(request); - - _logger.LogInformation("GetGuidAsync: Ответ получен: {StatusCode}", response.StatusCode); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync(); - - string? personId; - try - { - personId = JsonDocument.Parse(json).RootElement - .GetProperty("_embedded") - .GetProperty("persons")[0] - .GetProperty("id") - .GetString(); - } - catch - { - _logger.LogWarning( - "GetGuidAsync: Не удалось получить идентификатор пользователя, {FullName}, json: {Json}", fullName, - json); - return null; - } - - return personId; - } - - public async Task GetScheduleJsonAsync(ModeusScheduleRequest msr) - { - var schedule = await GetScheduleAsync(msr); - if (schedule == null) - { - _logger.LogError("GetScheduleJsonAsync: Schedule is null. Request: {@msr}", msr); - throw new Exception("Schedule is null"); - } - - Schedule? scheduleJson; - try - { - scheduleJson = Schedule.FromJson(schedule); - switch (scheduleJson) - { - case null: - _logger.LogError( - "GetScheduleJsonAsync: scheduleJson is null. Schedule: {Schedule}\n Request: {msr}", - schedule, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - break; - case { Embedded: null }: - _logger.LogError( - "GetScheduleJsonAsync: scheduleJson.Embedded is null. Response: {@response}\nscheduleJson: {@scheduleJson}\n Request: {msr}", - schedule, scheduleJson, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - break; - case { Embedded.Events: null }: - _logger.LogError( - "GetScheduleJsonAsync: scheduleJson.Embedded.Events is null. Response: {@response}\nEmbedded: {@Embedded}\n Request: {msr}", - schedule, scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - break; - case { Embedded.Events.Length: 0 }: - _logger.LogWarning( - "GetScheduleJsonAsync: scheduleJson.Embedded.Events is empty. Embedded: {@Embedded}\n Request: {msr}", - scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - break; - default: - return scheduleJson; - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "GetScheduleJsonAsync: Deserialization failed. Schedule: {Schedule}\n Request: {msr}", schedule, - JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - } - + _logger.LogWarning( + "GetGuidAsync: Не удалось получить идентификатор пользователя, {FullName}, json: {Json}", fullName, + json); return null; } - public async Task GetIcsAsync(ModeusScheduleRequest msr) + return personId; + } + + public async Task GetScheduleJsonAsync(ModeusScheduleRequest msr) + { + var schedule = await GetScheduleAsync(msr); + if (schedule == null) { - Schedule? scheduleJson = await GetScheduleJsonAsync(msr); - if (scheduleJson == null) + _logger.LogError("GetScheduleJsonAsync: Schedule is null. Request: {@msr}", msr); + throw new Exception("Schedule is null"); + } + + Schedule? scheduleJson; + try + { + scheduleJson = Schedule.FromJson(schedule); + switch (scheduleJson) { - _logger.LogError("GetIcsAsync: scheduleJson is null after deserialization. Request: " + JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions)); - return null; + case null: + _logger.LogError( + "GetScheduleJsonAsync: scheduleJson is null. Schedule: {Schedule}\n Request: {msr}", + schedule, JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + break; + case { Embedded: null }: + _logger.LogError( + "GetScheduleJsonAsync: scheduleJson.Embedded is null. Response: {@response}\nscheduleJson: {@scheduleJson}\n Request: {msr}", + schedule, scheduleJson, JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + break; + case { Embedded.Events: null }: + _logger.LogError( + "GetScheduleJsonAsync: scheduleJson.Embedded.Events is null. Response: {@response}\nEmbedded: {@Embedded}\n Request: {msr}", + schedule, scheduleJson.Embedded, + JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + break; + case { Embedded.Events.Length: 0 }: + _logger.LogWarning( + "GetScheduleJsonAsync: scheduleJson.Embedded.Events is empty. Embedded: {@Embedded}\n Request: {msr}", + scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + break; + default: + return scheduleJson; } + } + catch (Exception ex) + { + _logger.LogError(ex, + "GetScheduleJsonAsync: Deserialization failed. Schedule: {Schedule}\n Request: {msr}", schedule, + JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + } - var calendar = new Ical.Net.Calendar(); - calendar.AddTimeZone(new VTimeZone(_configuration["TZ"]!)); + return null; + } - foreach (var e in scheduleJson.Embedded.Events) + public async Task GetIcsAsync(ModeusScheduleRequest msr) + { + var scheduleJson = await GetScheduleJsonAsync(msr); + if (scheduleJson == null) + { + _logger.LogError("GetIcsAsync: scheduleJson is null after deserialization. Request: " + + JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions)); + return null; + } + + var calendar = new Calendar(); + calendar.AddTimeZone(new VTimeZone(_configuration["TZ"]!)); + + foreach (var e in scheduleJson.Embedded.Events) + { + // Получение названия аудитории для события + string? roomName = null; + if (scheduleJson.Embedded.EventLocations != null && scheduleJson.Embedded.Rooms != null && + scheduleJson.Embedded.EventRooms != null) { - // Получение названия аудитории для события - string? roomName = null; - if (scheduleJson.Embedded.EventLocations != null && scheduleJson.Embedded.Rooms != null && - scheduleJson.Embedded.EventRooms != null) + var eventLocation = scheduleJson.Embedded.EventLocations.FirstOrDefault(el => el.EventId == e.Id); + if (eventLocation != null + && eventLocation.Links != null + && eventLocation.Links.EventRooms != null + && eventLocation.Links.EventRooms.Href != null) { - var eventLocation = scheduleJson.Embedded.EventLocations.FirstOrDefault(el => el.EventId == e.Id); - if (eventLocation != null - && eventLocation.Links != null - && eventLocation.Links.EventRooms != null - && eventLocation.Links.EventRooms.Href != null) + var eventRoomId = eventLocation.Links.EventRooms.Href.Split('/').Last(); + var EventRoom = + scheduleJson.Embedded.EventRooms.FirstOrDefault(er => + er.Id.ToString().ToLower() == eventRoomId); + if (EventRoom != null) { - var eventRoomId = eventLocation.Links.EventRooms.Href.Split('/').Last(); - var EventRoom = - scheduleJson.Embedded.EventRooms.FirstOrDefault(er => - er.Id.ToString().ToLower() == eventRoomId); - if (EventRoom != null) - { - var roomId = EventRoom.Links.Room.Href.Split('/').Last(); - var room = scheduleJson.Embedded.Rooms.FirstOrDefault(r => - r.Id.ToString().ToLower() == roomId); - if (room != null) - roomName = room.Name; - } + var roomId = EventRoom.Links.Room.Href.Split('/').Last(); + var room = scheduleJson.Embedded.Rooms.FirstOrDefault(r => + r.Id.ToString().ToLower() == roomId); + if (room != null) + roomName = room.Name; } } + } - // Получение преподавателей для события - string teachersNames = ""; - if (scheduleJson.Embedded.EventOrganizers != null && scheduleJson.Embedded.EventAttendees != null && - scheduleJson.Embedded.Persons != null) + // Получение преподавателей для события + var teachersNames = ""; + if (scheduleJson.Embedded.EventOrganizers != null && scheduleJson.Embedded.EventAttendees != null && + scheduleJson.Embedded.Persons != null) + { + // Получаем eventOrganizer + var eventOrganizers = + scheduleJson.Embedded.EventOrganizers.FirstOrDefault(eo => eo.EventId == e.Id); + if (eventOrganizers != null && + eventOrganizers.Links.EventAttendees != null) { - // Получаем eventOrganizer - var eventOrganizers = - scheduleJson.Embedded.EventOrganizers.FirstOrDefault(eo => eo.EventId == e.Id); - if (eventOrganizers != null && - eventOrganizers.Links.EventAttendees != null) - { - // Получаем eventAttendee id - // Тут может прийти массив или 1 объект - Self[] eventAttendeeIds = Array.Empty(); - if (eventOrganizers.Links.EventAttendees.Value.Self != null) - eventAttendeeIds = new[] { eventOrganizers.Links.EventAttendees.Value.Self }; - else if (eventOrganizers.Links.EventAttendees.Value.SelfArray != null) - eventAttendeeIds = eventOrganizers.Links.EventAttendees.Value.SelfArray; + // Получаем eventAttendee id + // Тут может прийти массив или 1 объект + var eventAttendeeIds = Array.Empty(); + if (eventOrganizers.Links.EventAttendees.Value.Self != null) + eventAttendeeIds = new[] { eventOrganizers.Links.EventAttendees.Value.Self }; + else if (eventOrganizers.Links.EventAttendees.Value.SelfArray != null) + eventAttendeeIds = eventOrganizers.Links.EventAttendees.Value.SelfArray; - if (eventAttendeeIds.Length > 0) + if (eventAttendeeIds.Length > 0) + foreach (var eventAttendeeId in eventAttendeeIds) { - foreach (var eventAttendeeId in eventAttendeeIds) + var attendeeId = eventAttendeeId.Href.Split('/').Last(); + // Получаем eventAttendee + var eventAttendee = + scheduleJson.Embedded.EventAttendees.FirstOrDefault(ea => + ea.Id.ToString().ToLower() == attendeeId); + if (eventAttendee != null) { - var attendeeId = eventAttendeeId.Href.Split('/').Last(); - // Получаем eventAttendee - var eventAttendee = - scheduleJson.Embedded.EventAttendees.FirstOrDefault(ea => - ea.Id.ToString().ToLower() == attendeeId); - if (eventAttendee != null) - { - var personId = eventAttendee.Links.Person.Href.Split('/').Last(); - // Получаем person - var teacher = scheduleJson.Embedded.Persons.FirstOrDefault(p => - p.Id.ToString().ToLower() == personId); - if (teacher != null) - teachersNames += (string.IsNullOrEmpty(teachersNames) ? "" : ", ") + - teacher.FullName; - } + var personId = eventAttendee.Links.Person.Href.Split('/').Last(); + // Получаем person + var teacher = scheduleJson.Embedded.Persons.FirstOrDefault(p => + p.Id.ToString().ToLower() == personId); + if (teacher != null) + teachersNames += (string.IsNullOrEmpty(teachersNames) ? "" : ", ") + + teacher.FullName; } } - } } - - // Получение короткого названия для события - string shortNameCourse = ""; - if (scheduleJson.Embedded.CourseUnitRealizations != null) - { - try - { - var courseUnitRealizationsLinks = e.Links["course-unit-realization"]; - var courseUnitRealizationId = courseUnitRealizationsLinks.Href.Split('/').Last(); - if (!string.IsNullOrEmpty(courseUnitRealizationId)) - { - var courseUnitRealization = scheduleJson.Embedded.CourseUnitRealizations - .FirstOrDefault(cu => cu.Id.ToString().ToLower() == courseUnitRealizationId); - if (courseUnitRealization != null) - shortNameCourse = courseUnitRealization.NameShort ?? ""; - } - } - catch (Exception) - { - // Ignored - } - } - - calendar.Events.Add(new CalendarEvent - { - Summary = (string.IsNullOrEmpty(shortNameCourse) ? "" : shortNameCourse + " / ") + e.Name, - Description = e.NameShort + (string.IsNullOrEmpty(roomName) ? "" : $"\nАудитория: {roomName}") + - (string.IsNullOrEmpty(teachersNames) ? "" : $"\nПреподаватели: {teachersNames}"), - Start = new CalDateTime(e.StartsAtLocal, _configuration["TZ"]!), - End = new CalDateTime(e.EndsAtLocal, _configuration["TZ"]!), - }); } - var serializer = new CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(calendar); - _logger.LogInformation("GetIcsAsync: Serialized calendar created. Length: {Length}", - serializedCalendar?.Length ?? 0); - return serializedCalendar; + // Получение короткого названия для события + var shortNameCourse = ""; + if (scheduleJson.Embedded.CourseUnitRealizations != null) + try + { + var courseUnitRealizationsLinks = e.Links["course-unit-realization"]; + var courseUnitRealizationId = courseUnitRealizationsLinks.Href.Split('/').Last(); + if (!string.IsNullOrEmpty(courseUnitRealizationId)) + { + var courseUnitRealization = scheduleJson.Embedded.CourseUnitRealizations + .FirstOrDefault(cu => cu.Id.ToString().ToLower() == courseUnitRealizationId); + if (courseUnitRealization != null) + shortNameCourse = courseUnitRealization.NameShort ?? ""; + } + } + catch (Exception) + { + // Ignored + } + + calendar.Events.Add(new CalendarEvent + { + Summary = (string.IsNullOrEmpty(shortNameCourse) ? "" : shortNameCourse + " / ") + e.Name, + Description = e.NameShort + (string.IsNullOrEmpty(roomName) ? "" : $"\nАудитория: {roomName}") + + (string.IsNullOrEmpty(teachersNames) ? "" : $"\nПреподаватели: {teachersNames}"), + Start = new CalDateTime(e.StartsAtLocal, _configuration["TZ"]!), + End = new CalDateTime(e.EndsAtLocal, _configuration["TZ"]!) + }); } + + var serializer = new CalendarSerializer(); + var serializedCalendar = serializer.SerializeToString(calendar); + _logger.LogInformation("GetIcsAsync: Serialized calendar created. Length: {Length}", + serializedCalendar?.Length ?? 0); + return serializedCalendar; } } \ No newline at end of file diff --git a/SfeduSchedule/SfeduSchedule.csproj b/SfeduSchedule/SfeduSchedule.csproj index 2f27d9c..6ecacdf 100644 --- a/SfeduSchedule/SfeduSchedule.csproj +++ b/SfeduSchedule/SfeduSchedule.csproj @@ -10,16 +10,16 @@ - - - - - - + + + + + + - + diff --git a/SfeduSchedule/SfeduSchedule.sln b/SfeduSchedule/SfeduSchedule.sln new file mode 100644 index 0000000..8ccdfd4 --- /dev/null +++ b/SfeduSchedule/SfeduSchedule.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule", "SfeduSchedule.csproj", "{E8436480-8A01-6D45-1BA4-C84E185346D1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E8436480-8A01-6D45-1BA4-C84E185346D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8436480-8A01-6D45-1BA4-C84E185346D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8436480-8A01-6D45-1BA4-C84E185346D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8436480-8A01-6D45-1BA4-C84E185346D1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {59AA22AD-DE1E-4BBF-AE10-5644E7594DCF} + EndGlobalSection +EndGlobal diff --git a/SfeduSchedule/wwwroot/index.html b/SfeduSchedule/wwwroot/index.html index f566174..63a2bf0 100644 --- a/SfeduSchedule/wwwroot/index.html +++ b/SfeduSchedule/wwwroot/index.html @@ -15,12 +15,13 @@ align-items: center; background: #fff; } + img { height: auto; } - Вот так +Вот так