diff --git a/src/Application/ChannelStatuses/Queries/ChannelJobStatusItem.cs b/src/Application/ChannelStatuses/Queries/ChannelJobStatusItem.cs new file mode 100644 index 000000000..5c1cd175e --- /dev/null +++ b/src/Application/ChannelStatuses/Queries/ChannelJobStatusItem.cs @@ -0,0 +1,14 @@ +using Hippo.Application.Jobs; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +namespace Hippo.Application.ChannelStatuses.Queries; + +public class ChannelJobStatusItem +{ + [Required] + public Guid ChannelId { get; set; } + + [Required] + public JobStatus Status { get; set; } +} diff --git a/src/Application/ChannelStatuses/Queries/GetChannelStatusesQuery.cs b/src/Application/ChannelStatuses/Queries/GetChannelStatusesQuery.cs new file mode 100644 index 000000000..15a633c18 --- /dev/null +++ b/src/Application/ChannelStatuses/Queries/GetChannelStatusesQuery.cs @@ -0,0 +1,95 @@ +using Hippo.Application.Common.Exceptions; +using Hippo.Application.Common.Interfaces; +using Hippo.Application.Jobs; +using Hippo.Core.Models; +using MediatR; + +namespace Hippo.Application.ChannelStatuses.Queries; + +public class GetChannelStatusesQuery : SearchFilter, IRequest> +{ + public Guid? ChannelId { get; set; } +} + +public class GetChannelStatusesQueryHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + private readonly IJobService _jobService; + + public GetChannelStatusesQueryHandler(IApplicationDbContext context, + IJobService jobService) + { + _context = context; + _jobService = jobService; + } + + public async Task> Handle(GetChannelStatusesQuery request, CancellationToken cancellationToken) + { + List entities; + int totalItems; + + if (!request.ChannelId.HasValue) + { + (totalItems, entities) = GetPaginatedChannelsStatuses(request.Offset, request.PageSize); + } + else + { + entities = new List(); + entities.Add(GetChannelStatus(request.ChannelId.Value)); + totalItems = 1; + } + + return await Task.FromResult(new Page + { + Items = entities, + PageIndex = request.PageIndex, + PageSize = request.PageSize, + TotalItems = totalItems + }); + } + + private static JobStatus FindJobAndGetStatus(List? jobs, Guid jobId) + { + if (jobs is null) + { + return JobStatus.Unknown; + } + + var job = jobs.FirstOrDefault(job => job.Id == jobId); + + return GetJobStatus(job); + } + + private (int, List) GetPaginatedChannelsStatuses(int offset, int pageSize) + { + var jobs = _jobService.GetJobs()?.ToList(); + var totalItems = jobs?.Count ?? 0; + var paginatedChannelsStatuses = _context.Channels + .Select(c => new ChannelJobStatusItem + { + ChannelId = c.Id, + Status = FindJobAndGetStatus(jobs, c.Id), + }) + .Skip(offset) + .Take(pageSize) + .ToList(); + + return (totalItems, paginatedChannelsStatuses); + } + + private ChannelJobStatusItem GetChannelStatus(Guid channelId) + { + var job = _jobService.GetJob(channelId.ToString()); + + return new ChannelJobStatusItem + { + ChannelId = channelId, + Status = GetJobStatus(job), + }; + } + + private static JobStatus GetJobStatus(Job? job) + { + return job?.Status ?? JobStatus.Dead; + } +} diff --git a/src/Application/Channels/Commands/UpdateDesiredStatusCommand.cs b/src/Application/Channels/Commands/UpdateDesiredStatusCommand.cs new file mode 100644 index 000000000..a41b110ca --- /dev/null +++ b/src/Application/Channels/Commands/UpdateDesiredStatusCommand.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using Hippo.Application.Common.Exceptions; +using Hippo.Application.Common.Interfaces; +using Hippo.Core.Entities; +using Hippo.Core.Enums; +using Hippo.Core.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Hippo.Application.Channels.Commands; + +public class UpdateDesiredStatusCommand : IRequest +{ + [Required] + public Guid ChannelId { get; set; } + + [Required] + public DesiredStatus DesiredStatus { get; set; } +} + +public class UpdateDesiredStatusCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public UpdateDesiredStatusCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(UpdateDesiredStatusCommand request, CancellationToken cancellationToken) + { + var entity = _context.Channels + .Include(c => c.ActiveRevision) + .Include(c => c.EnvironmentVariables) + .Include(c => c.App) + .FirstOrDefault(c => c.Id == request.ChannelId); + + if (entity is null) + { + throw new NotFoundException(nameof(Channel), request.ChannelId); + } + + entity.DesiredStatus = request.DesiredStatus; + + entity.AddDomainEvent(new ModifiedEvent(entity)); + + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/Application/Channels/EventHandlers/ChannelStatusModifiedEventHandler.cs b/src/Application/Channels/EventHandlers/ChannelStatusModifiedEventHandler.cs new file mode 100644 index 000000000..de1dcfa0d --- /dev/null +++ b/src/Application/Channels/EventHandlers/ChannelStatusModifiedEventHandler.cs @@ -0,0 +1,59 @@ +using Hippo.Application.Common.Interfaces; +using Hippo.Application.Jobs; +using Hippo.Core.Entities; +using Hippo.Core.Enums; +using Hippo.Core.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Hippo.Application.Channels.EventHandlers; + +public class ChannelStatusModifiedEventHandler : INotificationHandler> +{ + private readonly ILogger _logger; + private readonly IJobService _jobService; + private readonly IApplicationDbContext _context; + + public ChannelStatusModifiedEventHandler(ILogger logger, IJobService jobService, IApplicationDbContext context) + { + _logger = logger; + _jobService = jobService; + _context = context; + } + + public Task Handle(ModifiedEvent notification, CancellationToken cancellationToken) + { + var channel = _context.Channels + .Include(c => c.ActiveRevision) + .Include(c => c.EnvironmentVariables) + .Include(c => c.App) + .First(c => c.Id == notification.Entity.Id); + + _logger.LogInformation($"Hippo Domain Event: {notification.GetType().Name}"); + + if (channel.DesiredStatus == DesiredStatus.Running) + { + if (channel.ActiveRevision is not null) + { + _logger.LogInformation($"{channel.App.Name}: Starting channel {channel.Name} at revision {channel.ActiveRevision.RevisionNumber}"); + var environmentVariables = channel.EnvironmentVariables.ToDictionary( + e => e.Key!, + e => e.Value! + ); + _jobService.StartJob(channel.Id, $"{channel.App.StorageId}/{channel.ActiveRevision.RevisionNumber}", environmentVariables, channel.Domain); + _logger.LogInformation($"Started {channel.App.Name} Channel {channel.Name} at revision {channel.ActiveRevision.RevisionNumber}"); + } + else + { + _logger.LogInformation($"Not starting {channel.App.Name} Channel {channel.Name}: no active revision"); + } + } + else if (channel.DesiredStatus == DesiredStatus.Dead) + { + _jobService.DeleteJob(channel.Id.ToString()); + } + + return Task.CompletedTask; + } +} diff --git a/src/Core/Entities/Channel.cs b/src/Core/Entities/Channel.cs index f7ea494af..5fe1f87a2 100644 --- a/src/Core/Entities/Channel.cs +++ b/src/Core/Entities/Channel.cs @@ -15,6 +15,7 @@ public class Channel : AuditableEntity public Revision? ActiveRevision { get; set; } public DateTime? LastPublishAt { get; set; } + public DesiredStatus DesiredStatus { get; set; } public Guid? CertificateId { get; set; } diff --git a/src/Core/Enums/DesiredStatus.cs b/src/Core/Enums/DesiredStatus.cs new file mode 100644 index 000000000..f2b2cac3c --- /dev/null +++ b/src/Core/Enums/DesiredStatus.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Hippo.Core.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum DesiredStatus +{ + Running = 2, + Dead = 3, +} diff --git a/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.Designer.cs b/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.Designer.cs new file mode 100644 index 000000000..f576168a5 --- /dev/null +++ b/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.Designer.cs @@ -0,0 +1,604 @@ +// +using System; +using Hippo.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hippo.Infrastructure.Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDbContext))] + [Migration("20220808121551_AddChannelDesiredStatus")] + partial class AddChannelDesiredStatus + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hippo.Core.Entities.App", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StorageId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Certificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PrivateKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Certificates"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveRevisionId") + .HasColumnType("uuid"); + + b.Property("AppId") + .HasColumnType("uuid"); + + b.Property("CertificateId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DesiredStatus") + .HasColumnType("integer"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("LastPublishAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PortId") + .HasColumnType("integer"); + + b.Property("RangeRule") + .HasColumnType("text"); + + b.Property("RevisionSelectionStrategy") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ActiveRevisionId"); + + b.HasIndex("AppId"); + + b.HasIndex("CertificateId"); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.EnvironmentVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChannelId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.ToTable("EnvironmentVariables"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("RevisionNumber") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("AppId"); + + b.ToTable("Revisions"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.RevisionComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Channel") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionId") + .HasColumnType("uuid"); + + b.Property("Route") + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RevisionId"); + + b.ToTable("RevisionComponents"); + }); + + modelBuilder.Entity("Hippo.Infrastructure.Identity.Account", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.HasOne("Hippo.Core.Entities.Revision", "ActiveRevision") + .WithMany() + .HasForeignKey("ActiveRevisionId"); + + b.HasOne("Hippo.Core.Entities.App", "App") + .WithMany("Channels") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hippo.Core.Entities.Certificate", "Certificate") + .WithMany("Channels") + .HasForeignKey("CertificateId"); + + b.Navigation("ActiveRevision"); + + b.Navigation("App"); + + b.Navigation("Certificate"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.EnvironmentVariable", b => + { + b.HasOne("Hippo.Core.Entities.Channel", "Channel") + .WithMany("EnvironmentVariables") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.HasOne("Hippo.Core.Entities.App", "App") + .WithMany("Revisions") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("App"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.RevisionComponent", b => + { + b.HasOne("Hippo.Core.Entities.Revision", "Revision") + .WithMany("Components") + .HasForeignKey("RevisionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Revision"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hippo.Core.Entities.App", b => + { + b.Navigation("Channels"); + + b.Navigation("Revisions"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Certificate", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.Navigation("EnvironmentVariables"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.Navigation("Components"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.cs b/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.cs new file mode 100644 index 000000000..697dd59a3 --- /dev/null +++ b/src/Infrastructure/Data/Migrations/Postgresql/20220808121551_AddChannelDesiredStatus.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hippo.Infrastructure.Data.Migrations.Postgresql +{ + public partial class AddChannelDesiredStatus : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DesiredStatus", + table: "Channels", + type: "integer", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DesiredStatus", + table: "Channels"); + } + } +} diff --git a/src/Infrastructure/Data/Migrations/Postgresql/PostgresqlDbContextModelSnapshot.cs b/src/Infrastructure/Data/Migrations/Postgresql/PostgresqlDbContextModelSnapshot.cs index 038c625ae..92e4e11c0 100644 --- a/src/Infrastructure/Data/Migrations/Postgresql/PostgresqlDbContextModelSnapshot.cs +++ b/src/Infrastructure/Data/Migrations/Postgresql/PostgresqlDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("ProductVersion", "6.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -112,6 +112,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedBy") .HasColumnType("text"); + b.Property("DesiredStatus") + .HasColumnType("integer"); + b.Property("Domain") .IsRequired() .HasColumnType("text"); diff --git a/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.Designer.cs b/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.Designer.cs new file mode 100644 index 000000000..524cc1ccf --- /dev/null +++ b/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.Designer.cs @@ -0,0 +1,595 @@ +// +using System; +using Hippo.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hippo.Infrastructure.Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDbContext))] + [Migration("20220808121640_AddChannelDesiredStatus")] + partial class AddChannelDesiredStatus + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.6"); + + modelBuilder.Entity("Hippo.Core.Entities.App", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Certificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Certificates"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ActiveRevisionId") + .HasColumnType("TEXT"); + + b.Property("AppId") + .HasColumnType("TEXT"); + + b.Property("CertificateId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("DesiredStatus") + .HasColumnType("INTEGER"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastPublishAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("PortId") + .HasColumnType("INTEGER"); + + b.Property("RangeRule") + .HasColumnType("TEXT"); + + b.Property("RevisionSelectionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ActiveRevisionId"); + + b.HasIndex("AppId"); + + b.HasIndex("CertificateId"); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.EnvironmentVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.ToTable("EnvironmentVariables"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("RevisionNumber") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppId"); + + b.ToTable("Revisions"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.RevisionComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Channel") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionId") + .HasColumnType("TEXT"); + + b.Property("Route") + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RevisionId"); + + b.ToTable("RevisionComponents"); + }); + + modelBuilder.Entity("Hippo.Infrastructure.Identity.Account", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.HasOne("Hippo.Core.Entities.Revision", "ActiveRevision") + .WithMany() + .HasForeignKey("ActiveRevisionId"); + + b.HasOne("Hippo.Core.Entities.App", "App") + .WithMany("Channels") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hippo.Core.Entities.Certificate", "Certificate") + .WithMany("Channels") + .HasForeignKey("CertificateId"); + + b.Navigation("ActiveRevision"); + + b.Navigation("App"); + + b.Navigation("Certificate"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.EnvironmentVariable", b => + { + b.HasOne("Hippo.Core.Entities.Channel", "Channel") + .WithMany("EnvironmentVariables") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.HasOne("Hippo.Core.Entities.App", "App") + .WithMany("Revisions") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("App"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.RevisionComponent", b => + { + b.HasOne("Hippo.Core.Entities.Revision", "Revision") + .WithMany("Components") + .HasForeignKey("RevisionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Revision"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hippo.Infrastructure.Identity.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hippo.Core.Entities.App", b => + { + b.Navigation("Channels"); + + b.Navigation("Revisions"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Certificate", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Channel", b => + { + b.Navigation("EnvironmentVariables"); + }); + + modelBuilder.Entity("Hippo.Core.Entities.Revision", b => + { + b.Navigation("Components"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.cs b/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.cs new file mode 100644 index 000000000..e65d4bc3c --- /dev/null +++ b/src/Infrastructure/Data/Migrations/Sqlite/20220808121640_AddChannelDesiredStatus.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hippo.Infrastructure.Data.Migrations.Sqlite +{ + public partial class AddChannelDesiredStatus : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DesiredStatus", + table: "Channels", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DesiredStatus", + table: "Channels"); + } + } +} diff --git a/src/Infrastructure/Data/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs b/src/Infrastructure/Data/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs index 69345567f..404e8fcdc 100644 --- a/src/Infrastructure/Data/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs +++ b/src/Infrastructure/Data/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.6"); modelBuilder.Entity("Hippo.Core.Entities.App", b => { @@ -107,6 +107,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedBy") .HasColumnType("TEXT"); + b.Property("DesiredStatus") + .HasColumnType("INTEGER"); + b.Property("Domain") .IsRequired() .HasColumnType("TEXT"); diff --git a/src/Web/Api/AccountsController.cs b/src/Web/Api/AccountsController.cs new file mode 100644 index 000000000..74d6ff8b5 --- /dev/null +++ b/src/Web/Api/AccountsController.cs @@ -0,0 +1,16 @@ +using Hippo.Application.Accounts.Commands; +using Hippo.Application.Common.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hippo.Web.Api; + +public class AccountsController : ApiControllerBase +{ + [AllowAnonymous] + [HttpPost] + public async Task> Register([FromBody] CreateAccountCommand command) + { + return await Mediator.Send(command); + } +} diff --git a/src/Web/Api/ApiControllerBase.cs b/src/Web/Api/ApiControllerBase.cs index 01ea62203..b09b1de79 100644 --- a/src/Web/Api/ApiControllerBase.cs +++ b/src/Web/Api/ApiControllerBase.cs @@ -1,5 +1,7 @@ using Hippo.Web.Filters; using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Hippo.Web.Api; @@ -9,6 +11,7 @@ namespace Hippo.Web.Api; [ApiVersion("0.1")] [ApiController] [ApiExceptionFilter] +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public abstract class ApiControllerBase : Controller { private ISender _mediator = null!; diff --git a/src/Web/Api/AppController.cs b/src/Web/Api/AppsController.cs similarity index 81% rename from src/Web/Api/AppController.cs rename to src/Web/Api/AppsController.cs index 375915d3b..7afbcf033 100644 --- a/src/Web/Api/AppController.cs +++ b/src/Web/Api/AppsController.cs @@ -7,8 +7,7 @@ namespace Hippo.Web.Api; -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class AppController : ApiControllerBase +public class AppsController : ApiControllerBase { [HttpGet] public async Task>> Index( @@ -28,14 +27,6 @@ public async Task>> Index( }); } - [HttpGet("export")] - public async Task Export() - { - var vm = await Mediator.Send(new ExportAppsQuery()); - - return File(vm.Content, vm.ContentType, vm.FileName); - } - [HttpPost] public async Task> Create([FromBody] CreateAppCommand command) { diff --git a/src/Web/Api/AccountController.cs b/src/Web/Api/AuthTokensController.cs similarity index 57% rename from src/Web/Api/AccountController.cs rename to src/Web/Api/AuthTokensController.cs index 57223ed8f..b515567c2 100644 --- a/src/Web/Api/AccountController.cs +++ b/src/Web/Api/AuthTokensController.cs @@ -1,18 +1,14 @@ using Hippo.Application.Accounts.Commands; using Hippo.Application.Common.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Hippo.Web.Api; -public class AccountController : ApiControllerBase +public class AuthTokensController : ApiControllerBase { + [AllowAnonymous] [HttpPost] - public async Task> Register([FromBody] CreateAccountCommand command) - { - return await Mediator.Send(command); - } - - [HttpPost("createtoken")] public async Task> CreateToken([FromBody] CreateTokenCommand command) { return await Mediator.Send(command); diff --git a/src/Web/Api/CertificateController.cs b/src/Web/Api/CertificatesController.cs similarity index 81% rename from src/Web/Api/CertificateController.cs rename to src/Web/Api/CertificatesController.cs index a5595b2b7..aa1da5d9b 100644 --- a/src/Web/Api/CertificateController.cs +++ b/src/Web/Api/CertificatesController.cs @@ -7,8 +7,7 @@ namespace Hippo.Web.Api; -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class CertificateController : ApiControllerBase +public class CertificatesController : ApiControllerBase { [HttpGet] public async Task>> Index( @@ -28,14 +27,6 @@ public async Task>> Index( }); } - [HttpGet("export")] - public async Task Export() - { - var vm = await Mediator.Send(new ExportCertificatesQuery()); - - return File(vm.Content, vm.ContentType, vm.FileName); - } - [HttpPost] public async Task> Create([FromBody] CreateCertificateCommand command) { diff --git a/src/Web/Api/ChannelStatusesController.cs b/src/Web/Api/ChannelStatusesController.cs new file mode 100644 index 000000000..65cf07bea --- /dev/null +++ b/src/Web/Api/ChannelStatusesController.cs @@ -0,0 +1,24 @@ +using Hippo.Application.ChannelStatuses.Queries; +using Hippo.Core.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hippo.Web.Api; + +public class ChannelStatusesController : ApiControllerBase +{ + [HttpGet] + public async Task>> Index( + [FromQuery] int pageIndex = 0, + [FromQuery] int pageSize = int.MaxValue, + [FromQuery] Guid? channelId = null) + { + return await Mediator.Send(new GetChannelStatusesQuery() + { + PageIndex = pageIndex, + PageSize = pageSize, + ChannelId = channelId + }); + } +} diff --git a/src/Web/Api/ChannelController.cs b/src/Web/Api/ChannelsController.cs similarity index 79% rename from src/Web/Api/ChannelController.cs rename to src/Web/Api/ChannelsController.cs index e75a6574d..292b938d6 100644 --- a/src/Web/Api/ChannelController.cs +++ b/src/Web/Api/ChannelsController.cs @@ -1,15 +1,11 @@ using Hippo.Application.Channels.Commands; using Hippo.Application.Channels.Queries; -using Hippo.Application.EnvironmentVariables.Commands; using Hippo.Core.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Hippo.Web.Api; -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class ChannelController : ApiControllerBase +public class ChannelsController : ApiControllerBase { [HttpGet] public async Task>> Index( @@ -35,14 +31,6 @@ public async Task> GetChannel(Guid id) return await Mediator.Send(new GetChannelQuery { Id = id }); } - [HttpGet("export")] - public async Task Export() - { - var vm = await Mediator.Send(new ExportChannelsQuery()); - - return File(vm.Content, vm.ContentType, vm.FileName); - } - [HttpPost] public async Task> Create([FromBody] CreateChannelCommand command) { @@ -62,6 +50,19 @@ public async Task Update(Guid id, UpdateChannelCommand command) return NoContent(); } + [HttpPut("{channelId}/desired-status")] + public async Task UpdateDesiredStatus(Guid channelId, UpdateDesiredStatusCommand command) + { + if (channelId != command.ChannelId) + { + return BadRequest(); + } + + await Mediator.Send(command); + + return NoContent(); + } + [HttpPatch("{id}")] public async Task Patch([FromRoute] Guid id, [FromBody] PatchChannelCommand command) @@ -80,7 +81,7 @@ public async Task Delete(Guid id) return NoContent(); } - [HttpGet("logs/{id}")] + [HttpGet("{id}/logs")] public async Task Logs([FromRoute] Guid id) { return await Mediator.Send(new GetChannelLogsQuery(id)); diff --git a/src/Web/Api/JobStatusController.cs b/src/Web/Api/JobStatusController.cs deleted file mode 100644 index a89788c57..000000000 --- a/src/Web/Api/JobStatusController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Hippo.Application.Channels.Queries; -using Hippo.Core.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Hippo.Web.Api; - -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class JobStatusController : ApiControllerBase -{ - [HttpGet] - public async Task>> Index( - [FromQuery] int pageIndex = 0, - [FromQuery] int pageSize = int.MaxValue) - { - return await Mediator.Send(new GetJobStatusesQuery() - { - PageIndex = pageIndex, - PageSize = pageSize, - }); - } - - [HttpGet("{channelId}")] - public async Task> GetJobStatus(Guid channelId) - { - return await Mediator.Send(new GetJobStatusQuery - { - ChannelId = channelId, - }); - } -} diff --git a/src/Web/Api/RevisionController.cs b/src/Web/Api/RevisionsController.cs similarity index 67% rename from src/Web/Api/RevisionController.cs rename to src/Web/Api/RevisionsController.cs index 9be3cbac9..0a1cd1cc0 100644 --- a/src/Web/Api/RevisionController.cs +++ b/src/Web/Api/RevisionsController.cs @@ -1,14 +1,11 @@ using Hippo.Application.Revisions.Commands; using Hippo.Application.Revisions.Queries; using Hippo.Core.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Hippo.Web.Api; -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class RevisionController : ApiControllerBase +public class RevisionsController : ApiControllerBase { [HttpGet] public async Task>> Index( @@ -22,14 +19,6 @@ public async Task>> Index( }); } - [HttpGet("export")] - public async Task Export() - { - var vm = await Mediator.Send(new ExportRevisionsQuery()); - - return File(vm.Content, vm.ContentType, vm.FileName); - } - [HttpPost] public async Task Create([FromBody] RegisterRevisionCommand command) { diff --git a/src/Web/Api/StorageController.cs b/src/Web/Api/StoragesController.cs similarity index 73% rename from src/Web/Api/StorageController.cs rename to src/Web/Api/StoragesController.cs index d96f930b6..bee031bab 100644 --- a/src/Web/Api/StorageController.cs +++ b/src/Web/Api/StoragesController.cs @@ -1,13 +1,10 @@ using Hippo.Application.Revisions.Queries; using Hippo.Core.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Hippo.Web.Api; -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class StorageController : ApiControllerBase +public class StoragesController : ApiControllerBase { [HttpGet] public async Task> QueryStorages( diff --git a/src/Web/Helpers/ApiControllerNameConvention.cs b/src/Web/Helpers/ApiControllerNameConvention.cs new file mode 100644 index 000000000..3a1c5a1a2 --- /dev/null +++ b/src/Web/Helpers/ApiControllerNameConvention.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using System.Text; + +namespace Hippo.Web.Helpers; + +public class ApiControllerNameConvention : IControllerModelConvention +{ + public void Apply(ControllerModel controller) + { + var sb = new StringBuilder(); + foreach (var c in controller.ControllerName) + { + if (char.IsUpper(c)) + sb.Append("-"); + sb.Append(char.ToLower(c)); + } + + controller.ControllerName = sb.ToString().TrimStart('-'); + } +} \ No newline at end of file diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 67de36fff..78b510df7 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -7,6 +7,7 @@ using Hippo.Infrastructure.HealthChecks; using Hippo.Infrastructure.Identity; using Hippo.Web.Extensions; +using Hippo.Web.Helpers; using Hippo.Web.Services; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -46,6 +47,11 @@ builder.Services.Configure(options => options.SuppressModelStateInvalidFilter = true); +builder.Services.AddMvc(options => +{ + options.Conventions.Add(new ApiControllerNameConvention()); +}); + builder.Services.AddAuthentication().AddCookie().AddJwtBearer(cfg => { cfg.TokenValidationParameters = new TokenValidationParameters