From 057683d97ad7fe9afdc3a1ed6f737903f9a81af8 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 28 Sep 2017 19:53:30 +0100 Subject: [PATCH] Fixed the issue of it showing as not requested when we find it in Radarr. Made the tv shows match a bit more to the movie requests Added the ability for plex and emby users to login Improved the welcome email, will only show for users that have not logged in Fixed discord notifications the about screen now checks if there is an update ready #1513 --- src/Ombi.Api.Discord/DiscordApi.cs | 4 +- .../Authentication/OmbiUserManager.cs | 124 +++ src/Ombi.Core/Engine/BaseMediaEngine.cs | 3 +- src/Ombi.Core/Engine/Interfaces/BaseEngine.cs | 5 +- src/Ombi.Core/Engine/MovieRequestEngine.cs | 5 +- src/Ombi.Core/Engine/MovieSearchEngine.cs | 3 +- src/Ombi.Core/Engine/TvRequestEngine.cs | 3 +- src/Ombi.Core/Engine/TvSearchEngine.cs | 3 +- .../Requests}/IRequestServiceMain.cs | 0 src/Ombi.Core/Models/UI/UserViewModel.cs | 6 +- .../Jobs/Plex/PlexUserImporter.cs | 19 +- src/Ombi.Store/Context/OmbiContext.cs | 10 +- src/Ombi.Store/Entities/OmbiUser.cs | 6 +- .../20170928150420_LastLoggedIn.Designer.cs | 731 ++++++++++++++++++ .../Migrations/20170928150420_LastLoggedIn.cs | 25 + .../Migrations/OmbiContextModelSnapshot.cs | 2 + src/Ombi/ClientApp/app/interfaces/IUser.ts | 3 +- .../app/search/moviesearch.component.html | 2 +- .../app/settings/about/about.component.html | 2 +- .../app/settings/about/about.component.ts | 12 +- .../app/settings/radarr/radarr.component.ts | 2 +- .../usermanagement.component.html | 2 +- .../usermanagement-add.component.ts | 3 +- .../usermanagement-edit.component.html | 2 +- .../usermanagement-edit.component.ts | 2 +- .../usermanagement.component.html | 8 +- .../usermanagement.component.ts | 10 +- src/Ombi/ClientApp/styles/base.scss | 116 +-- src/Ombi/Controllers/IdentityController.cs | 8 +- src/Ombi/Controllers/TokenController.cs | 8 +- src/Ombi/Program.cs | 14 +- src/Ombi/Startup.cs | 4 +- 32 files changed, 1039 insertions(+), 108 deletions(-) create mode 100644 src/Ombi.Core/Authentication/OmbiUserManager.cs rename src/Ombi.Core/{Requests/Models => Models/Requests}/IRequestServiceMain.cs (100%) create mode 100644 src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.Designer.cs create mode 100644 src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.cs diff --git a/src/Ombi.Api.Discord/DiscordApi.cs b/src/Ombi.Api.Discord/DiscordApi.cs index 0300ee2e2..7bbb9350c 100644 --- a/src/Ombi.Api.Discord/DiscordApi.cs +++ b/src/Ombi.Api.Discord/DiscordApi.cs @@ -11,12 +11,12 @@ namespace Ombi.Api.Discord Api = api; } - private string Endpoint => "https://discordapp.com/api/"; + private const string BaseUrl = "https://discordapp.com/api/"; private IApi Api { get; } public async Task SendMessage(DiscordWebhookBody body, string webhookId, string webhookToken) { - var request = new Request(Endpoint, $"webhooks/{webhookId}/{webhookToken}", HttpMethod.Post); + var request = new Request($"webhooks/{webhookId}/{webhookToken}", BaseUrl, HttpMethod.Post); request.AddJsonBody(body); diff --git a/src/Ombi.Core/Authentication/OmbiUserManager.cs b/src/Ombi.Core/Authentication/OmbiUserManager.cs new file mode 100644 index 000000000..6a51cca6e --- /dev/null +++ b/src/Ombi.Core/Authentication/OmbiUserManager.cs @@ -0,0 +1,124 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: OmbiUserManager.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ombi.Api.Emby; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Store.Entities; + +namespace Ombi.Core.Authentication +{ + public class OmbiUserManager : UserManager + { + public OmbiUserManager(IUserStore store, IOptions optionsAccessor, + IPasswordHasher passwordHasher, IEnumerable> userValidators, + IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IPlexApi plexApi, + IEmbyApi embyApi, ISettingsService embySettings) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + _plexApi = plexApi; + _embyApi = embyApi; + _embySettings = embySettings; + } + + private readonly IPlexApi _plexApi; + private readonly IEmbyApi _embyApi; + private readonly ISettingsService _embySettings; + + public override async Task CheckPasswordAsync(OmbiUser user, string password) + { + if (user.UserType == UserType.LocalUser) + { + return await base.CheckPasswordAsync(user, password); + } + if (user.UserType == UserType.PlexUser) + { + return await CheckPlexPasswordAsync(user, password); + } + if (user.UserType == UserType.EmbyUser) + { + return await CheckEmbyPasswordAsync(user, password); + } + return false; + } + + /// + /// Sign the user into plex and make sure we can get the authentication token. + /// We do not check if the user is in the owners "friends" since they must have a local user account to get this far + /// + /// + /// + /// + private async Task CheckPlexPasswordAsync(OmbiUser user, string password) + { + var result = await _plexApi.SignIn(new UserRequest { password = password, login = user.UserName }); + if (result.user?.authentication_token != null) + { + return true; + } + return false; + } + + /// + /// Sign the user into Emby + /// We do not check if the user is in the owners "friends" since they must have a local user account to get this far. + /// We also have to try and authenticate them with every server, the first server that work we just say it was a success + /// + /// + /// + /// + private async Task CheckEmbyPasswordAsync(OmbiUser user, string password) + { + var embySettings = await _embySettings.GetSettingsAsync(); + foreach (var server in embySettings.Servers) + { + try + { + var result = await _embyApi.LogIn(user.UserName, password, server.ApiKey, server.FullUri); + if (result != null) + { + return true; + } + } + catch (Exception e) + { + Logger.LogError(e, "Emby Login Failed"); + } + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index b04cf9e5a..7cd252304 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -13,6 +13,7 @@ using Ombi.Store.Repository; using Ombi.Store.Repository.Requests; using Ombi.Store.Entities; using Microsoft.AspNetCore.Identity; +using Ombi.Core.Authentication; namespace Ombi.Core.Engine { @@ -23,7 +24,7 @@ namespace Ombi.Core.Engine private Dictionary _dbTv; protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService, - IRuleEvaluator rules, UserManager um) : base(identity, um, rules) + IRuleEvaluator rules, OmbiUserManager um) : base(identity, um, rules) { RequestService = requestService; } diff --git a/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs b/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs index 6a67b951f..32f08e2b1 100644 --- a/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs @@ -9,12 +9,13 @@ using Ombi.Store.Entities; using Microsoft.AspNetCore.Identity; using System.Linq; using Microsoft.EntityFrameworkCore; +using Ombi.Core.Authentication; namespace Ombi.Core.Engine.Interfaces { public abstract class BaseEngine { - protected BaseEngine(IPrincipal user, UserManager um, IRuleEvaluator rules) + protected BaseEngine(IPrincipal user, OmbiUserManager um, IRuleEvaluator rules) { UserPrinciple = user; Rules = rules; @@ -23,7 +24,7 @@ namespace Ombi.Core.Engine.Interfaces protected IPrincipal UserPrinciple { get; } protected IRuleEvaluator Rules { get; } - protected UserManager UserManager { get; } + protected OmbiUserManager UserManager { get; } protected string Username => UserPrinciple.Identity.Name; private OmbiUser _user; diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index fe31bdf19..e2652edeb 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Ombi.Core.Authentication; using Ombi.Core.Engine.Interfaces; using Ombi.Core.Rule.Interfaces; using Ombi.Store.Entities.Requests; @@ -21,8 +22,8 @@ namespace Ombi.Core.Engine public class MovieRequestEngine : BaseMediaEngine, IMovieRequestEngine { public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, - INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, - UserManager manager) : base(user, requestService, r, manager) + INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, + OmbiUserManager manager) : base(user, requestService, r, manager) { MovieApi = movieApi; NotificationHelper = helper; diff --git a/src/Ombi.Core/Engine/MovieSearchEngine.cs b/src/Ombi.Core/Engine/MovieSearchEngine.cs index 033efcaf9..ce986a5f0 100644 --- a/src/Ombi.Core/Engine/MovieSearchEngine.cs +++ b/src/Ombi.Core/Engine/MovieSearchEngine.cs @@ -15,13 +15,14 @@ using Ombi.Core.Rule.Interfaces; using StackExchange.Profiling; using Ombi.Store.Entities; using Microsoft.AspNetCore.Identity; +using Ombi.Core.Authentication; namespace Ombi.Core.Engine { public class MovieSearchEngine : BaseMediaEngine, IMovieEngine { public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, - ILogger logger, IRuleEvaluator r, UserManager um) + ILogger logger, IRuleEvaluator r, OmbiUserManager um) : base(identity, service, r, um) { MovieApi = movApi; diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 0f5502f27..1c25bdec4 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -11,6 +11,7 @@ using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Ombi.Core.Authentication; using Ombi.Core.Engine.Interfaces; using Ombi.Core.Helpers; using Ombi.Core.Rule; @@ -24,7 +25,7 @@ namespace Ombi.Core.Engine { public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, INotificationHelper helper, IMapper map, - IRuleEvaluator rule, UserManager manager, + IRuleEvaluator rule, OmbiUserManager manager, ITvSender sender, IAuditRepository audit) : base(user, requestService, rule, manager) { TvApi = tvApi; diff --git a/src/Ombi.Core/Engine/TvSearchEngine.cs b/src/Ombi.Core/Engine/TvSearchEngine.cs index 4eee6a74f..44559cbf6 100644 --- a/src/Ombi.Core/Engine/TvSearchEngine.cs +++ b/src/Ombi.Core/Engine/TvSearchEngine.cs @@ -20,13 +20,14 @@ using Ombi.Store.Repository.Requests; using Ombi.Store.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Ombi.Core.Authentication; namespace Ombi.Core.Engine { public class TvSearchEngine : BaseMediaEngine, ITvSearchEngine { public TvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService plexSettings, - ISettingsService embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, UserManager um) + ISettingsService embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um) : base(identity, service, r, um) { TvMazeApi = tvMaze; diff --git a/src/Ombi.Core/Requests/Models/IRequestServiceMain.cs b/src/Ombi.Core/Models/Requests/IRequestServiceMain.cs similarity index 100% rename from src/Ombi.Core/Requests/Models/IRequestServiceMain.cs rename to src/Ombi.Core/Models/Requests/IRequestServiceMain.cs diff --git a/src/Ombi.Core/Models/UI/UserViewModel.cs b/src/Ombi.Core/Models/UI/UserViewModel.cs index 705332e4a..87ad7a97f 100644 --- a/src/Ombi.Core/Models/UI/UserViewModel.cs +++ b/src/Ombi.Core/Models/UI/UserViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Ombi.Core.Models.UI { @@ -10,7 +11,8 @@ namespace Ombi.Core.Models.UI public List Claims { get; set; } public string EmailAddress { get; set; } public string Password { get; set; } - public bool IsSetup { get; set; } + public DateTime? LastLoggedIn { get; set; } + public bool HasLoggedIn { get; set; } public UserType UserType { get; set; } } diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs index 52f79332f..64de7fb67 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexUserImporter.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -53,10 +54,10 @@ namespace Ombi.Schedule.Jobs.Plex var users = await _api.GetUsers(server.PlexAuthToken); - foreach (var plexUsers in users.User) + foreach (var plexUser in users.User) { // Check if we should import this user - if (userManagementSettings.BannedPlexUserIds.Contains(plexUsers.Id)) + if (userManagementSettings.BannedPlexUserIds.Contains(plexUser.Id)) { // Do not import these, they are not allowed into the country. continue; @@ -64,7 +65,7 @@ namespace Ombi.Schedule.Jobs.Plex // Check if this Plex User already exists // We are using the Plex USERNAME and Not the TITLE, the Title is for HOME USERS - var existingPlexUser = allUsers.FirstOrDefault(x => x.ProviderUserId == plexUsers.Id); + var existingPlexUser = allUsers.FirstOrDefault(x => x.ProviderUserId == plexUser.Id); if (existingPlexUser == null) { // Create this users @@ -72,9 +73,9 @@ namespace Ombi.Schedule.Jobs.Plex var newUser = new OmbiUser { UserType = UserType.PlexUser, - UserName = plexUsers.Username, - ProviderUserId = plexUsers.Id, - Email = plexUsers.Email, + UserName = plexUser.Username, + ProviderUserId = plexUser.Id, + Email = plexUser.Email, Alias = string.Empty }; var result = await _userManager.CreateAsync(newUser); @@ -97,6 +98,10 @@ namespace Ombi.Schedule.Jobs.Plex else { // Do we need to update this user? + existingPlexUser.Email = plexUser.Email; + existingPlexUser.UserName = plexUser.Username; + + await _userManager.UpdateAsync(existingPlexUser); } } } diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 68539aa42..257e6a3c8 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -100,13 +100,13 @@ namespace Ombi.Store.Context foreach (var agent in allAgents) { - if (templates.Any(x => x.Agent == agent)) - { - // We have all the templates for this notification agent - continue; - } foreach (var notificationType in allTypes) { + if (templates.Any(x => x.Agent == agent && x.NotificationType == notificationType)) + { + // We already have this + continue; + } NotificationTemplates notificationToAdd; switch (notificationType) { diff --git a/src/Ombi.Store/Entities/OmbiUser.cs b/src/Ombi.Store/Entities/OmbiUser.cs index 24353ecfc..42dce2fe7 100644 --- a/src/Ombi.Store/Entities/OmbiUser.cs +++ b/src/Ombi.Store/Entities/OmbiUser.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System; +using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNetCore.Identity; namespace Ombi.Store.Entities @@ -7,11 +8,14 @@ namespace Ombi.Store.Entities { public string Alias { get; set; } public UserType UserType { get; set; } + /// /// This will be the unique Plex/Emby user id reference /// public string ProviderUserId { get; set; } + public DateTime? LastLoggedIn { get; set; } + [NotMapped] public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias; } diff --git a/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.Designer.cs b/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.Designer.cs new file mode 100644 index 000000000..2cb6dbcc5 --- /dev/null +++ b/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.Designer.cs @@ -0,0 +1,731 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Ombi.Helpers; +using Ombi.Store.Context; +using Ombi.Store.Entities; +using System; + +namespace Ombi.Store.Migrations +{ + [DbContext(typeof(OmbiContext))] + [Migration("20170928150420_LastLoggedIn")] + partial class LastLoggedIn + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ProviderId"); + + b.Property("Title"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("UserType"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("Key"); + + b.Property("ProviderId"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("Title"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueId"); + + b.Property("MovieId"); + + b.Property("Subect"); + + b.HasKey("Id"); + + b.HasIndex("IssueId"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieIssues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueId"); + + b.Property("Subect"); + + b.Property("TvId"); + + b.HasKey("Id"); + + b.HasIndex("IssueId"); + + b.HasIndex("TvId"); + + b.ToTable("TvIssues"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexContent") + .WithMany("Seasons") + .HasForeignKey("PlexContentId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest") + .WithMany("ChildRequests") + .HasForeignKey("ParentRequestId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.MovieRequests", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") + .WithMany() + .HasForeignKey("RequestedUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests") + .WithMany("Issues") + .HasForeignKey("IssueId"); + + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "Child") + .WithMany() + .HasForeignKey("TvId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest") + .WithMany("SeasonRequests") + .HasForeignKey("ChildRequestId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.cs b/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.cs new file mode 100644 index 000000000..cf393c7a5 --- /dev/null +++ b/src/Ombi.Store/Migrations/20170928150420_LastLoggedIn.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations +{ + public partial class LastLoggedIn : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastLoggedIn", + table: "AspNetUsers", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastLoggedIn", + table: "AspNetUsers"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs index 9c913fa59..1a46eb954 100644 --- a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs @@ -260,6 +260,8 @@ namespace Ombi.Store.Migrations b.Property("EmailConfirmed"); + b.Property("LastLoggedIn"); + b.Property("LockoutEnabled"); b.Property("LockoutEnd"); diff --git a/src/Ombi/ClientApp/app/interfaces/IUser.ts b/src/Ombi/ClientApp/app/interfaces/IUser.ts index 5e42afafe..fae6a4bc1 100644 --- a/src/Ombi/ClientApp/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/app/interfaces/IUser.ts @@ -8,7 +8,8 @@ export interface IUser { emailAddress: string; password: string; userType: UserType; - isSetup: boolean; + lastLoggedIn: Date; + hasLoggedIn: boolean; // FOR UI checked: boolean; } diff --git a/src/Ombi/ClientApp/app/search/moviesearch.component.html b/src/Ombi/ClientApp/app/search/moviesearch.component.html index 8851b09b9..663b5bb34 100644 --- a/src/Ombi/ClientApp/app/search/moviesearch.component.html +++ b/src/Ombi/ClientApp/app/search/moviesearch.component.html @@ -54,7 +54,7 @@ Available Processing Request Pending Approval - Not Requested + Not Requested diff --git a/src/Ombi/ClientApp/app/settings/about/about.component.html b/src/Ombi/ClientApp/app/settings/about/about.component.html index 5893a39d5..9ac67c87b 100644 --- a/src/Ombi/ClientApp/app/settings/about/about.component.html +++ b/src/Ombi/ClientApp/app/settings/about/about.component.html @@ -14,7 +14,7 @@ Version - {{about.version}} + {{about.version}} (New Update Available) diff --git a/src/Ombi/ClientApp/app/settings/about/about.component.ts b/src/Ombi/ClientApp/app/settings/about/about.component.ts index cd4cc83af..8061af057 100644 --- a/src/Ombi/ClientApp/app/settings/about/about.component.ts +++ b/src/Ombi/ClientApp/app/settings/about/about.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { IAbout } from "../../interfaces/ISettings"; -import { SettingsService } from "../../services"; +import { JobService, SettingsService } from "../../services"; @Component({ templateUrl: "./about.component.html", @@ -8,10 +8,18 @@ import { SettingsService } from "../../services"; export class AboutComponent implements OnInit { public about: IAbout; + public newUpdate: boolean; - constructor(private settingsService: SettingsService) { } + constructor(private readonly settingsService: SettingsService, + private readonly jobService: JobService) { } public ngOnInit() { this.settingsService.about().subscribe(x => this.about = x); + this.jobService.checkForNewUpdate().subscribe(x => { + if (x === true) { + this.newUpdate = true; + } + }); + } } diff --git a/src/Ombi/ClientApp/app/settings/radarr/radarr.component.ts b/src/Ombi/ClientApp/app/settings/radarr/radarr.component.ts index 8487cf80a..198b6e060 100644 --- a/src/Ombi/ClientApp/app/settings/radarr/radarr.component.ts +++ b/src/Ombi/ClientApp/app/settings/radarr/radarr.component.ts @@ -93,7 +93,7 @@ export class RadarrComponent implements OnInit { } const settings = form.value; this.testerService.radarrTest(settings).subscribe(x => { - if (x) { + if (x === true) { this.notificationService.success("Connected", "Successfully connected to Radarr!"); } else { this.notificationService.error("Connected", "We could not connect to Radarr!"); diff --git a/src/Ombi/ClientApp/app/settings/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/app/settings/usermanagement/usermanagement.component.html index b1b2b9572..0dc11ca92 100644 --- a/src/Ombi/ClientApp/app/settings/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/app/settings/usermanagement/usermanagement.component.html @@ -23,7 +23,7 @@
- +
diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts index 3f7b4a3af..5100f675e 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-add.component.ts @@ -27,7 +27,8 @@ export class UserManagementAddComponent implements OnInit { username: "", userType: UserType.LocalUser, checked:false, - isSetup:false, + hasLoggedIn: false, + lastLoggedIn:new Date(), }; } diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html index 31cf6cd2f..a4fec0fd0 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.html @@ -27,7 +27,7 @@
- +
diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts index 715f17569..e16d1bb06 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement-edit.component.ts @@ -18,7 +18,7 @@ export class UserManagementEditComponent { private notificationSerivce: NotificationService, private router: Router) { this.route.params - .subscribe(params => { + .subscribe((params: any) => { this.userId = params.id; this.identityService.getUserById(this.userId).subscribe(x => { diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html index bb5df3647..265548636 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html @@ -47,6 +47,9 @@ Roles + + Last Logged In + User Type @@ -74,6 +77,9 @@ + + {{u.lastLoggedIn | date: 'short'}} + Local User Plex User @@ -83,7 +89,7 @@ Details/Edit - Send Welcome Email + diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts index 2149884be..144dc42be 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from "@angular/core"; -import { IEmailNotificationSettings, IUser } from "../interfaces"; +import { ICustomizationSettings, IEmailNotificationSettings, IUser } from "../interfaces"; import { IdentityService, NotificationService, SettingsService } from "../services"; @Component({ @@ -11,10 +11,11 @@ export class UserManagementComponent implements OnInit { public users: IUser[]; public checkAll = false; public emailSettings: IEmailNotificationSettings; + public customizationSettings: ICustomizationSettings; - constructor(private identityService: IdentityService, - private settingsService: SettingsService, - private notificationService: NotificationService) { } + constructor(private readonly identityService: IdentityService, + private readonly settingsService: SettingsService, + private readonly notificationService: NotificationService) { } public ngOnInit() { this.users = []; @@ -22,6 +23,7 @@ export class UserManagementComponent implements OnInit { this.users = x; }); + this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); this.settingsService.getEmailNotificationSettings().subscribe(x => this.emailSettings = x); } diff --git a/src/Ombi/ClientApp/styles/base.scss b/src/Ombi/ClientApp/styles/base.scss index ea3c6f02b..e1491e0b0 100644 --- a/src/Ombi/ClientApp/styles/base.scss +++ b/src/Ombi/ClientApp/styles/base.scss @@ -26,7 +26,7 @@ $i:!important; } } -@media (max-width: 48em) { +@media (max-width: 48em) { .home { padding-top: 1rem; } @@ -97,20 +97,20 @@ label { margin-bottom: .5rem $i; font-size: 16px $i; } + .small-label { display: inline-block $i; margin-bottom: .5rem $i; font-size: 11px $i; } -.small-checkbox{ - min-height:0 $i; - +.small-checkbox { + min-height: 0 $i; } .round-checkbox { - border-radius:8px; + border-radius: 8px; } .nav-tabs > li { @@ -428,7 +428,7 @@ $border-radius: 10px; bottom: 1px; border: 2px solid #eee; border-radius: 8px; - min-height:0px $i; + min-height: 0px $i; } .small-checkbox input[type=checkbox] { @@ -444,11 +444,11 @@ $border-radius: 10px; } .small-checkbox label { - min-height: 0 $i; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; + min-height: 0 $i; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; } .input-group-sm { @@ -517,6 +517,7 @@ $border-radius: 10px; -webkit-box-shadow: 3px 3px 5px 6px #191919; box-shadow: 3px 3px 5px 6px #191919; } + .img-circle { border-radius: 50%; } @@ -542,7 +543,7 @@ $border-radius: 10px; margin-right: -250px; overflow-y: auto; background: #4e5d6c; - padding-left:0; + padding-left: 0; -webkit-transition: all 0.5s ease; -moz-transition: all 0.5s ease; -o-transition: all 0.5s ease; @@ -641,61 +642,60 @@ $border-radius: 10px; } #lightbox { - background-color: grey; - filter:alpha(opacity=50); /* IE */ + filter: alpha(opacity=50); /* IE */ opacity: 0.5; /* Safari, Opera */ - -moz-opacity:0.50; /* FireFox */ + -moz-opacity: 0.50; /* FireFox */ top: 0px; left: 0px; z-index: 20; height: 100%; width: 100%; - background-repeat:no-repeat; - background-position:center; - position:absolute; + background-repeat: no-repeat; + background-position: center; + position: absolute; } .list-group-item-dropdown { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #3e3e3e; - border: 1px solid transparent; + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #3e3e3e; + border: 1px solid transparent; } -.wizard-heading{ +.wizard-heading { text-align: center; } -.wizard-img{ + +.wizard-img { width: 300px; display: block $i; margin: 0 auto $i; } .pace { - -webkit-pointer-events: none; - pointer-events: none; - - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } .pace-inactive { - display: none; + display: none; } .pace .pace-progress { - background: $primary-colour; - position: fixed; - z-index: 2000; - top: 0; - right: 100%; - width: 100%; - height: 5px; + background: $primary-colour; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 5px; } .navbar-brand { @@ -705,8 +705,8 @@ $border-radius: 10px; height: 40px; } -.gravatar{ - border-radius:1em; +.gravatar { + border-radius: 1em; } @@ -716,6 +716,7 @@ html { font-size: 16px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; @@ -727,6 +728,7 @@ body { .ui-datatable-odd { background-color: $form-color $i; } + .ui-datatable-even { background-color: $form-color-lighter $i; } @@ -742,8 +744,9 @@ body { border-bottom: 1px solid transparent; background: $form-color; } -.card-header > a{ - color:white; + +.card-header > a { + color: white; } @@ -758,7 +761,7 @@ textarea { .poster { box-shadow: 5px 5px 30px #000000; - border-radius: 30px; + border-radius: 30px; } @@ -776,9 +779,10 @@ textarea { box-shadow: none; } -.ui-state-default.ui-unselectable-text{ - display:none; +.ui-state-default.ui-unselectable-text { + display: none; } + .ui-treetable-toggler.fa.fa-fw.ui-clickable.fa-caret-right, .ui-treetable-toggler.fa.fa-fw.ui-clickable.fa-caret-down { display: none; @@ -795,6 +799,20 @@ textarea { .ui-state-default { border: 1px solid $form-color-lighter; } + .ui-treetable tbody td { - white-space:inherit; -} \ No newline at end of file + white-space: inherit; +} + +table a:not(.btn) { + text-decoration: none; +} + +a > h4 { + color: #df691a; + text-decoration: none; +} + +a > h4:hover { + text-decoration: underline; +} diff --git a/src/Ombi/Controllers/IdentityController.cs b/src/Ombi/Controllers/IdentityController.cs index d94f7bd4e..b4321c703 100644 --- a/src/Ombi/Controllers/IdentityController.cs +++ b/src/Ombi/Controllers/IdentityController.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Options; using Ombi.Attributes; using Ombi.Config; +using Ombi.Core.Authentication; using Ombi.Core.Claims; using Ombi.Core.Helpers; using Ombi.Core.Models.UI; @@ -41,7 +42,7 @@ namespace Ombi.Controllers [Produces("application/json")] public class IdentityController : Controller { - public IdentityController(UserManager user, IMapper mapper, RoleManager rm, IEmailProvider prov, + public IdentityController(OmbiUserManager user, IMapper mapper, RoleManager rm, IEmailProvider prov, ISettingsService s, ISettingsService c, IOptions userSettings, @@ -57,7 +58,7 @@ namespace Ombi.Controllers WelcomeEmail = welcome; } - private UserManager UserManager { get; } + private OmbiUserManager UserManager { get; } private RoleManager RoleManager { get; } private IMapper Mapper { get; } private IEmailProvider EmailProvider { get; } @@ -178,7 +179,8 @@ namespace Ombi.Controllers EmailAddress = user.Email, UserType = (Core.Models.UserType)(int)user.UserType, Claims = new List(), - IsSetup = !string.IsNullOrEmpty(user.PasswordHash) + LastLoggedIn = user.LastLoggedIn, + HasLoggedIn = user.LastLoggedIn.HasValue }; foreach (var role in userRoles) diff --git a/src/Ombi/Controllers/TokenController.cs b/src/Ombi/Controllers/TokenController.cs index bbb2379da..1a20af0c9 100644 --- a/src/Ombi/Controllers/TokenController.cs +++ b/src/Ombi/Controllers/TokenController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Ombi.Core.Authentication; using Ombi.Core.Claims; using Ombi.Models; using Ombi.Models.Identity; @@ -21,7 +22,7 @@ namespace Ombi.Controllers [Produces("application/json")] public class TokenController { - public TokenController(UserManager um, IOptions ta, + public TokenController(OmbiUserManager um, IOptions ta, IApplicationConfigRepository config, IAuditRepository audit, ITokenRepository token) { _userManager = um; @@ -35,7 +36,7 @@ namespace Ombi.Controllers private IApplicationConfigRepository _config; private readonly IAuditRepository _audit; private readonly ITokenRepository _token; - private readonly UserManager _userManager; + private readonly OmbiUserManager _userManager; /// /// Gets the token. @@ -65,6 +66,9 @@ namespace Ombi.Controllers return new UnauthorizedResult(); } + user.LastLoggedIn = DateTime.UtcNow; + await _userManager.UpdateAsync(user); + var claims = new List { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), diff --git a/src/Ombi/Program.cs b/src/Ombi/Program.cs index b41975950..2567d4c78 100644 --- a/src/Ombi/Program.cs +++ b/src/Ombi/Program.cs @@ -15,7 +15,6 @@ namespace Ombi public class Program { private static string UrlArgs { get; set; } - private static string WebRoot { get; set; } public static void Main(string[] args) { Console.Title = "Ombi"; @@ -26,16 +25,11 @@ namespace Ombi .WithParsed(o => { host = o.Host; - WebRoot = Path.Combine(o.WebRoot, "wwwroot"); storagePath = o.StoragePath; }); Console.WriteLine(HelpOutput(result)); - if (string.IsNullOrEmpty(WebRoot)) - { - WebRoot = Path.Combine(WebHost.CreateDefaultBuilder().GetSetting("contentRoot"), "wwwroot"); - } UrlArgs = host; var urlValue = string.Empty; @@ -73,7 +67,6 @@ namespace Ombi .UseStartup() .UseUrls(UrlArgs) .PreferHostingUrls(true) - .UseWebRoot(WebRoot) .Build(); private static string HelpOutput(ParserResult args) @@ -100,11 +93,6 @@ namespace Ombi [Option('s', "storage", Required = false, HelpText = "Storage path, where we save the logs and database")] public string StoragePath { get; set; } - - [Option('w', "webroot", Required = false, - HelpText = "(Root Path for Reverse Proxies) If not specified, the default is \"(Working Directory)\", if the path exists. If the path doesn\'t exist, then a no-op file provider is used." - ,Default = "")] - public string WebRoot { get; set; } - + } } diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 87600a3ca..8558f91ee 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -29,6 +29,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.IdentityModel.Tokens; using Ombi.Config; +using Ombi.Core.Authentication; using Ombi.Core.Claims; using Ombi.Core.Settings; using Ombi.DependencyInjection; @@ -85,7 +86,8 @@ namespace Ombi services.AddIdentity() .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + .AddDefaultTokenProviders() + .AddUserManager(); services.Configure(options => {