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
pull/1529/head
tidusjar 7 years ago
parent 9556ed1b34
commit 057683d97a

@ -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);

@ -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<OmbiUser>
{
public OmbiUserManager(IUserStore<OmbiUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<OmbiUser> passwordHasher, IEnumerable<IUserValidator<OmbiUser>> userValidators,
IEnumerable<IPasswordValidator<OmbiUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
IEmbyApi embyApi, ISettingsService<EmbySettings> 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> _embySettings;
public override async Task<bool> 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;
}
/// <summary>
/// Sign the user into plex and make sure we can get the authentication token.
/// <remarks>We do not check if the user is in the owners "friends" since they must have a local user account to get this far</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> 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;
}
/// <summary>
/// Sign the user into Emby
/// <remarks>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</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> 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;
}
}
}

@ -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<int, TvRequests> _dbTv;
protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService,
IRuleEvaluator rules, UserManager<OmbiUser> um) : base(identity, um, rules)
IRuleEvaluator rules, OmbiUserManager um) : base(identity, um, rules)
{
RequestService = requestService;
}

@ -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<OmbiUser> 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<OmbiUser> UserManager { get; }
protected OmbiUserManager UserManager { get; }
protected string Username => UserPrinciple.Identity.Name;
private OmbiUser _user;

@ -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<MovieRequestEngine> log,
UserManager<OmbiUser> manager) : base(user, requestService, r, manager)
INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log,
OmbiUserManager manager) : base(user, requestService, r, manager)
{
MovieApi = movieApi;
NotificationHelper = helper;

@ -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<MovieSearchEngine> logger, IRuleEvaluator r, UserManager<OmbiUser> um)
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um)
: base(identity, service, r, um)
{
MovieApi = movApi;

@ -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<OmbiUser> manager,
IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit) : base(user, requestService, rule, manager)
{
TvApi = tvApi;

@ -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> plexSettings,
ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, UserManager<OmbiUser> um)
ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um)
: base(identity, service, r, um)
{
TvMazeApi = tvMaze;

@ -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<ClaimCheckboxes> 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; }
}

@ -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);
}
}
}

@ -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)
{

@ -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; }
/// <summary>
/// This will be the unique Plex/Emby user id reference
/// </summary>
public string ProviderUserId { get; set; }
public DateTime? LastLoggedIn { get; set; }
[NotMapped]
public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias;
}

@ -0,0 +1,731 @@
// <auto-generated />
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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("Key");
b.Property<string>("ProviderId");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("Title");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int?>("IssueId");
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.HasKey("Id");
b.HasIndex("IssueId");
b.HasIndex("MovieId");
b.ToTable("MovieIssues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<int>("TvId");
b.HasKey("Id");
b.HasIndex("IssueId");
b.HasIndex("TvId");
b.ToTable("TvIssues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

@ -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<DateTime>(
name: "LastLoggedIn",
table: "AspNetUsers",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastLoggedIn",
table: "AspNetUsers");
}
}
}

@ -260,6 +260,8 @@ namespace Ombi.Store.Migrations
b.Property<bool>("EmailConfirmed");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");

@ -8,7 +8,8 @@ export interface IUser {
emailAddress: string;
password: string;
userType: UserType;
isSetup: boolean;
lastLoggedIn: Date;
hasLoggedIn: boolean;
// FOR UI
checked: boolean;
}

@ -54,7 +54,7 @@
<ng-template [ngIf]="result.available"><span class="label label-success">Available</span></ng-template>
<ng-template [ngIf]="result.approved && !result.available"><span class="label label-info">Processing Request</span></ng-template>
<ng-template [ngIf]="result.requested && !result.approved && !result.available"><span class="label label-warning">Pending Approval</span></ng-template>
<ng-template [ngIf]="!result.requested && !result.available"><span class="label label-danger">Not Requested</span></ng-template>
<ng-template [ngIf]="!result.requested && !result.available && !result.approved"><span class="label label-danger">Not Requested</span></ng-template>

@ -14,7 +14,7 @@
<span>Version</span>
</td>
<td>
<span>{{about.version}}</span>
<span>{{about.version}} <a [routerLink]="['/Settings/Update']" *ngIf="newUpdate" style="color:#df691a"><b>(New Update Available)</b></a></span>
</td>
</tr>
<tr>

@ -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;
}
});
}
}

@ -93,7 +93,7 @@ export class RadarrComponent implements OnInit {
}
const settings = <IRadarrSettings>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!");

@ -23,7 +23,7 @@
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="importEmbyUsers" [(ngModel)]="settings.importPlexUsers">
<input type="checkbox" id="importEmbyUsers" [(ngModel)]="settings.importEmbyUsers">
<label for="importEmbyUsers">Import Emby Users</label>
</div>
</div>

@ -27,7 +27,8 @@ export class UserManagementAddComponent implements OnInit {
username: "",
userType: UserType.LocalUser,
checked:false,
isSetup:false,
hasLoggedIn: false,
lastLoggedIn:new Date(),
};
}

@ -27,7 +27,7 @@
<div class="form-group">
<label for="alias" class="control-label">Email Address</label>
<div>
<input type="text" [(ngModel)]="user.emailAddress" class="form-control form-control-custom " id="emailAddress" name="emailAddress" value="{{user?.emailAddress}}">
<input type="text" [(ngModel)]="user.emailAddress" class="form-control form-control-custom " id="emailAddress" name="emailAddress" value="{{user?.emailAddress}}" [disabled]="user?.userType != 1">
</div>
</div>
</div>

@ -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 => {

@ -47,6 +47,9 @@
<th>
Roles
</th>
<th>
Last Logged In
</th>
<th>
<a>
User Type
@ -74,6 +77,9 @@
</div>
</td>
<td>
{{u.lastLoggedIn | date: 'short'}}
</td>
<td ng-hide="hideColumns">
<span *ngIf="u.userType === 1">Local User</span>
<span *ngIf="u.userType === 2">Plex User</span>
@ -83,7 +89,7 @@
<a [routerLink]="['/usermanagement/edit/' + u.id]" class="btn btn-sm btn-info-outline">Details/Edit</a>
</td>
<td>
<a *ngIf="!u.isSetup" (click)="welcomeEmail(u)" class="btn btn-sm btn-info-outline">Send Welcome Email</a>
<button *ngIf="!u.hasLoggedIn" (click)="welcomeEmail(u)" [disabled]="!customizationSettings.applicationUrl" class="btn btn-sm btn-info-outline">Send Welcome Email</button>
</td>
</tr>
</tbody>

@ -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);
}

@ -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;
}
white-space: inherit;
}
table a:not(.btn) {
text-decoration: none;
}
a > h4 {
color: #df691a;
text-decoration: none;
}
a > h4:hover {
text-decoration: underline;
}

@ -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<OmbiUser> user, IMapper mapper, RoleManager<IdentityRole> rm, IEmailProvider prov,
public IdentityController(OmbiUserManager user, IMapper mapper, RoleManager<IdentityRole> rm, IEmailProvider prov,
ISettingsService<EmailNotificationSettings> s,
ISettingsService<CustomizationSettings> c,
IOptions<UserSettings> userSettings,
@ -57,7 +58,7 @@ namespace Ombi.Controllers
WelcomeEmail = welcome;
}
private UserManager<OmbiUser> UserManager { get; }
private OmbiUserManager UserManager { get; }
private RoleManager<IdentityRole> 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<ClaimCheckboxes>(),
IsSetup = !string.IsNullOrEmpty(user.PasswordHash)
LastLoggedIn = user.LastLoggedIn,
HasLoggedIn = user.LastLoggedIn.HasValue
};
foreach (var role in userRoles)

@ -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<OmbiUser> um, IOptions<TokenAuthentication> ta,
public TokenController(OmbiUserManager um, IOptions<TokenAuthentication> 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<OmbiUser> _userManager;
private readonly OmbiUserManager _userManager;
/// <summary>
/// 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<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),

@ -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<Startup>()
.UseUrls(UrlArgs)
.PreferHostingUrls(true)
.UseWebRoot(WebRoot)
.Build();
private static string HelpOutput(ParserResult<Options> 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; }
}
}

@ -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<OmbiUser, IdentityRole>()
.AddEntityFrameworkStores<OmbiContext>()
.AddDefaultTokenProviders();
.AddDefaultTokenProviders()
.AddUserManager<OmbiUserManager>();
services.Configure<IdentityOptions>(options =>
{

Loading…
Cancel
Save