diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index 10f33cc8..6bb72e99 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -139,19 +139,19 @@ else switch (function) { case "Create": - classname = "table-primary"; + classname = "table-success"; break; case "Read": - classname = "table-secondary"; + classname = "table-primary"; break; case "Update": - classname = "table-success"; + classname = "table-warning"; break; case "Delete": classname = "table-danger"; break; case "Security": - classname = "table-warning"; + classname = "table-secondary"; break; default: classname = ""; diff --git a/Oqtane.Client/Services/LogService.cs b/Oqtane.Client/Services/LogService.cs index ebe34729..1241d433 100644 --- a/Oqtane.Client/Services/LogService.cs +++ b/Oqtane.Client/Services/LogService.cs @@ -51,7 +51,7 @@ namespace Oqtane.Services log.Level = Enum.GetName(typeof(LogLevel), level); if (exception != null) { - log.Exception = JsonSerializer.Serialize(exception); + log.Exception = exception.ToString(); } log.Message = message; log.MessageTemplate = ""; diff --git a/Oqtane.Server/Infrastructure/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/HostedServiceBase.cs new file mode 100644 index 00000000..09b91572 --- /dev/null +++ b/Oqtane.Server/Infrastructure/HostedServiceBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public abstract class HostedServiceBase : IHostedService, IDisposable + { + private Task ExecutingTask; + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + private readonly IServiceScopeFactory ServiceScopeFactory; + + public HostedServiceBase(IServiceScopeFactory ServiceScopeFactory) + { + this.ServiceScopeFactory = ServiceScopeFactory; + } + + // abstract method must be overridden by job + public abstract void ExecuteJob(IServiceProvider provider); + + + protected async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + while (!stoppingToken.IsCancellationRequested) + { + // allows consumption of scoped services + using (var scope = ServiceScopeFactory.CreateScope()) + { + string JobType = Utilities.GetFullTypeName(this.GetType().AssemblyQualifiedName); + IScheduleRepository ScheduleRepository = scope.ServiceProvider.GetRequiredService(); + List schedules = ScheduleRepository.GetSchedules().ToList(); + Schedule schedule = schedules.Where(item => item.JobType == JobType).FirstOrDefault(); + if (schedule != null && schedule.IsActive) + { + ExecuteJob(scope.ServiceProvider); + } + } + + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } + catch + { + // can occur during the initial installation as there is no DBContext + } + + } + + public Task StartAsync(CancellationToken cancellationToken) + { + ExecutingTask = ExecuteAsync(CancellationTokenSource.Token); + + if (ExecutingTask.IsCompleted) + { + return ExecutingTask; + } + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken CancellationToken) + { + if (ExecutingTask == null) + { + return; + } + + try + { + CancellationTokenSource.Cancel(); + } + finally + { + await Task.WhenAny(ExecutingTask, Task.Delay(Timeout.Infinite, CancellationToken)); + } + } + + public void Dispose() + { + CancellationTokenSource.Cancel(); + } + } +} diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs index aaa10f5d..3ec24b76 100644 --- a/Oqtane.Server/Infrastructure/LogManager.cs +++ b/Oqtane.Server/Infrastructure/LogManager.cs @@ -62,11 +62,18 @@ namespace Oqtane.Infrastructure log.Level = Enum.GetName(typeof(LogLevel), Level); if (Exception != null) { - log.Exception = JsonSerializer.Serialize(Exception.ToString()); + log.Exception = Exception.ToString(); } log.Message = Message; log.MessageTemplate = ""; - log.Properties = JsonSerializer.Serialize(Args); + try + { + log.Properties = JsonSerializer.Serialize(Args); + } + catch // serialization error occurred + { + log.Properties = ""; + } Log(log); } diff --git a/Oqtane.Server/Infrastructure/TestJob.cs b/Oqtane.Server/Infrastructure/TestJob.cs new file mode 100644 index 00000000..1cabfc2d --- /dev/null +++ b/Oqtane.Server/Infrastructure/TestJob.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class TestJob : HostedServiceBase + { + public TestJob(IServiceScopeFactory ServiceScopeFactory) : base(ServiceScopeFactory) {} + + public override void ExecuteJob(IServiceProvider provider) + { + var Tenants = provider.GetRequiredService(); + foreach(Tenant tenant in Tenants.GetTenants()) + { + // is it possible to set the HttpContext so that DbContextBase will resolve properly for tenants? + } + } + } +} diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index e86c6213..ea05c552 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -44,6 +44,10 @@ namespace Oqtane.Repository moduledef = new ModuleDefinition { ModuleDefinitionName = moduledefinition.ModuleDefinitionName }; db.ModuleDefinition.Add(moduledef); db.SaveChanges(); + if (moduledefinition.Permissions != "") + { + Permissions.UpdatePermissions(SiteId, "ModuleDefinition", moduledef.ModuleDefinitionId, moduledefinition.Permissions); + } } else { @@ -123,7 +127,8 @@ namespace Oqtane.Repository ServerAssemblyName = GetProperty(properties, "ServerAssemblyName"), ControlTypeTemplate = ModuleType + "." + Constants.ActionToken + ", " + typename[1], ControlTypeRoutes = "", - AssemblyName = assembly.FullName.Split(",")[0] + AssemblyName = assembly.FullName.Split(",")[0], + Permissions = "" }; } else @@ -144,7 +149,8 @@ namespace Oqtane.Repository ServerAssemblyName = "", ControlTypeTemplate = ModuleType + "." + Constants.ActionToken + ", " + typename[1], ControlTypeRoutes = "", - AssemblyName = assembly.FullName.Split(",")[0] + AssemblyName = assembly.FullName.Split(",")[0], + Permissions = ((QualifiedModuleType.StartsWith("Oqtane.Modules.Admin.")) ? "[{\"PermissionName\":\"Utilize\",\"Permissions\":\"Administrators\"}]" : "") }; } moduledefinitions.Add(moduledefinition); diff --git a/Oqtane.Server/Scripts/Master.sql b/Oqtane.Server/Scripts/Master.sql index 7d876ab6..2d68947a 100644 --- a/Oqtane.Server/Scripts/Master.sql +++ b/Oqtane.Server/Scripts/Master.sql @@ -52,12 +52,14 @@ GO CREATE TABLE [dbo].[Schedule] ( [ScheduleId] [int] IDENTITY(1,1) NOT NULL, - [Name] [nvarchar](200) NULL, + [Name] [nvarchar](200) NOT NULL, [JobType] [nvarchar](200) NOT NULL, [Period] [int] NOT NULL, [Frequency] [char](1) NOT NULL, [StartDate] [datetime] NULL, [IsActive] [bit] NOT NULL, + [IsExecuting] [bit] NOT NULL, + [NextExecution] [datetime] NULL, [RetentionHistory] [int] NOT NULL, [CreatedBy] [nvarchar](256) NULL, [CreatedOn] [datetime] NULL, @@ -77,7 +79,6 @@ CREATE TABLE [dbo].[ScheduleLog] ( [FinishDate] [datetime] NULL, [Succeeded] [bit] NULL, [Notes] [nvarchar](max) NULL, - [NextExecution] [datetime] NULL, CONSTRAINT [PK_ScheduleLog] PRIMARY KEY CLUSTERED ( [ScheduleLogId] ASC diff --git a/Oqtane.Server/Security/ClaimsPrincipalFactory.cs b/Oqtane.Server/Security/ClaimsPrincipalFactory.cs index 21e3f913..735201d4 100644 --- a/Oqtane.Server/Security/ClaimsPrincipalFactory.cs +++ b/Oqtane.Server/Security/ClaimsPrincipalFactory.cs @@ -38,10 +38,17 @@ namespace Oqtane.Security foreach (UserRole userrole in userroles) { id.AddClaim(new Claim(options.ClaimsIdentity.RoleClaimType, userrole.Role.Name)); - // host users are admins of every site - if (userrole.Role.Name == Constants.HostRole && userroles.Where(item => item.Role.Name == Constants.AdminRole).FirstOrDefault() == null) + // host users are members of every site + if (userrole.Role.Name == Constants.HostRole) { - id.AddClaim(new Claim(options.ClaimsIdentity.RoleClaimType, Constants.AdminRole)); + if (userroles.Where(item => item.Role.Name == Constants.RegisteredRole).FirstOrDefault() == null) + { + id.AddClaim(new Claim(options.ClaimsIdentity.RoleClaimType, Constants.RegisteredRole)); + } + if (userroles.Where(item => item.Role.Name == Constants.AdminRole).FirstOrDefault() == null) + { + id.AddClaim(new Claim(options.ClaimsIdentity.RoleClaimType, Constants.AdminRole)); + } } } } diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 0b582b37..3d5a8497 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -154,10 +154,6 @@ namespace Oqtane.Server services.AddSingleton(Configuration); services.AddSingleton(); - //ServiceProvider sp = services.BuildServiceProvider(); - //var InstallationManager = sp.GetRequiredService(); - //InstallationManager.InstallPackages("Modules,Themes"); - // register transient scoped core services services.AddTransient(); services.AddTransient(); @@ -235,6 +231,21 @@ namespace Oqtane.Server } } + // dynamically register hosted services + foreach (Assembly assembly in assemblies) + { + Type[] servicetypes = assembly.GetTypes() + .Where(item => item.GetInterfaces().Contains(typeof(IHostedService))) + .ToArray(); + foreach (Type servicetype in servicetypes) + { + if (servicetype.Name != "HostedServiceBase") + { + services.AddSingleton(typeof(IHostedService), servicetype); + } + } + } + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Oqtane", Version = "v1" }); diff --git a/Oqtane.Shared/Models/Schedule.cs b/Oqtane.Shared/Models/Schedule.cs index 83dd3f27..f82dfab3 100644 --- a/Oqtane.Shared/Models/Schedule.cs +++ b/Oqtane.Shared/Models/Schedule.cs @@ -11,6 +11,8 @@ namespace Oqtane.Models public string Frequency { get; set; } public DateTime? StartDate { get; set; } public bool IsActive { get; set; } + public bool IsExecuting { get; set; } + public DateTime? NextExecution { get; set; } public int RetentionHistory { get; set; } public string CreatedBy { get; set; } diff --git a/Oqtane.Shared/Models/ScheduleLog.cs b/Oqtane.Shared/Models/ScheduleLog.cs index 3c8ac5cd..77096d9b 100644 --- a/Oqtane.Shared/Models/ScheduleLog.cs +++ b/Oqtane.Shared/Models/ScheduleLog.cs @@ -10,6 +10,5 @@ namespace Oqtane.Models public DateTime? FinishDate { get; set; } public bool? Succeeded { get; set; } public string Notes { get; set; } - public DateTime? NextExecution { get; set; } } } diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index bd741d68..2bfff132 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -70,6 +70,18 @@ namespace Oqtane.Shared } } + public static string GetFullTypeName(string fullyqualifiedtypename) + { + if (fullyqualifiedtypename.Contains(", Version=")) + { + return fullyqualifiedtypename.Substring(0, fullyqualifiedtypename.IndexOf(", Version=")); + } + else + { + return fullyqualifiedtypename; + } + } + public static string GetTypeNameLastSegment(string typename, int segment) { if (typename.Contains(","))